3642 lines
136 KiB
Rust
3642 lines
136 KiB
Rust
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
//! Engine is container for all subsystems (renderer, ui, sound, resource manager). It also
|
|
//! creates a window and an OpenGL context.
|
|
|
|
#![warn(missing_docs)]
|
|
|
|
pub mod error;
|
|
pub mod executor;
|
|
pub mod input;
|
|
pub mod task;
|
|
|
|
mod hotreload;
|
|
mod wasm_utils;
|
|
|
|
use crate::plugin::error::{GameError, GameResult};
|
|
use crate::{
|
|
asset::{
|
|
event::ResourceEvent,
|
|
manager::{ResourceManager, ResourceWaitContext},
|
|
state::ResourceState,
|
|
untyped::ResourceKind,
|
|
},
|
|
core::{
|
|
algebra::Vector2,
|
|
err,
|
|
futures::{executor::block_on, future::join_all},
|
|
instant,
|
|
log::Log,
|
|
pool::Handle,
|
|
reflect::prelude::*,
|
|
reflect::Reflect,
|
|
task::TaskPool,
|
|
warn, SafeLock,
|
|
},
|
|
engine::{error::EngineError, input::InputState, task::TaskPoolHandler},
|
|
event::Event,
|
|
graph::SceneGraph,
|
|
graphics::error::FrameworkError,
|
|
gui::{
|
|
constructor::WidgetConstructorContainer,
|
|
font::{
|
|
loader::FontLoader, Font, BOLD_ITALIC, BUILT_IN_BOLD, BUILT_IN_FONT, BUILT_IN_ITALIC,
|
|
},
|
|
loader::UserInterfaceLoader,
|
|
style::{self, resource::StyleLoader, Style},
|
|
RenderMode, UiContainer, UiUpdateSwitches, UserInterface,
|
|
},
|
|
material::{
|
|
self,
|
|
loader::MaterialLoader,
|
|
shader::{loader::ShaderLoader, Shader, ShaderResource, ShaderResourceExtension},
|
|
Material,
|
|
},
|
|
plugin::{
|
|
dylib::DyLibDynamicPlugin, DynamicPlugin, Plugin, PluginContainer, PluginContext,
|
|
PluginRegistrationContext,
|
|
},
|
|
renderer::{ui_renderer::UiRenderInfo, Renderer},
|
|
resource::{
|
|
curve::{loader::CurveLoader, CurveResourceState},
|
|
gltf::material::GLTF_SHADER,
|
|
model::{loader::ModelLoader, Model, ModelResource},
|
|
texture::{
|
|
self, loader::TextureLoader, CompressionOptions, Texture, TextureImportOptions,
|
|
TextureKind, TextureMinificationFilter, TextureResource, TextureResourceExtension,
|
|
},
|
|
},
|
|
scene::{
|
|
base::NodeScriptMessage,
|
|
graph::GraphUpdateSwitches,
|
|
mesh::surface::{self, SurfaceData, SurfaceDataLoader},
|
|
node::{
|
|
constructor::{new_node_constructor_container, NodeConstructorContainer},
|
|
Node,
|
|
},
|
|
skybox::SkyBoxKind,
|
|
sound::SoundEngine,
|
|
tilemap::{
|
|
brush::{TileMapBrush, TileMapBrushLoader},
|
|
tileset::{TileSet, TileSetLoader},
|
|
CustomTileCollider, TileMapData,
|
|
},
|
|
Scene, SceneContainer,
|
|
},
|
|
script::{
|
|
constructor::ScriptConstructorContainer, DynamicTypeId, PluginsRefMut, RoutingStrategy,
|
|
Script, ScriptContext, ScriptDeinitContext, ScriptMessage, ScriptMessageContext,
|
|
ScriptMessageKind, ScriptMessageSender, UniversalScriptContext,
|
|
},
|
|
window::Window,
|
|
};
|
|
use fxhash::{FxHashMap, FxHashSet};
|
|
use fyrox_animation::AnimationTracksData;
|
|
use fyrox_core::dyntype::DynTypeConstructorContainer;
|
|
use fyrox_core::NameProvider;
|
|
use fyrox_graphics::server::SharedGraphicsServer;
|
|
use fyrox_graphics_gl::server::GlGraphicsServer;
|
|
use fyrox_sound::{
|
|
buffer::{loader::SoundBufferLoader, SoundBuffer},
|
|
renderer::hrtf::{HrirSphereLoader, HrirSphereResourceData},
|
|
};
|
|
use std::fmt::Debug;
|
|
use std::{
|
|
any::TypeId,
|
|
cell::Cell,
|
|
collections::{HashSet, VecDeque},
|
|
fmt::{Display, Formatter},
|
|
io::Cursor,
|
|
ops::{Deref, DerefMut},
|
|
path::Path,
|
|
rc::Rc,
|
|
sync::{
|
|
mpsc::{channel, Receiver},
|
|
Arc,
|
|
},
|
|
time::Duration,
|
|
};
|
|
use uuid::Uuid;
|
|
use winit::event::{DeviceEvent, ElementState, WindowEvent};
|
|
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
|
use winit::{
|
|
dpi::{Position, Size},
|
|
window::Icon,
|
|
window::WindowAttributes,
|
|
};
|
|
|
|
/// Serialization context holds runtime type information that allows to create unknown types using
|
|
/// their UUIDs and a respective constructors.
|
|
#[derive(Reflect)]
|
|
#[reflect(
|
|
hide_all,
|
|
non_cloneable,
|
|
non_comparable,
|
|
type_uuid = "1973a8c9-6ac6-48a2-bca1-9b4f3cce0774"
|
|
)]
|
|
pub struct SerializationContext {
|
|
/// A node constructor container.
|
|
pub node_constructors: NodeConstructorContainer,
|
|
/// A script constructor container.
|
|
pub script_constructors: ScriptConstructorContainer,
|
|
}
|
|
|
|
impl Debug for SerializationContext {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "SerializationContext")
|
|
}
|
|
}
|
|
|
|
impl Default for SerializationContext {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl SerializationContext {
|
|
/// Creates default serialization context.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
node_constructors: new_node_constructor_container(),
|
|
script_constructors: ScriptConstructorContainer::new(),
|
|
}
|
|
}
|
|
|
|
/// Removes all registered constructors.
|
|
pub fn clear(&self) {
|
|
self.node_constructors.clear();
|
|
self.script_constructors.clear();
|
|
}
|
|
}
|
|
|
|
/// Performance statistics.
|
|
#[derive(Debug, Default)]
|
|
pub struct PerformanceStatistics {
|
|
/// Amount of time spent in the UI system.
|
|
pub ui_time: Duration,
|
|
|
|
/// Amount of time spent in updating/initializing/destructing scripts of all scenes.
|
|
pub scripts_time: Duration,
|
|
|
|
/// Amount of time spent in plugins updating.
|
|
pub plugins_time: Duration,
|
|
}
|
|
|
|
impl Display for PerformanceStatistics {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(
|
|
f,
|
|
"Performance Statistics:\n\tUI: {:?}\n\tScripts: {:?}\n\tPlugins: {:?}",
|
|
self.ui_time, self.scripts_time, self.plugins_time
|
|
)
|
|
}
|
|
}
|
|
|
|
/// An initialized graphics context. It contains the main application window and the renderer instance.
|
|
pub struct InitializedGraphicsContext {
|
|
/// Main application window.
|
|
pub window: Window,
|
|
|
|
/// Current renderer.
|
|
pub renderer: Renderer,
|
|
|
|
params: GraphicsContextParams,
|
|
}
|
|
|
|
impl InitializedGraphicsContext {
|
|
/// Tries to set a new icon for the window from the given data source. The data source must contain
|
|
/// some of the supported texture types data (png, bmp, jpg images). You can call this method
|
|
/// with [`include_bytes`] macro to pass file's data directly.
|
|
pub fn set_window_icon_from_memory(&mut self, data: &[u8]) {
|
|
if let Ok(texture) = TextureResource::load_from_memory(
|
|
Uuid::new_v4(),
|
|
ResourceKind::Embedded,
|
|
data,
|
|
TextureImportOptions::default()
|
|
.with_compression(CompressionOptions::NoCompression)
|
|
.with_minification_filter(TextureMinificationFilter::Linear),
|
|
) {
|
|
self.set_window_icon_from_texture(&texture);
|
|
}
|
|
}
|
|
|
|
/// Tries to set a new icon for the window using the given texture.
|
|
pub fn set_window_icon_from_texture(&mut self, texture: &TextureResource) {
|
|
let data = texture.data_ref();
|
|
if let TextureKind::Rectangle { width, height } = data.kind() {
|
|
if let Ok(img) = Icon::from_rgba(data.data().to_vec(), width, height) {
|
|
self.window.set_window_icon(Some(img));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Graphics context of the engine, it could be in two main states:
|
|
///
|
|
/// - [`GraphicsContext::Initialized`] - active graphics context, that is fully initialized and ready for use.
|
|
/// - [`GraphicsContext::Uninitialized`] - suspended graphics context, that contains a set of params that could
|
|
/// be used for further initialization.
|
|
///
|
|
/// By default, when you creating an engine, there's no graphics context initialized. It must be initialized
|
|
/// manually (if you need it) on [`Event::Resumed`]. On most operating systems, it is possible to initialize
|
|
/// graphics context right after the engine was created. However Android won't allow you to do this, also on
|
|
/// some versions of macOS immediate initialization could lead to panic.
|
|
///
|
|
/// You can switch between these states whenever you need, for example if your application does not need a
|
|
/// window and a renderer at all you can just not create graphics context. This could be useful for game
|
|
/// servers or background applications. When you destroy a graphics context, the engine will remember the options
|
|
/// with which it was created and some of the main window parameters (position, size, etc.) and will re-use these
|
|
/// parameters on a next initialization attempt.
|
|
#[allow(clippy::large_enum_variant)]
|
|
pub enum GraphicsContext {
|
|
/// Fully initialized graphics context. See [`InitializedGraphicsContext`] docs for more info.
|
|
Initialized(InitializedGraphicsContext),
|
|
|
|
/// Uninitialized (suspended) graphics context. See [`GraphicsContextParams`] docs for more info.
|
|
Uninitialized(GraphicsContextParams),
|
|
}
|
|
|
|
impl GraphicsContext {
|
|
/// Attempts to cast a graphics context to its initialized version. The method will panic if the context
|
|
/// is not initialized.
|
|
pub fn as_initialized_ref(&self) -> &InitializedGraphicsContext {
|
|
if let GraphicsContext::Initialized(ctx) = self {
|
|
ctx
|
|
} else {
|
|
panic!("Graphics context is uninitialized!")
|
|
}
|
|
}
|
|
|
|
/// Attempts to cast a graphics context to its initialized version. The method will panic if the context
|
|
/// is not initialized.
|
|
pub fn as_initialized_mut(&mut self) -> &mut InitializedGraphicsContext {
|
|
if let GraphicsContext::Initialized(ctx) = self {
|
|
ctx
|
|
} else {
|
|
panic!("Graphics context is uninitialized!")
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) enum GameErrorSource {
|
|
PluginMethod(&'static str),
|
|
ScriptMethod {
|
|
scene_handle: Handle<Scene>,
|
|
node_handle: Handle<Node>,
|
|
method_name: &'static str,
|
|
},
|
|
}
|
|
|
|
pub(crate) struct GameErrorContainer {
|
|
source: GameErrorSource,
|
|
error: GameError,
|
|
}
|
|
|
|
type ErrorQueue = VecDeque<GameErrorContainer>;
|
|
|
|
/// See module docs.
|
|
pub struct Engine {
|
|
/// Graphics context of the engine. See [`GraphicsContext`] docs for more info.
|
|
pub graphics_context: GraphicsContext,
|
|
|
|
/// Current resource manager. Resource manager can be cloned (it does clone only ref) to be able to
|
|
/// use resource manager from any thread, this is useful to load resources from multiple
|
|
/// threads to decrease loading times of your game by utilizing all available power of
|
|
/// your CPU.
|
|
pub resource_manager: ResourceManager,
|
|
|
|
/// All available user interfaces in the engine.
|
|
pub user_interfaces: UiContainer,
|
|
|
|
/// All available scenes in the engine.
|
|
pub scenes: SceneContainer,
|
|
|
|
/// Task pool for asynchronous task management.
|
|
pub task_pool: TaskPoolHandler,
|
|
|
|
performance_statistics: PerformanceStatistics,
|
|
|
|
model_events_receiver: Receiver<ResourceEvent>,
|
|
|
|
sound_engine: SoundEngine,
|
|
|
|
// A set of plugins used by the engine.
|
|
plugins: Vec<PluginContainer>,
|
|
|
|
plugins_enabled: bool,
|
|
|
|
// Amount of time (in seconds) that passed from creation of the engine.
|
|
elapsed_time: f32,
|
|
|
|
input_state: InputState,
|
|
|
|
/// A special container that is able to create nodes by their type UUID. Use a copy of this
|
|
/// value whenever you need it as a parameter in other parts of the engine.
|
|
pub serialization_context: Arc<SerializationContext>,
|
|
|
|
/// A container with widget constructors.
|
|
pub widget_constructors: Arc<WidgetConstructorContainer>,
|
|
|
|
/// A container with constructors for dynamic types. See [`DynTypeConstructorContainer`] for more
|
|
/// info.
|
|
pub dyn_type_constructors: Arc<DynTypeConstructorContainer>,
|
|
|
|
/// Script processor is used to run script methods in a strict order.
|
|
pub script_processor: ScriptProcessor,
|
|
|
|
error_queue: ErrorQueue,
|
|
}
|
|
|
|
#[derive(Debug, Hash, PartialEq, Eq)]
|
|
enum MessageTypeId {
|
|
Static(TypeId),
|
|
Dynamic(DynamicTypeId),
|
|
}
|
|
|
|
/// Performs dispatch of script messages.
|
|
pub struct ScriptMessageDispatcher {
|
|
type_groups: FxHashMap<MessageTypeId, FxHashSet<Handle<Node>>>,
|
|
message_receiver: Receiver<ScriptMessage>,
|
|
}
|
|
|
|
fn try_enqueue_plugin_error(method_name: &'static str, result: GameResult, queue: &mut ErrorQueue) {
|
|
if let Err(error) = result {
|
|
queue.push_back(GameErrorContainer {
|
|
source: GameErrorSource::PluginMethod(method_name),
|
|
error,
|
|
});
|
|
}
|
|
}
|
|
|
|
impl ScriptMessageDispatcher {
|
|
fn new(message_receiver: Receiver<ScriptMessage>) -> Self {
|
|
Self {
|
|
type_groups: Default::default(),
|
|
message_receiver,
|
|
}
|
|
}
|
|
|
|
/// Subscribes a node to receive any message of the given type `T`. Subscription is automatically removed
|
|
/// if the node dies.
|
|
pub fn subscribe_to<T: 'static>(&mut self, receiver: Handle<Node>) {
|
|
self.subscribe_internal_to(receiver, MessageTypeId::Static(TypeId::of::<T>()));
|
|
}
|
|
|
|
/// Subscribes a node to receive any message of the given type `type_id`. Subscription is automatically removed
|
|
/// if the node dies.
|
|
pub fn subscribe_dynamic_to(&mut self, receiver: Handle<Node>, type_id: DynamicTypeId) {
|
|
self.subscribe_internal_to(receiver, MessageTypeId::Dynamic(type_id));
|
|
}
|
|
|
|
#[inline]
|
|
fn subscribe_internal_to(&mut self, receiver: Handle<Node>, type_id: MessageTypeId) {
|
|
self.type_groups
|
|
.entry(type_id)
|
|
.and_modify(|v| {
|
|
v.insert(receiver);
|
|
})
|
|
.or_insert_with(|| FxHashSet::from_iter([receiver]));
|
|
}
|
|
|
|
/// Unsubscribes a node from receiving any messages of the given type `T`.
|
|
pub fn unsubscribe_from<T: 'static>(&mut self, receiver: Handle<Node>) {
|
|
self.unsubscribe_internal_from(receiver, &MessageTypeId::Static(TypeId::of::<T>()));
|
|
}
|
|
|
|
/// Unsubscribes a node from receiving any messages of the given type `type_id`.
|
|
pub fn unsubscribe_dynamic_from(&mut self, receiver: Handle<Node>, type_id: DynamicTypeId) {
|
|
self.unsubscribe_internal_from(receiver, &MessageTypeId::Dynamic(type_id));
|
|
}
|
|
|
|
#[inline]
|
|
fn unsubscribe_internal_from(&mut self, receiver: Handle<Node>, type_id: &MessageTypeId) {
|
|
if let Some(group) = self.type_groups.get_mut(type_id) {
|
|
group.remove(&receiver);
|
|
}
|
|
}
|
|
|
|
/// Unsubscribes a node from receiving any messages.
|
|
pub fn unsubscribe(&mut self, receiver: Handle<Node>) {
|
|
for group in self.type_groups.values_mut() {
|
|
group.remove(&receiver);
|
|
}
|
|
}
|
|
|
|
fn dispatch_messages(
|
|
&self,
|
|
scene: &mut Scene,
|
|
scene_handle: Handle<Scene>,
|
|
plugins: &mut [PluginContainer],
|
|
resource_manager: &ResourceManager,
|
|
dt: f32,
|
|
elapsed_time: f32,
|
|
message_sender: &ScriptMessageSender,
|
|
user_interfaces: &mut UiContainer,
|
|
graphics_context: &mut GraphicsContext,
|
|
task_pool: &mut TaskPoolHandler,
|
|
input_state: &InputState,
|
|
error_queue: &mut ErrorQueue,
|
|
) {
|
|
while let Ok(message) = self.message_receiver.try_recv() {
|
|
let type_id = match message.payload.get_dynamic_type_id() {
|
|
Some(it) => MessageTypeId::Dynamic(it),
|
|
None => MessageTypeId::Static(message.payload.deref().type_id()),
|
|
};
|
|
let receivers = self.type_groups.get(&type_id);
|
|
|
|
if receivers.is_none_or(|r| r.is_empty()) {
|
|
Log::warn(format!(
|
|
"Script message {message:?} was sent, but there's no receivers. \
|
|
Did you forgot to subscribe your script to the message?"
|
|
));
|
|
}
|
|
|
|
if let Some(receivers) = receivers {
|
|
let mut payload = message.payload;
|
|
|
|
match message.kind {
|
|
ScriptMessageKind::Targeted(target) => {
|
|
if receivers.contains(&target) {
|
|
let mut context = ScriptMessageContext {
|
|
dt,
|
|
elapsed_time,
|
|
plugins: PluginsRefMut(plugins),
|
|
handle: target,
|
|
scene,
|
|
scene_handle,
|
|
resource_manager,
|
|
message_sender,
|
|
task_pool,
|
|
graphics_context,
|
|
user_interfaces,
|
|
script_index: 0,
|
|
input_state,
|
|
};
|
|
|
|
process_node_scripts(
|
|
"on_message",
|
|
&mut context,
|
|
scene_handle,
|
|
error_queue,
|
|
&mut |s, ctx| s.on_message(&mut *payload, ctx),
|
|
)
|
|
}
|
|
}
|
|
ScriptMessageKind::Hierarchical { root, routing } => match routing {
|
|
RoutingStrategy::Up => {
|
|
let mut node = root;
|
|
while let Ok(node_ref) = scene.graph.try_get_node(node) {
|
|
let parent = node_ref.parent();
|
|
|
|
let mut context = ScriptMessageContext {
|
|
dt,
|
|
elapsed_time,
|
|
plugins: PluginsRefMut(plugins),
|
|
handle: node,
|
|
scene,
|
|
scene_handle,
|
|
resource_manager,
|
|
message_sender,
|
|
task_pool,
|
|
graphics_context,
|
|
user_interfaces,
|
|
script_index: 0,
|
|
input_state,
|
|
};
|
|
|
|
if receivers.contains(&node) {
|
|
process_node_scripts(
|
|
"on_message",
|
|
&mut context,
|
|
scene_handle,
|
|
error_queue,
|
|
&mut |s, ctx| s.on_message(&mut *payload, ctx),
|
|
);
|
|
}
|
|
|
|
node = parent;
|
|
}
|
|
}
|
|
RoutingStrategy::Down => {
|
|
for node in scene.graph.traverse_handle_iter(root).collect::<Vec<_>>() {
|
|
let mut context = ScriptMessageContext {
|
|
dt,
|
|
elapsed_time,
|
|
plugins: PluginsRefMut(plugins),
|
|
handle: node,
|
|
scene,
|
|
scene_handle,
|
|
resource_manager,
|
|
message_sender,
|
|
task_pool,
|
|
graphics_context,
|
|
user_interfaces,
|
|
script_index: 0,
|
|
input_state,
|
|
};
|
|
|
|
if receivers.contains(&node) {
|
|
process_node_scripts(
|
|
"on_message",
|
|
&mut context,
|
|
scene_handle,
|
|
error_queue,
|
|
&mut |s, ctx| s.on_message(&mut *payload, ctx),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
ScriptMessageKind::Global => {
|
|
for &node in receivers {
|
|
let mut context = ScriptMessageContext {
|
|
dt,
|
|
elapsed_time,
|
|
plugins: PluginsRefMut(plugins),
|
|
handle: node,
|
|
scene,
|
|
scene_handle,
|
|
resource_manager,
|
|
message_sender,
|
|
task_pool,
|
|
graphics_context,
|
|
user_interfaces,
|
|
script_index: 0,
|
|
input_state,
|
|
};
|
|
|
|
process_node_scripts(
|
|
"on_message",
|
|
&mut context,
|
|
scene_handle,
|
|
error_queue,
|
|
&mut |s, ctx| s.on_message(&mut *payload, ctx),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Scripted scene is a handle to scene with some additional data associated with it.
|
|
pub struct ScriptedScene {
|
|
/// Handle of a scene.
|
|
pub handle: Handle<Scene>,
|
|
/// Script message sender.
|
|
pub message_sender: ScriptMessageSender,
|
|
message_dispatcher: ScriptMessageDispatcher,
|
|
}
|
|
|
|
/// Script processor is used to run script methods in a strict order.
|
|
#[derive(Default)]
|
|
pub struct ScriptProcessor {
|
|
wait_list: Vec<ResourceWaitContext>,
|
|
/// A list of scenes.
|
|
pub scripted_scenes: Vec<ScriptedScene>,
|
|
}
|
|
|
|
impl ScriptProcessor {
|
|
fn has_scripted_scene(&self, scene: Handle<Scene>) -> bool {
|
|
self.scripted_scenes.iter().any(|s| s.handle == scene)
|
|
}
|
|
|
|
fn register_scripted_scene(
|
|
&mut self,
|
|
scene: Handle<Scene>,
|
|
resource_manager: &ResourceManager,
|
|
) {
|
|
// Ensure that the scene wasn't registered previously.
|
|
assert!(!self.has_scripted_scene(scene));
|
|
|
|
let (tx, rx) = channel();
|
|
self.scripted_scenes.push(ScriptedScene {
|
|
handle: scene,
|
|
message_sender: ScriptMessageSender { sender: tx },
|
|
message_dispatcher: ScriptMessageDispatcher::new(rx),
|
|
});
|
|
|
|
self.wait_list
|
|
.push(resource_manager.state().get_wait_context());
|
|
}
|
|
|
|
fn handle_scripts(
|
|
&mut self,
|
|
scenes: &mut SceneContainer,
|
|
plugins: &mut [PluginContainer],
|
|
resource_manager: &ResourceManager,
|
|
task_pool: &mut TaskPoolHandler,
|
|
graphics_context: &mut GraphicsContext,
|
|
user_interfaces: &mut UiContainer,
|
|
dt: f32,
|
|
elapsed_time: f32,
|
|
input_state: &InputState,
|
|
error_queue: &mut ErrorQueue,
|
|
) {
|
|
self.wait_list
|
|
.retain_mut(|context| !context.is_all_loaded());
|
|
|
|
if !self.wait_list.is_empty() {
|
|
return;
|
|
}
|
|
|
|
self.scripted_scenes
|
|
.retain(|s| scenes.is_valid_handle(s.handle));
|
|
|
|
'scene_loop: for scripted_scene in self.scripted_scenes.iter_mut() {
|
|
let scene = &mut scenes[scripted_scene.handle];
|
|
|
|
// Disabled scenes should not update their scripts.
|
|
if !*scene.enabled {
|
|
continue 'scene_loop;
|
|
}
|
|
|
|
// Fill in initial handles to nodes to initialize, start, update.
|
|
let mut update_queue = VecDeque::new();
|
|
let mut start_queue = VecDeque::new();
|
|
let script_message_sender = scene.graph.script_message_sender.clone();
|
|
for (handle, node) in scene.graph.pair_iter_mut() {
|
|
// Remove unused script entries.
|
|
node.scripts
|
|
.retain(|e| e.script.is_some() && !e.should_be_deleted);
|
|
|
|
if node.is_globally_enabled() {
|
|
for (i, entry) in node.scripts.iter().enumerate() {
|
|
if let Some(script) = entry.script.as_ref() {
|
|
if script.initialized {
|
|
if script.started {
|
|
update_queue.push_back((handle, i));
|
|
} else {
|
|
start_queue.push_back((handle, i));
|
|
}
|
|
} else {
|
|
script_message_sender
|
|
.send(NodeScriptMessage::InitializeScript {
|
|
handle,
|
|
script_index: i,
|
|
})
|
|
.unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// We'll gather all scripts queued for destruction and destroy them all at once at the
|
|
// end of the frame.
|
|
let mut destruction_queue = VecDeque::new();
|
|
|
|
let max_iterations = 64;
|
|
|
|
'update_loop: for update_loop_iteration in 0..max_iterations {
|
|
let mut context = ScriptContext {
|
|
dt,
|
|
elapsed_time,
|
|
plugins: PluginsRefMut(plugins),
|
|
handle: Default::default(),
|
|
scene,
|
|
scene_handle: scripted_scene.handle,
|
|
resource_manager,
|
|
message_sender: &scripted_scene.message_sender,
|
|
message_dispatcher: &mut scripted_scene.message_dispatcher,
|
|
task_pool,
|
|
graphics_context,
|
|
user_interfaces,
|
|
script_index: 0,
|
|
input_state,
|
|
};
|
|
|
|
'init_loop: for init_loop_iteration in 0..max_iterations {
|
|
// Process events first. `on_init` of a script can also create some other instances
|
|
// and these will be correctly initialized on current frame.
|
|
while let Ok(event) = context.scene.graph.script_message_receiver.try_recv() {
|
|
match event {
|
|
NodeScriptMessage::InitializeScript {
|
|
handle,
|
|
script_index,
|
|
} => {
|
|
context.handle = handle;
|
|
context.script_index = script_index;
|
|
|
|
process_node_script(
|
|
"on_init",
|
|
script_index,
|
|
scripted_scene.handle,
|
|
&mut context,
|
|
error_queue,
|
|
&mut |script, context| {
|
|
if !script.initialized {
|
|
script.on_init(context)?;
|
|
script.initialized = true;
|
|
}
|
|
|
|
// `on_start` must be called even if the script was initialized.
|
|
start_queue.push_back((handle, script_index));
|
|
|
|
Ok(())
|
|
},
|
|
);
|
|
}
|
|
NodeScriptMessage::DestroyScript {
|
|
handle,
|
|
script,
|
|
script_index,
|
|
} => {
|
|
// Destruction is delayed to the end of the frame.
|
|
destruction_queue.push_back((handle, script, script_index));
|
|
}
|
|
}
|
|
}
|
|
|
|
if start_queue.is_empty() {
|
|
// There is no more new nodes, we can safely leave the init loop.
|
|
break 'init_loop;
|
|
} else {
|
|
// Call `on_start` for every recently initialized node and go to next
|
|
// iteration of init loop. This is needed because `on_start` can spawn
|
|
// some other nodes that must be initialized before update.
|
|
while let Some((handle, script_index)) = start_queue.pop_front() {
|
|
context.handle = handle;
|
|
context.script_index = script_index;
|
|
|
|
process_node_script(
|
|
"on_start",
|
|
script_index,
|
|
scripted_scene.handle,
|
|
&mut context,
|
|
error_queue,
|
|
&mut |script, context| {
|
|
if script.initialized && !script.started {
|
|
let result = script.on_start(context);
|
|
script.started = true;
|
|
update_queue.push_back((handle, script_index));
|
|
result
|
|
} else {
|
|
Ok(())
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
if init_loop_iteration == max_iterations - 1 {
|
|
Log::warn(
|
|
"Infinite init loop detected! Most likely some of \
|
|
your scripts causing infinite prefab instantiation!",
|
|
)
|
|
}
|
|
}
|
|
|
|
// Update all initialized and started scripts until there is something to initialize.
|
|
if update_queue.is_empty() {
|
|
break 'update_loop;
|
|
} else {
|
|
while let Some((handle, script_index)) = update_queue.pop_front() {
|
|
context.handle = handle;
|
|
context.script_index = script_index;
|
|
|
|
process_node_script(
|
|
"on_update",
|
|
script_index,
|
|
scripted_scene.handle,
|
|
&mut context,
|
|
error_queue,
|
|
&mut |script, context| script.on_update(context),
|
|
);
|
|
}
|
|
}
|
|
|
|
if update_loop_iteration == max_iterations - 1 {
|
|
Log::warn(
|
|
"Infinite update loop detected! Most likely some of \
|
|
your scripts causing infinite prefab instantiation!",
|
|
)
|
|
}
|
|
}
|
|
|
|
// Dispatch script messages only when everything is initialized and updated. This has to
|
|
// be done this way, because all those methods could spawn new messages. However, if a new
|
|
// message is spawned directly in `on_message` the dispatcher will correctly handle it
|
|
// on this frame, since it will be placed in the common queue anyway.
|
|
scripted_scene.message_dispatcher.dispatch_messages(
|
|
scene,
|
|
scripted_scene.handle,
|
|
plugins,
|
|
resource_manager,
|
|
dt,
|
|
elapsed_time,
|
|
&scripted_scene.message_sender,
|
|
user_interfaces,
|
|
graphics_context,
|
|
task_pool,
|
|
input_state,
|
|
error_queue,
|
|
);
|
|
|
|
// As the last step, destroy queued scripts.
|
|
let mut context = ScriptDeinitContext {
|
|
elapsed_time,
|
|
plugins: PluginsRefMut(plugins),
|
|
resource_manager,
|
|
scene,
|
|
scene_handle: scripted_scene.handle,
|
|
node_handle: Default::default(),
|
|
message_sender: &scripted_scene.message_sender,
|
|
user_interfaces,
|
|
graphics_context,
|
|
task_pool,
|
|
script_index: 0,
|
|
input_state,
|
|
};
|
|
while let Some((handle, mut script, index)) = destruction_queue.pop_front() {
|
|
context.node_handle = handle;
|
|
context.script_index = index;
|
|
|
|
// Unregister self in message dispatcher.
|
|
scripted_scene.message_dispatcher.unsubscribe(handle);
|
|
|
|
// `on_deinit` could also spawn new nodes, but we won't take those into account on
|
|
// this frame. They'll be correctly handled on next frame.
|
|
if let Err(error) = script.on_deinit(&mut context) {
|
|
error_queue.push_back(GameErrorContainer {
|
|
source: GameErrorSource::ScriptMethod {
|
|
scene_handle: scripted_scene.handle,
|
|
node_handle: handle,
|
|
method_name: "on_deinit",
|
|
},
|
|
error,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process scripts from destroyed scenes.
|
|
for (handle, mut detached_scene) in scenes.destruction_list.drain(..) {
|
|
if let Some(scripted_scene) = self.scripted_scenes.iter().find(|s| s.handle == handle) {
|
|
let mut context = ScriptDeinitContext {
|
|
elapsed_time,
|
|
plugins: PluginsRefMut(plugins),
|
|
resource_manager,
|
|
scene: &mut detached_scene,
|
|
scene_handle: scripted_scene.handle,
|
|
node_handle: Default::default(),
|
|
message_sender: &scripted_scene.message_sender,
|
|
task_pool,
|
|
graphics_context,
|
|
user_interfaces,
|
|
script_index: 0,
|
|
input_state,
|
|
};
|
|
|
|
// Destroy every script instance from nodes that were still alive.
|
|
for node_index in 0..context.scene.graph.capacity() {
|
|
let handle_node = context.scene.graph.handle_from_index(node_index);
|
|
context.node_handle = handle_node;
|
|
|
|
process_node_scripts(
|
|
"on_deinit",
|
|
&mut context,
|
|
scripted_scene.handle,
|
|
error_queue,
|
|
&mut |script, context| {
|
|
if script.initialized {
|
|
script.on_deinit(context)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ResourceGraphVertex {
|
|
resource: ModelResource,
|
|
children: Vec<ResourceGraphVertex>,
|
|
}
|
|
|
|
impl ResourceGraphVertex {
|
|
pub fn new(model: ModelResource, resource_manager: ResourceManager) -> Self {
|
|
let mut children = Vec::new();
|
|
|
|
// Look for dependent resources.
|
|
let mut dependent_resources = HashSet::new();
|
|
for resource in resource_manager.state().iter() {
|
|
if let Some(other_model) = resource.try_cast::<Model>() {
|
|
let mut state = other_model.state();
|
|
if let Some(model_data) = state.data() {
|
|
if model_data
|
|
.get_scene()
|
|
.graph
|
|
.linear_iter()
|
|
.any(|n| n.resource.as_ref() == Some(&model))
|
|
{
|
|
dependent_resources.insert(other_model.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
children.extend(
|
|
dependent_resources
|
|
.into_iter()
|
|
.map(|r| ResourceGraphVertex::new(r, resource_manager.clone())),
|
|
);
|
|
|
|
Self {
|
|
resource: model,
|
|
children,
|
|
}
|
|
}
|
|
|
|
pub fn resolve(&self) {
|
|
Log::info(format!(
|
|
"Resolving {} resource from dependency graph...",
|
|
self.resource.resource_uuid()
|
|
));
|
|
|
|
// Wait until resource is fully loaded, then resolve.
|
|
if block_on(self.resource.clone()).is_ok() {
|
|
self.resource.data_ref().get_scene_mut();
|
|
|
|
for child in self.children.iter() {
|
|
child.resolve();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ResourceDependencyGraph {
|
|
root: ResourceGraphVertex,
|
|
}
|
|
|
|
impl ResourceDependencyGraph {
|
|
pub fn new(model: ModelResource, resource_manager: ResourceManager) -> Self {
|
|
Self {
|
|
root: ResourceGraphVertex::new(model, resource_manager),
|
|
}
|
|
}
|
|
|
|
pub fn resolve(&self) {
|
|
self.root.resolve()
|
|
}
|
|
}
|
|
|
|
/// A result returned by a graphics server constructor.
|
|
pub type GraphicsServerConstructorResult = Result<(Window, SharedGraphicsServer), FrameworkError>;
|
|
|
|
/// Graphics server constructor callback responsible for actual server creation. Graphics server
|
|
/// initialization usually tied together with window creation on some operating systems, that's why
|
|
/// the constructor accepts window builder and must return the window built with the builder as well
|
|
/// as the server.
|
|
pub type GraphicsServerConstructorCallback = dyn Fn(
|
|
&GraphicsContextParams,
|
|
&ActiveEventLoop,
|
|
WindowAttributes,
|
|
bool,
|
|
) -> GraphicsServerConstructorResult;
|
|
|
|
/// Graphics server constructor is used to initialize different graphics servers in unified manner.
|
|
/// [`Default`] trait implementation currently creates OpenGL graphics server.
|
|
#[derive(Clone)]
|
|
pub struct GraphicsServerConstructor(Rc<GraphicsServerConstructorCallback>);
|
|
|
|
impl Default for GraphicsServerConstructor {
|
|
fn default() -> Self {
|
|
Self(Rc::new(
|
|
|params, window_target, window_builder, named_objects| {
|
|
GlGraphicsServer::new(
|
|
params.vsync,
|
|
params.msaa_sample_count,
|
|
window_target,
|
|
window_builder,
|
|
named_objects,
|
|
)
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
/// A set of parameters that could be used to initialize graphics context.
|
|
#[derive(Clone)]
|
|
pub struct GraphicsContextParams {
|
|
/// Attributes of the main application window.
|
|
pub window_attributes: WindowAttributes,
|
|
|
|
/// Whether to use vertical synchronization or not. V-sync will force your game to render frames with the synchronization
|
|
/// rate of your monitor (which is ~60 FPS). Keep in mind that vertical synchronization might not be available on your OS.
|
|
pub vsync: bool,
|
|
|
|
/// Amount of samples for MSAA. Must be a power of two (1, 2, 4, 8). `None` means disabled.
|
|
/// MSAA works only for forward rendering and does not work for deferred rendering.
|
|
pub msaa_sample_count: Option<u8>,
|
|
|
|
/// Graphic server constructor. See [`GraphicsServerConstructor`] docs for more info.
|
|
pub graphics_server_constructor: GraphicsServerConstructor,
|
|
|
|
/// A flag, that if raised tells the engine to assign meaningful names for GPU objects. This
|
|
/// option is very useful for debugging. This option is off by default, because if may cause
|
|
/// crashes on some video driver due to poor implementation in the driver.
|
|
pub named_objects: bool,
|
|
}
|
|
|
|
impl Default for GraphicsContextParams {
|
|
fn default() -> Self {
|
|
Self {
|
|
window_attributes: Default::default(),
|
|
vsync: true,
|
|
msaa_sample_count: None,
|
|
graphics_server_constructor: Default::default(),
|
|
named_objects: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Engine initialization parameters.
|
|
pub struct EngineInitParams {
|
|
/// A set of parameters for graphics context initialization. Keep in mind that the engine **will not** initialize
|
|
/// graphics context for you. Instead, you need to call [`Engine::initialize_graphics_context`] on [`Event::Resumed`]
|
|
/// event and [`Engine::destroy_graphics_context`] on [`Event::Suspended`] event. If you don't need a graphics context
|
|
/// (for example for game servers), then you can pass [`Default::default`] here and do not call any methods.
|
|
pub graphics_context_params: GraphicsContextParams,
|
|
/// A special container that is able to create nodes by their type UUID.
|
|
pub serialization_context: Arc<SerializationContext>,
|
|
/// A container with widget constructors.
|
|
pub widget_constructors: Arc<WidgetConstructorContainer>,
|
|
/// A container for dynamic types. See [`DynTypeConstructorContainer`] docs for more info.
|
|
pub dyn_type_constructors: Arc<DynTypeConstructorContainer>,
|
|
/// A resource manager.
|
|
pub resource_manager: ResourceManager,
|
|
/// Task pool for asynchronous task management.
|
|
pub task_pool: Arc<TaskPool>,
|
|
}
|
|
|
|
fn process_node_script<T, C>(
|
|
caller_name: &'static str,
|
|
index: usize,
|
|
scene_handle: Handle<Scene>,
|
|
context: &mut C,
|
|
error_queue: &mut ErrorQueue,
|
|
func: &mut T,
|
|
) -> bool
|
|
where
|
|
T: FnMut(&mut Script, &mut C) -> GameResult,
|
|
C: UniversalScriptContext,
|
|
{
|
|
let Ok(node) = context.node() else {
|
|
// A node was destroyed.
|
|
return false;
|
|
};
|
|
|
|
if !node.is_globally_enabled() {
|
|
return false;
|
|
}
|
|
|
|
let Some(entry) = node.scripts.get_mut(index) else {
|
|
// All scripts were visited.
|
|
return false;
|
|
};
|
|
|
|
let Some(mut script) = entry.take() else {
|
|
return false;
|
|
};
|
|
|
|
if let Err(error) = func(&mut script, context) {
|
|
let node = context.node();
|
|
let handle = node.map_or(Handle::NONE, |n| n.handle());
|
|
error_queue.push_back(GameErrorContainer {
|
|
source: GameErrorSource::ScriptMethod {
|
|
scene_handle,
|
|
node_handle: handle,
|
|
method_name: caller_name,
|
|
},
|
|
error,
|
|
});
|
|
}
|
|
|
|
match context.node() {
|
|
Ok(node) => {
|
|
let entry = node
|
|
.scripts
|
|
.get_mut(index)
|
|
.expect("Scripts array cannot be modified!");
|
|
|
|
if entry.should_be_deleted {
|
|
context.destroy_script_deferred(script, index);
|
|
} else {
|
|
// Put the script back at its place.
|
|
entry.script = Some(script);
|
|
}
|
|
}
|
|
Err(_) => {
|
|
// If the node was deleted by the `func` call, we must send the script to destruction
|
|
// queue, not silently drop it.
|
|
context.destroy_script_deferred(script, index);
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
fn process_node_scripts<T, C>(
|
|
caller_name: &'static str,
|
|
context: &mut C,
|
|
scene_handle: Handle<Scene>,
|
|
error_queue: &mut ErrorQueue,
|
|
func: &mut T,
|
|
) where
|
|
T: FnMut(&mut Script, &mut C) -> GameResult,
|
|
C: UniversalScriptContext,
|
|
{
|
|
let mut index = 0;
|
|
loop {
|
|
context.set_script_index(index);
|
|
|
|
if !process_node_script(caller_name, index, scene_handle, context, error_queue, func) {
|
|
return;
|
|
}
|
|
|
|
// Advance to the next script.
|
|
index += 1;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn process_scripts<T>(
|
|
caller_name: &'static str,
|
|
scene: &mut Scene,
|
|
scene_handle: Handle<Scene>,
|
|
plugins: &mut [PluginContainer],
|
|
resource_manager: &ResourceManager,
|
|
message_sender: &ScriptMessageSender,
|
|
message_dispatcher: &mut ScriptMessageDispatcher,
|
|
task_pool: &mut TaskPoolHandler,
|
|
graphics_context: &mut GraphicsContext,
|
|
user_interfaces: &mut UiContainer,
|
|
dt: f32,
|
|
elapsed_time: f32,
|
|
input_state: &InputState,
|
|
error_queue: &mut ErrorQueue,
|
|
mut func: T,
|
|
) where
|
|
T: FnMut(&mut Script, &mut ScriptContext) -> GameResult,
|
|
{
|
|
let mut context = ScriptContext {
|
|
dt,
|
|
elapsed_time,
|
|
plugins: PluginsRefMut(plugins),
|
|
handle: Default::default(),
|
|
scene,
|
|
scene_handle,
|
|
resource_manager,
|
|
message_sender,
|
|
message_dispatcher,
|
|
task_pool,
|
|
graphics_context,
|
|
user_interfaces,
|
|
script_index: 0,
|
|
input_state,
|
|
};
|
|
|
|
for node_index in 0..context.scene.graph.capacity() {
|
|
context.handle = context.scene.graph.handle_from_index(node_index);
|
|
|
|
process_node_scripts(
|
|
caller_name,
|
|
&mut context,
|
|
scene_handle,
|
|
error_queue,
|
|
&mut func,
|
|
);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn initialize_resource_manager_loaders(
|
|
resource_manager: &ResourceManager,
|
|
serialization_context: Arc<SerializationContext>,
|
|
widget_constructors: Arc<WidgetConstructorContainer>,
|
|
dyn_type_constructors: Arc<DynTypeConstructorContainer>,
|
|
) {
|
|
let model_loader = ModelLoader {
|
|
resource_manager: resource_manager.clone(),
|
|
serialization_context,
|
|
dyn_type_constructors: dyn_type_constructors.clone(),
|
|
default_import_options: Default::default(),
|
|
};
|
|
|
|
let mut state = resource_manager.state();
|
|
|
|
for shader in ShaderResource::standard_shaders() {
|
|
state.built_in_resources.add((*shader).clone());
|
|
}
|
|
state.built_in_resources.add(GLTF_SHADER.clone());
|
|
|
|
for texture in SkyBoxKind::built_in_skybox_textures() {
|
|
state.built_in_resources.add(texture.clone());
|
|
}
|
|
|
|
state.built_in_resources.add(BUILT_IN_FONT.clone());
|
|
state.built_in_resources.add(BUILT_IN_BOLD.clone());
|
|
state.built_in_resources.add(BUILT_IN_ITALIC.clone());
|
|
state.built_in_resources.add(BOLD_ITALIC.clone());
|
|
|
|
state.built_in_resources.add(texture::PLACEHOLDER.clone());
|
|
state.built_in_resources.add(texture::PURE_COLOR.clone());
|
|
state.built_in_resources.add(style::DEFAULT_STYLE.clone());
|
|
state.built_in_resources.add(style::LIGHT_STYLE.clone());
|
|
|
|
for material in [
|
|
&*material::STANDARD,
|
|
&*material::STANDARD_2D,
|
|
&*material::STANDARD_SPRITE,
|
|
&*material::STANDARD_TERRAIN,
|
|
&*material::STANDARD_TWOSIDES,
|
|
&*material::STANDARD_PARTICLE_SYSTEM,
|
|
&*material::STANDARD_WIDGET,
|
|
] {
|
|
state.built_in_resources.add(material.clone());
|
|
}
|
|
|
|
for surface in [
|
|
&*surface::CUBE,
|
|
&*surface::QUAD,
|
|
&*surface::CYLINDER,
|
|
&*surface::SPHERE,
|
|
&*surface::CONE,
|
|
&*surface::TORUS,
|
|
] {
|
|
state.built_in_resources.add(surface.clone());
|
|
}
|
|
|
|
state.constructors_container.add::<Texture>();
|
|
state.constructors_container.add::<Shader>();
|
|
state.constructors_container.add::<Model>();
|
|
state.constructors_container.add::<CurveResourceState>();
|
|
state.constructors_container.add::<SoundBuffer>();
|
|
state.constructors_container.add::<HrirSphereResourceData>();
|
|
state.constructors_container.add::<Material>();
|
|
state.constructors_container.add::<Font>();
|
|
state.constructors_container.add::<UserInterface>();
|
|
state.constructors_container.add::<SurfaceData>();
|
|
state.constructors_container.add::<TileSet>();
|
|
state.constructors_container.add::<TileMapBrush>();
|
|
state.constructors_container.add::<TileMapData>();
|
|
state.constructors_container.add::<CustomTileCollider>();
|
|
state.constructors_container.add::<AnimationTracksData>();
|
|
state.constructors_container.add::<Style>();
|
|
|
|
let mut loaders = state.loaders.safe_lock();
|
|
let gltf_loader = super::resource::gltf::GltfLoader {
|
|
resource_manager: resource_manager.clone(),
|
|
default_import_options: Default::default(),
|
|
};
|
|
loaders.set(gltf_loader);
|
|
loaders.set(model_loader);
|
|
loaders.set(TextureLoader {
|
|
default_import_options: Default::default(),
|
|
});
|
|
loaders.set(SoundBufferLoader {
|
|
default_import_options: Default::default(),
|
|
});
|
|
loaders.set(ShaderLoader);
|
|
loaders.set(CurveLoader);
|
|
loaders.set(HrirSphereLoader);
|
|
loaders.set(MaterialLoader {
|
|
resource_manager: resource_manager.clone(),
|
|
});
|
|
loaders.set(FontLoader::new(resource_manager.clone()));
|
|
loaders.set(UserInterfaceLoader {
|
|
resource_manager: resource_manager.clone(),
|
|
constructors: widget_constructors,
|
|
dyn_type_constructors,
|
|
});
|
|
loaders.set(SurfaceDataLoader {});
|
|
loaders.set(TileSetLoader {
|
|
resource_manager: resource_manager.clone(),
|
|
});
|
|
loaders.set(TileMapBrushLoader {
|
|
resource_manager: resource_manager.clone(),
|
|
});
|
|
loaders.set(StyleLoader {
|
|
resource_manager: resource_manager.clone(),
|
|
});
|
|
}
|
|
|
|
/// A controller for the application loop.
|
|
#[derive(Copy, Clone)]
|
|
pub enum ApplicationLoopController<'a> {
|
|
/// Headless loop controller. This variant is used when the engine is running in headless mode
|
|
/// (with no window, graphics, sound, etc. support), usually used by game servers.
|
|
Headless {
|
|
/// A variable that controls execution of the game loop. If set to `false` the loop will
|
|
/// exit and the application will end its execution.
|
|
running: &'a Cell<bool>,
|
|
},
|
|
/// Normal application loop controller with full window, graphics, sound support.
|
|
ActiveEventLoop(&'a ActiveEventLoop),
|
|
/// Normal application loop controller without a window.
|
|
EventLoop(&'a EventLoop<()>),
|
|
}
|
|
|
|
impl ApplicationLoopController<'_> {
|
|
/// Asks the loop controller to end the application execution.
|
|
pub fn exit(&self) {
|
|
match self {
|
|
ApplicationLoopController::Headless { running } => running.set(false),
|
|
ApplicationLoopController::ActiveEventLoop(event_loop) => event_loop.exit(),
|
|
ApplicationLoopController::EventLoop(_) => {
|
|
warn!("Can't exit the loop until it is activated!")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the loop will be destroyed in the next frame and the application will
|
|
/// shut down.
|
|
pub fn exiting(&self) -> bool {
|
|
match self {
|
|
ApplicationLoopController::Headless { running } => !running.get(),
|
|
ApplicationLoopController::ActiveEventLoop(event_loop) => event_loop.exiting(),
|
|
ApplicationLoopController::EventLoop(_) => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Engine {
|
|
/// Creates new instance of engine from given initialization parameters. Automatically creates all sub-systems
|
|
/// (sound, ui, resource manager, etc.) **except** graphics context. Graphics context should be created manually
|
|
/// only on [`Event::Resumed`] by calling [`Engine::initialize_graphics_context`] and destroyed on [`Event::Suspended`]
|
|
/// by calling [`Engine::destroy_graphics_context`]. If you don't need a graphics context (for example if you're
|
|
/// making a game server), then you can ignore these methods.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # use fyrox_impl::{
|
|
/// # asset::manager::ResourceManager,
|
|
/// # engine::{
|
|
/// # Engine, EngineInitParams, GraphicsContextParams,
|
|
/// # SerializationContext,
|
|
/// # },
|
|
/// # event_loop::EventLoop,
|
|
/// # window::WindowAttributes,
|
|
/// # };
|
|
/// # use std::sync::Arc;
|
|
/// use fyrox_core::dyntype::DynTypeConstructorContainer;
|
|
/// # use fyrox_core::task::TaskPool;
|
|
/// # use fyrox_resource::io::FsResourceIo;
|
|
/// # use fyrox_ui::constructor::new_widget_constructor_container;
|
|
///
|
|
/// let mut window_attributes = WindowAttributes::default();
|
|
/// window_attributes.title = "Some title".to_string();
|
|
/// let graphics_context_params = GraphicsContextParams {
|
|
/// window_attributes,
|
|
/// vsync: true,
|
|
/// msaa_sample_count: None,
|
|
/// graphics_server_constructor: Default::default(),
|
|
/// named_objects: false
|
|
/// };
|
|
/// let task_pool = Arc::new(TaskPool::new());
|
|
///
|
|
/// Engine::new(EngineInitParams {
|
|
/// graphics_context_params,
|
|
/// resource_manager: ResourceManager::new(Arc::new(FsResourceIo), task_pool.clone()),
|
|
/// dyn_type_constructors: Arc::new(DynTypeConstructorContainer::default()),
|
|
/// serialization_context: Arc::new(SerializationContext::new()),
|
|
/// task_pool,
|
|
/// widget_constructors: Arc::new(new_widget_constructor_container()),
|
|
/// })
|
|
/// .unwrap();
|
|
/// ```
|
|
#[inline]
|
|
#[allow(unused_variables)]
|
|
pub fn new(params: EngineInitParams) -> Result<Self, EngineError> {
|
|
let EngineInitParams {
|
|
graphics_context_params,
|
|
serialization_context,
|
|
widget_constructors,
|
|
dyn_type_constructors,
|
|
resource_manager,
|
|
task_pool,
|
|
} = params;
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
wasm_utils::set_panic_hook();
|
|
|
|
initialize_resource_manager_loaders(
|
|
&resource_manager,
|
|
serialization_context.clone(),
|
|
widget_constructors.clone(),
|
|
dyn_type_constructors.clone(),
|
|
);
|
|
|
|
let (rx, tx) = channel();
|
|
resource_manager.state().event_broadcaster.add(rx);
|
|
|
|
let sound_engine = SoundEngine::without_device(SoundEngine::DEFAULT_SAMPLE_RATE);
|
|
|
|
Ok(Self {
|
|
graphics_context: GraphicsContext::Uninitialized(graphics_context_params),
|
|
model_events_receiver: tx,
|
|
resource_manager,
|
|
scenes: SceneContainer::new(sound_engine.clone()),
|
|
sound_engine,
|
|
user_interfaces: Default::default(),
|
|
performance_statistics: Default::default(),
|
|
plugins: Default::default(),
|
|
serialization_context,
|
|
widget_constructors,
|
|
dyn_type_constructors,
|
|
script_processor: Default::default(),
|
|
plugins_enabled: false,
|
|
elapsed_time: 0.0,
|
|
task_pool: TaskPoolHandler::new(task_pool),
|
|
input_state: Default::default(),
|
|
error_queue: Default::default(),
|
|
})
|
|
}
|
|
|
|
/// Tries to initialize the graphics context. The method will attempt to use the info stored in `graphics_context`
|
|
/// variable of the engine to attempt to initialize the graphics context. It will fail if the graphics context is
|
|
/// already initialized as well as if there any platform-dependent error (for example your hardware does not support
|
|
/// OpenGL 3.3 Core or OpenGL ES 3.0).
|
|
///
|
|
/// This method should be called on [`Event::Resumed`] of your game loop, however you can ignore it if you don't need
|
|
/// graphics context at all (for example - if you're making game server).
|
|
pub fn initialize_graphics_context(
|
|
&mut self,
|
|
event_loop: &ActiveEventLoop,
|
|
) -> Result<(), EngineError> {
|
|
if let GraphicsContext::Uninitialized(params) = &self.graphics_context {
|
|
let (window, server) = params.graphics_server_constructor.0(
|
|
params,
|
|
event_loop,
|
|
params.window_attributes.clone(),
|
|
params.named_objects,
|
|
)?;
|
|
let frame_size = (window.inner_size().width, window.inner_size().height);
|
|
|
|
let renderer = Renderer::new(server, frame_size, &self.resource_manager)?;
|
|
|
|
for ui in self.user_interfaces.iter_mut() {
|
|
ui.set_screen_size(Vector2::new(frame_size.0 as f32, frame_size.1 as f32));
|
|
}
|
|
|
|
self.graphics_context = GraphicsContext::Initialized(InitializedGraphicsContext {
|
|
renderer,
|
|
window,
|
|
params: params.clone(),
|
|
});
|
|
|
|
if let Err(err) = self.sound_engine.initialize_audio_output_device() {
|
|
Log::err(format!(
|
|
"Unable to initialize audio output device! Reason: {err:?}"
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
} else {
|
|
Err(EngineError::Custom(
|
|
"Graphics context is already initialized!".to_string(),
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Returns current sample rate of the sound engine.
|
|
pub fn sound_sample_rate(&self) -> u32 {
|
|
self.sound_engine.sample_rate()
|
|
}
|
|
|
|
/// Normalizes given frequency using sampling rate of the sound output device. Normalized frequency
|
|
/// then can be used to create filters.
|
|
pub fn normalize_sound_frequency(&self, f: f32) -> f32 {
|
|
self.sound_engine.normalize_frequency(f)
|
|
}
|
|
|
|
/// Tries to destroy current graphics context. It will succeed only if the `graphics_context` is fully initialized.
|
|
/// The method will try to save all possible runtime changes of the window, so the next [`Engine::initialize_graphics_context`]
|
|
/// will result in the almost exact copy of the context that was made before destruction.
|
|
///
|
|
/// This method should be called on [`Event::Suspended`] of your game loop, however if you do not use any graphics context
|
|
/// (for example - if you're making a game server), then you can ignore this method completely.
|
|
pub fn destroy_graphics_context(&mut self) -> Result<(), EngineError> {
|
|
if let GraphicsContext::Initialized(ref ctx) = self.graphics_context {
|
|
let params = &ctx.params;
|
|
let window = &ctx.window;
|
|
|
|
let mut window_attributes = WindowAttributes::default();
|
|
|
|
window_attributes.inner_size = Some(Size::Physical(window.inner_size()));
|
|
window_attributes.min_inner_size = params.window_attributes.min_inner_size;
|
|
window_attributes.max_inner_size = params.window_attributes.max_inner_size;
|
|
window_attributes.position = window.outer_position().ok().map(Position::Physical);
|
|
window_attributes.resizable = window.is_resizable();
|
|
window_attributes.enabled_buttons = window.enabled_buttons();
|
|
window_attributes.title = window.title();
|
|
window_attributes.maximized = window.is_maximized();
|
|
window_attributes.visible = window.is_visible().unwrap_or(true);
|
|
window_attributes.transparent = params.window_attributes.transparent;
|
|
window_attributes.decorations = window.is_decorated();
|
|
window_attributes.preferred_theme = params.window_attributes.preferred_theme;
|
|
window_attributes.resize_increments = window.resize_increments().map(Size::Physical);
|
|
window_attributes.content_protected = params.window_attributes.content_protected;
|
|
window_attributes.window_level = params.window_attributes.window_level;
|
|
window_attributes.active = params.window_attributes.active;
|
|
window_attributes
|
|
.window_icon
|
|
.clone_from(¶ms.window_attributes.window_icon);
|
|
|
|
self.graphics_context = GraphicsContext::Uninitialized(GraphicsContextParams {
|
|
window_attributes,
|
|
vsync: params.vsync,
|
|
msaa_sample_count: params.msaa_sample_count,
|
|
graphics_server_constructor: params.graphics_server_constructor.clone(),
|
|
named_objects: params.named_objects,
|
|
});
|
|
|
|
self.sound_engine.destroy_audio_output_device();
|
|
|
|
Ok(())
|
|
} else {
|
|
Err(EngineError::Custom(
|
|
"Graphics context is already destroyed!".to_string(),
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Adjust size of the frame to be rendered. Must be called after the window size changes.
|
|
/// Will update the renderer and GL context frame size.
|
|
pub fn set_frame_size(&mut self, new_size: (u32, u32)) -> Result<(), FrameworkError> {
|
|
if let GraphicsContext::Initialized(ctx) = &mut self.graphics_context {
|
|
ctx.renderer.set_frame_size(new_size)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Amount of time (in seconds) that passed from creation of the engine. Keep in mind, that
|
|
/// this value is **not** guaranteed to match real time. A user can change delta time with
|
|
/// which the engine "ticks" and this delta time affects elapsed time.
|
|
pub fn elapsed_time(&self) -> f32 {
|
|
self.elapsed_time
|
|
}
|
|
|
|
/// Performs single update tick with given time delta. Engine internally will perform update
|
|
/// of all scenes, sub-systems, user interface, etc. Must be called in order to get engine
|
|
/// functioning.
|
|
///
|
|
/// ## Parameters
|
|
///
|
|
/// `lag` - is a reference to time accumulator, that holds remaining amount of time that should be used
|
|
/// to update a plugin. A caller splits `lag` into multiple sub-steps using `dt` and thus stabilizes
|
|
/// update rate. The main use of this variable, is to be able to reset `lag` when you doing some heavy
|
|
/// calculations in a your game loop (i.e. loading a new level) so the engine won't try to "catch up" with
|
|
/// all the time that was spent in heavy calculation. The engine does **not** use this variable itself,
|
|
/// but the plugins attach may use it, that's why you need to provide it. If you don't use plugins, then
|
|
/// put `&mut 0.0` here.
|
|
pub fn update(
|
|
&mut self,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
switches: FxHashMap<Handle<Scene>, GraphUpdateSwitches>,
|
|
) {
|
|
self.pre_update(dt, controller, lag, switches);
|
|
self.post_update(dt, &Default::default(), lag, controller);
|
|
self.handle_plugins_hot_reloading(dt, controller, lag, |_| {});
|
|
}
|
|
|
|
/// Tries to hot-reload dynamic plugins marked for reloading.
|
|
///
|
|
/// ## Platform-specific
|
|
///
|
|
/// - Windows, Unix-like systems (Linux, macOS, FreeBSD, etc) - fully supported.
|
|
/// - WebAssembly - not supported
|
|
/// - Android - not supported
|
|
pub fn handle_plugins_hot_reloading<F>(
|
|
&mut self,
|
|
#[allow(unused_variables)] dt: f32,
|
|
#[allow(unused_variables)] controller: ApplicationLoopController,
|
|
#[allow(unused_variables)] lag: &mut f32,
|
|
#[allow(unused_variables)] on_reloaded: F,
|
|
) where
|
|
F: FnMut(&dyn Plugin),
|
|
{
|
|
#[cfg(any(unix, windows))]
|
|
{
|
|
if let Err(message) = self.reload_dynamic_plugins(dt, controller, lag, on_reloaded) {
|
|
Log::err(format!(
|
|
"Unable to reload dynamic plugins. Reason: {message}"
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performs pre update for the engine.
|
|
///
|
|
/// Normally, this is called from `Engine::update()`.
|
|
/// You should only call this manually if you don't use that method.
|
|
///
|
|
/// ## Parameters
|
|
///
|
|
/// `lag` - is a reference to time accumulator, that holds remaining amount of time that should be used
|
|
/// to update a plugin. A caller splits `lag` into multiple sub-steps using `dt` and thus stabilizes
|
|
/// update rate. The main use of this variable, is to be able to reset `lag` when you doing some heavy
|
|
/// calculations in a your game loop (i.e. loading a new level) so the engine won't try to "catch up" with
|
|
/// all the time that was spent in heavy calculation. The engine does **not** use this variable itself,
|
|
/// but the plugins attach may use it, that's why you need to provide it. If you don't use plugins, then
|
|
/// put `&mut 0.0` here.
|
|
pub fn pre_update(
|
|
&mut self,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
switches: FxHashMap<Handle<Scene>, GraphUpdateSwitches>,
|
|
) {
|
|
// Run some plugin and script methods, potentially causing nodes to be added
|
|
// or removed. This is where most of the rules of the game happen.
|
|
self.update_plugins(dt, controller, lag);
|
|
self.handle_scripts(dt);
|
|
|
|
// Now that the plugins and scripts have made whatever changes are needed, we must respond
|
|
// to those changes by updating the scenes and the state of the engine.
|
|
|
|
self.resource_manager.state().update(dt);
|
|
self.handle_model_events();
|
|
|
|
let window_size = if let GraphicsContext::Initialized(ctx) = &mut self.graphics_context {
|
|
let inner_size = ctx.window.inner_size();
|
|
let window_size = Vector2::new(inner_size.width as f32, inner_size.height as f32);
|
|
ctx.renderer.update_caches(&self.resource_manager, dt);
|
|
window_size
|
|
} else {
|
|
Vector2::new(1.0, 1.0)
|
|
};
|
|
|
|
for (handle, scene) in self.scenes.pair_iter_mut().filter(|(_, s)| *s.enabled) {
|
|
let frame_size =
|
|
scene
|
|
.rendering_options
|
|
.render_target
|
|
.as_ref()
|
|
.map_or(window_size, |rt| {
|
|
if let TextureKind::Rectangle { width, height } = rt.data_ref().kind() {
|
|
Vector2::new(width as f32, height as f32)
|
|
} else {
|
|
panic!("only rectangle textures can be used as render target!");
|
|
}
|
|
});
|
|
|
|
scene.update(
|
|
frame_size,
|
|
dt,
|
|
switches.get(&handle).cloned().unwrap_or_default(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Performs post update for the engine.
|
|
///
|
|
/// Normally, this is called from `Engine::update()`.
|
|
/// You should only call this manually if you don't use that method.
|
|
pub fn post_update(
|
|
&mut self,
|
|
dt: f32,
|
|
ui_update_switches: &UiUpdateSwitches,
|
|
lag: &mut f32,
|
|
controller: ApplicationLoopController,
|
|
) {
|
|
let screen_size = if let GraphicsContext::Initialized(ref ctx) = self.graphics_context {
|
|
let inner_size = ctx.window.inner_size();
|
|
Some(Vector2::new(
|
|
inner_size.width as f32,
|
|
inner_size.height as f32,
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let time = instant::Instant::now();
|
|
for ui in self.user_interfaces.iter_mut() {
|
|
let screen_size = screen_size.unwrap_or_else(|| ui.screen_size());
|
|
ui.update(screen_size, dt, ui_update_switches);
|
|
}
|
|
self.performance_statistics.ui_time = instant::Instant::now() - time;
|
|
self.elapsed_time += dt;
|
|
|
|
if let GraphicsContext::Initialized(_) = self.graphics_context {
|
|
self.post_update_plugins(dt, controller, lag);
|
|
|
|
self.input_state.mouse.speed = Vector2::default();
|
|
self.input_state.keyboard.released_keys.clear();
|
|
self.input_state.keyboard.pressed_keys.clear();
|
|
}
|
|
}
|
|
|
|
/// Returns true if the scene is registered for script processing.
|
|
pub fn has_scripted_scene(&self, scene: Handle<Scene>) -> bool {
|
|
self.script_processor.has_scripted_scene(scene)
|
|
}
|
|
|
|
/// Registers a scene for script processing.
|
|
pub fn register_scripted_scene(&mut self, scene: Handle<Scene>) {
|
|
self.script_processor
|
|
.register_scripted_scene(scene, &self.resource_manager)
|
|
}
|
|
|
|
fn handle_scripts(&mut self, dt: f32) {
|
|
let time = instant::Instant::now();
|
|
|
|
self.script_processor.handle_scripts(
|
|
&mut self.scenes,
|
|
&mut self.plugins,
|
|
&self.resource_manager,
|
|
&mut self.task_pool,
|
|
&mut self.graphics_context,
|
|
&mut self.user_interfaces,
|
|
dt,
|
|
self.elapsed_time,
|
|
&self.input_state,
|
|
&mut self.error_queue,
|
|
);
|
|
|
|
self.performance_statistics.scripts_time = instant::Instant::now() - time;
|
|
}
|
|
|
|
fn handle_async_tasks(
|
|
&mut self,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
) {
|
|
while let Some(result) = self.task_pool.inner().next_task_result() {
|
|
if let Some(plugin_task_handler) = self.task_pool.pop_plugin_task_handler(result.id) {
|
|
// Handle plugin task.
|
|
let mut ctx = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt,
|
|
lag,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
try_enqueue_plugin_error(
|
|
"handle_async_tasks",
|
|
(plugin_task_handler)(result.payload, &mut self.plugins, &mut ctx),
|
|
&mut self.error_queue,
|
|
)
|
|
} else if let Some(node_task_handler) = self.task_pool.pop_node_task_handler(result.id)
|
|
{
|
|
// Handle script task.
|
|
if let Some(scripted_scene) = self
|
|
.script_processor
|
|
.scripted_scenes
|
|
.iter_mut()
|
|
.find(|e| e.handle == node_task_handler.scene_handle)
|
|
{
|
|
let payload = result.payload;
|
|
if let Ok(scene) = self.scenes.try_get_mut(node_task_handler.scene_handle) {
|
|
if let Ok(node) =
|
|
scene.graph.try_get_node_mut(node_task_handler.node_handle)
|
|
{
|
|
if let Some(mut script) = node
|
|
.scripts
|
|
.get_mut(node_task_handler.script_index)
|
|
.and_then(|e| e.script.take())
|
|
{
|
|
let mut ctx = ScriptContext {
|
|
dt,
|
|
elapsed_time: self.elapsed_time,
|
|
plugins: PluginsRefMut(&mut self.plugins),
|
|
handle: node_task_handler.node_handle,
|
|
scene,
|
|
scene_handle: scripted_scene.handle,
|
|
resource_manager: &self.resource_manager,
|
|
message_sender: &scripted_scene.message_sender,
|
|
message_dispatcher: &mut scripted_scene.message_dispatcher,
|
|
task_pool: &mut self.task_pool,
|
|
graphics_context: &mut self.graphics_context,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
script_index: node_task_handler.script_index,
|
|
input_state: &self.input_state,
|
|
};
|
|
try_enqueue_plugin_error(
|
|
"handle_async_tasks",
|
|
(node_task_handler.closure)(
|
|
payload,
|
|
script.deref_mut(),
|
|
&mut ctx,
|
|
),
|
|
&mut self.error_queue,
|
|
);
|
|
|
|
if let Ok(node) =
|
|
scene.graph.try_get_node_mut(node_task_handler.node_handle)
|
|
{
|
|
if let Some(entry) =
|
|
node.scripts.get_mut(node_task_handler.script_index)
|
|
{
|
|
if entry.should_be_deleted {
|
|
Log::verify(scene.graph.script_message_sender.send(
|
|
NodeScriptMessage::DestroyScript {
|
|
script,
|
|
handle: node_task_handler.node_handle,
|
|
script_index: node_task_handler.script_index,
|
|
},
|
|
));
|
|
} else {
|
|
entry.script = Some(script);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_plugins(&mut self, dt: f32, controller: ApplicationLoopController, lag: &mut f32) {
|
|
let time = instant::Instant::now();
|
|
|
|
if self.plugins_enabled {
|
|
// Handle asynchronous tasks first.
|
|
self.handle_async_tasks(dt, controller, lag);
|
|
|
|
// Then update all the plugins.
|
|
let mut context = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt,
|
|
lag,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
|
|
for plugin in self.plugins.iter_mut() {
|
|
try_enqueue_plugin_error(
|
|
"update",
|
|
plugin.update(&mut context),
|
|
&mut self.error_queue,
|
|
);
|
|
}
|
|
|
|
let mut uis = self
|
|
.user_interfaces
|
|
.pair_iter()
|
|
.map(|(h, _)| h)
|
|
.collect::<VecDeque<_>>();
|
|
|
|
while let Some(ui) = uis.pop_front() {
|
|
let mut processed_messages = 0;
|
|
|
|
while let Some(message) = self
|
|
.user_interfaces
|
|
.try_get_mut(ui)
|
|
.ok()
|
|
.and_then(|ui| ui.poll_message())
|
|
{
|
|
let mut context = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt,
|
|
lag,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
|
|
for plugin in self.plugins.iter_mut() {
|
|
try_enqueue_plugin_error(
|
|
"on_ui_message",
|
|
plugin.on_ui_message(&mut context, &message, ui),
|
|
&mut self.error_queue,
|
|
);
|
|
}
|
|
|
|
processed_messages += 1;
|
|
|
|
if processed_messages >= 8192 {
|
|
warn!(
|
|
"Potential infinite message loop detected \
|
|
while processing message:\n{message:?}\nfrom {ui} user interface!\n\
|
|
Consider sending a message via send_sync to break the loop."
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.performance_statistics.plugins_time = instant::Instant::now() - time;
|
|
}
|
|
|
|
fn post_update_plugins(
|
|
&mut self,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
) {
|
|
let time = instant::Instant::now();
|
|
|
|
if self.plugins_enabled {
|
|
let mut context = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt,
|
|
lag,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
|
|
for plugin in self.plugins.iter_mut() {
|
|
try_enqueue_plugin_error(
|
|
"post_update",
|
|
plugin.post_update(&mut context),
|
|
&mut self.error_queue,
|
|
);
|
|
}
|
|
|
|
while let Some(container) = self.error_queue.pop_back() {
|
|
for plugin in self.plugins.iter_mut() {
|
|
if plugin.on_game_error(&mut context, &container.error) {
|
|
continue;
|
|
}
|
|
match container.source {
|
|
GameErrorSource::PluginMethod(method_name) => {
|
|
err!(
|
|
"An error occurred in {method_name} plugin method. Reason: {}",
|
|
container.error
|
|
);
|
|
}
|
|
GameErrorSource::ScriptMethod {
|
|
scene_handle,
|
|
node_handle,
|
|
method_name,
|
|
} => {
|
|
let node_name = context
|
|
.scenes
|
|
.try_get(scene_handle)
|
|
.ok()
|
|
.and_then(|scene| {
|
|
scene.graph.try_get(node_handle).ok().map(|n| n.name())
|
|
})
|
|
.unwrap_or("<undefined>");
|
|
|
|
err!(
|
|
"An error occurred during {method_name} call in {node_handle} \
|
|
node (name: {node_name}). Reason: {}",
|
|
container.error
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.performance_statistics.plugins_time += instant::Instant::now() - time;
|
|
}
|
|
|
|
/// Should be called on every OS event to update the internal state of the engine accordingly.
|
|
pub fn handle_os_events(
|
|
&mut self,
|
|
event: &Event<()>,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
) {
|
|
match event {
|
|
Event::WindowEvent { event, .. } => match event {
|
|
WindowEvent::KeyboardInput { event, .. } => {
|
|
let keyboard = &mut self.input_state.keyboard;
|
|
|
|
match event.state {
|
|
ElementState::Pressed => {
|
|
if keyboard
|
|
.keys
|
|
.get(&event.physical_key)
|
|
.is_none_or(|state| *state == ElementState::Released)
|
|
{
|
|
keyboard.pressed_keys.insert(event.physical_key);
|
|
}
|
|
}
|
|
ElementState::Released => {
|
|
if keyboard
|
|
.keys
|
|
.get(&event.physical_key)
|
|
.is_some_and(|state| *state == ElementState::Pressed)
|
|
{
|
|
keyboard.released_keys.insert(event.physical_key);
|
|
}
|
|
}
|
|
}
|
|
|
|
keyboard.keys.insert(event.physical_key, event.state);
|
|
}
|
|
WindowEvent::CursorMoved { position, .. } => {
|
|
self.input_state.mouse.position =
|
|
Vector2::new(position.x as f32, position.y as f32);
|
|
}
|
|
_ => (),
|
|
},
|
|
Event::DeviceEvent { event, .. } => match event {
|
|
DeviceEvent::MouseMotion { delta } => {
|
|
self.input_state.mouse.speed = Vector2::new(delta.0 as f32, delta.1 as f32);
|
|
}
|
|
DeviceEvent::Button { button, state } => {
|
|
let mouse = &mut self.input_state.mouse;
|
|
|
|
match *state {
|
|
ElementState::Pressed => {
|
|
if mouse
|
|
.buttons_state
|
|
.get(button)
|
|
.is_none_or(|state| *state == ElementState::Released)
|
|
{
|
|
mouse.pressed_buttons.insert(*button);
|
|
}
|
|
}
|
|
ElementState::Released => {
|
|
if mouse
|
|
.buttons_state
|
|
.get(button)
|
|
.is_some_and(|state| *state == ElementState::Pressed)
|
|
{
|
|
mouse.released_buttons.insert(*button);
|
|
}
|
|
}
|
|
}
|
|
|
|
mouse.buttons_state.insert(*button, *state);
|
|
}
|
|
_ => (),
|
|
},
|
|
_ => (),
|
|
}
|
|
|
|
if self.plugins_enabled {
|
|
for plugin in self.plugins.iter_mut() {
|
|
let ctx = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt,
|
|
lag,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
|
|
try_enqueue_plugin_error(
|
|
"on_os_event",
|
|
plugin.on_os_event(event, ctx),
|
|
&mut self.error_queue,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_graphics_context_created_by_plugins(
|
|
&mut self,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
) {
|
|
if self.plugins_enabled {
|
|
for plugin in self.plugins.iter_mut() {
|
|
let ctx = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt,
|
|
lag,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
|
|
try_enqueue_plugin_error(
|
|
"on_graphics_context_initialized",
|
|
plugin.on_graphics_context_initialized(ctx),
|
|
&mut self.error_queue,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_graphics_context_destroyed_by_plugins(
|
|
&mut self,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
) {
|
|
if self.plugins_enabled {
|
|
for plugin in self.plugins.iter_mut() {
|
|
let ctx = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt,
|
|
lag,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
|
|
try_enqueue_plugin_error(
|
|
"on_graphics_context_destroyed",
|
|
plugin.on_graphics_context_destroyed(ctx),
|
|
&mut self.error_queue,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_before_rendering_by_plugins(
|
|
&mut self,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
) {
|
|
if self.plugins_enabled {
|
|
for plugin in self.plugins.iter_mut() {
|
|
let ctx = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt,
|
|
lag,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
|
|
try_enqueue_plugin_error(
|
|
"before_rendering",
|
|
plugin.before_rendering(ctx),
|
|
&mut self.error_queue,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Passes specified OS event to every script of the specified scene.
|
|
///
|
|
/// # Important notes
|
|
///
|
|
/// This method is intended to be used by the editor and game runner. If you're using the
|
|
/// engine as a framework, then you should not call this method because you'll most likely
|
|
/// do something wrong.
|
|
pub(crate) fn handle_os_event_by_scripts(
|
|
&mut self,
|
|
event: &Event<()>,
|
|
scene_handle: Handle<Scene>,
|
|
dt: f32,
|
|
) {
|
|
if let Some(scripted_scene) = self
|
|
.script_processor
|
|
.scripted_scenes
|
|
.iter_mut()
|
|
.find(|s| s.handle == scene_handle)
|
|
{
|
|
let scene = &mut self.scenes[scene_handle];
|
|
if *scene.enabled {
|
|
process_scripts(
|
|
"on_os_event",
|
|
scene,
|
|
scene_handle,
|
|
&mut self.plugins,
|
|
&self.resource_manager,
|
|
&scripted_scene.message_sender,
|
|
&mut scripted_scene.message_dispatcher,
|
|
&mut self.task_pool,
|
|
&mut self.graphics_context,
|
|
&mut self.user_interfaces,
|
|
dt,
|
|
self.elapsed_time,
|
|
&self.input_state,
|
|
&mut self.error_queue,
|
|
|script, context| {
|
|
if script.initialized && script.started {
|
|
script.on_os_event(event, context)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handle hot-reloading of resources.
|
|
///
|
|
/// Normally, this is called from `Engine::update()`.
|
|
/// You should only call this manually if you don't use that method.
|
|
pub fn handle_model_events(&mut self) {
|
|
while let Ok(event) = self.model_events_receiver.try_recv() {
|
|
if let ResourceEvent::Reloaded(resource) = event {
|
|
if let Some(model) = resource.try_cast::<Model>() {
|
|
Log::info(format!(
|
|
"A model resource {} was reloaded, propagating changes...",
|
|
model.resource_uuid()
|
|
));
|
|
|
|
// Build resource dependency graph and resolve it first.
|
|
ResourceDependencyGraph::new(model, self.resource_manager.clone()).resolve();
|
|
|
|
Log::info("Propagating changes to active scenes...");
|
|
|
|
// Resolve all scenes.
|
|
// TODO: This might be inefficient if there is bunch of scenes loaded,
|
|
// however this seems to be very rare case so it should be ok.
|
|
for scene in self.scenes.iter_mut() {
|
|
scene.resolve();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performs rendering of single frame, must be called from your game loop, otherwise you won't
|
|
/// see anything.
|
|
#[inline]
|
|
pub fn render(&mut self) -> Result<(), FrameworkError> {
|
|
for ui in self.user_interfaces.iter_mut() {
|
|
ui.set_time(self.elapsed_time);
|
|
}
|
|
|
|
if let GraphicsContext::Initialized(ref mut ctx) = self.graphics_context {
|
|
ctx.renderer.render_and_swap_buffers(
|
|
&self.scenes,
|
|
self.elapsed_time,
|
|
self.user_interfaces
|
|
.iter_mut()
|
|
.filter(|ui| match ui.render_mode {
|
|
RenderMode::EveryFrame => true,
|
|
RenderMode::OnChanges => ui.need_render,
|
|
})
|
|
.map(|ui| {
|
|
ui.need_render = false;
|
|
UiRenderInfo {
|
|
ui,
|
|
render_target: ui.render_target.clone(),
|
|
clear_color: Default::default(),
|
|
resource_manager: &self.resource_manager,
|
|
}
|
|
}),
|
|
&ctx.window,
|
|
&self.resource_manager,
|
|
)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Enables or disables registered plugins.
|
|
pub(crate) fn enable_plugins(
|
|
&mut self,
|
|
scene_path: Option<&str>,
|
|
enabled: bool,
|
|
controller: ApplicationLoopController,
|
|
) {
|
|
if self.plugins_enabled != enabled {
|
|
self.plugins_enabled = enabled;
|
|
|
|
if self.plugins_enabled {
|
|
// Create and initialize instances.
|
|
for plugin in self.plugins.iter_mut() {
|
|
let ctx = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt: 0.0,
|
|
lag: &mut 0.0,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
|
|
try_enqueue_plugin_error(
|
|
"init",
|
|
plugin.init(scene_path, ctx),
|
|
&mut self.error_queue,
|
|
);
|
|
}
|
|
} else {
|
|
self.handle_scripts(0.0);
|
|
|
|
for mut plugin in self.plugins.drain(..) {
|
|
let ctx = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt: 0.0,
|
|
lag: &mut 0.0,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
|
|
// Deinit plugin first.
|
|
try_enqueue_plugin_error(
|
|
"on_deinit",
|
|
plugin.on_deinit(ctx),
|
|
&mut self.error_queue,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn register_plugin_internal(
|
|
serialization_context: &Arc<SerializationContext>,
|
|
widget_constructors: &Arc<WidgetConstructorContainer>,
|
|
dyn_type_constructors: &Arc<DynTypeConstructorContainer>,
|
|
resource_manager: &ResourceManager,
|
|
plugin: &dyn Plugin,
|
|
error_queue: &mut ErrorQueue,
|
|
) {
|
|
try_enqueue_plugin_error(
|
|
"register",
|
|
plugin.register(PluginRegistrationContext {
|
|
serialization_context,
|
|
widget_constructors,
|
|
dyn_type_constructors,
|
|
resource_manager,
|
|
}),
|
|
error_queue,
|
|
);
|
|
}
|
|
|
|
fn register_plugin(&mut self, plugin: &dyn Plugin) {
|
|
Self::register_plugin_internal(
|
|
&self.serialization_context,
|
|
&self.widget_constructors,
|
|
&self.dyn_type_constructors,
|
|
&self.resource_manager,
|
|
plugin,
|
|
&mut self.error_queue,
|
|
)
|
|
}
|
|
|
|
/// Adds a new static plugin.
|
|
pub fn add_plugin<P>(&mut self, plugin: P)
|
|
where
|
|
P: Plugin + 'static,
|
|
{
|
|
self.register_plugin(&plugin);
|
|
|
|
self.plugins.push(PluginContainer::Static(Box::new(plugin)));
|
|
}
|
|
|
|
/// Tries to add a new dynamic plugin. This method attempts to load a dynamic library by the
|
|
/// given path and searches for `fyrox_plugin` function. This function is called to create a
|
|
/// plugin instance. This method will fail if there's no dynamic library at the given path or
|
|
/// the `fyrox_plugin` function is not found.
|
|
///
|
|
/// # Hot reloading
|
|
///
|
|
/// This method can enable hot reloading for the plugin, by setting `reload_when_changed` parameter
|
|
/// to `true`. When enabled, the engine will clone the library to implementation-defined path
|
|
/// and load it. It will setup file system watcher to receive changes from the OS and reload
|
|
/// the plugin.
|
|
pub fn add_dynamic_plugin<P>(
|
|
&mut self,
|
|
path: P,
|
|
reload_when_changed: bool,
|
|
use_relative_paths: bool,
|
|
) -> Result<&dyn Plugin, String>
|
|
where
|
|
P: AsRef<Path> + 'static,
|
|
{
|
|
Ok(self.add_dynamic_plugin_custom(DyLibDynamicPlugin::new(
|
|
path,
|
|
reload_when_changed,
|
|
use_relative_paths,
|
|
)?))
|
|
}
|
|
|
|
/// Adds a new abstract dynamic plugin
|
|
pub fn add_dynamic_plugin_custom<P>(&mut self, plugin: P) -> &dyn Plugin
|
|
where
|
|
P: DynamicPlugin + 'static,
|
|
{
|
|
let display_name = plugin.display_name();
|
|
|
|
let plugin_container = PluginContainer::Dynamic(Box::new(plugin));
|
|
|
|
self.register_plugin(plugin_container.deref());
|
|
self.plugins.push(plugin_container);
|
|
|
|
Log::info(format!("Plugin {display_name:?} was loaded successfully"));
|
|
|
|
&**self.plugins.last().unwrap()
|
|
}
|
|
|
|
/// Tries to reload a specified plugin. This method tries to perform least invasive reloading, by
|
|
/// only detaching parts from the scenes and engine internals, that belongs to reloadable plugin.
|
|
pub fn reload_plugin(
|
|
&mut self,
|
|
plugin_index: usize,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
) -> Result<(), String> {
|
|
let plugin_container = &mut self.plugins[plugin_index];
|
|
let PluginContainer::Dynamic(plugin) = plugin_container else {
|
|
return Err(format!(
|
|
"Plugin {plugin_index} is static and cannot be reloaded!",
|
|
));
|
|
};
|
|
|
|
if !plugin.is_loaded() {
|
|
// TODO: this means that something bad happened during plugin reloading.
|
|
// don't we want to recover from this situation by trying to load it again
|
|
// (maybe with clearing `need_reload` flag, to perform new attempt only when something is changed)
|
|
return Err(format!("Cannot reload unloaded plugin {plugin_index}!"));
|
|
}
|
|
plugin.prepare_to_reload();
|
|
|
|
let plugin_type_id = plugin.as_loaded_ref().type_id();
|
|
let plugin_assembly_name = plugin.as_loaded_ref().type_info_ref().assembly_name;
|
|
|
|
// Collect all the data that belongs to the plugin
|
|
let mut scenes_state = Vec::new();
|
|
for (scene_handle, scene) in self.scenes.pair_iter_mut() {
|
|
if let Some(data) = hotreload::SceneState::try_create_from_plugin(
|
|
scene_handle,
|
|
scene,
|
|
&self.serialization_context,
|
|
plugin.as_loaded_ref(),
|
|
)? {
|
|
scenes_state.push(data);
|
|
}
|
|
}
|
|
|
|
// Check every prefab for plugin content.
|
|
let mut prefab_scenes = Vec::new();
|
|
let rm_state = self.resource_manager.state();
|
|
for resource in rm_state.resources().iter() {
|
|
if let Some(model) = resource.try_cast::<Model>() {
|
|
let mut model_state = model.state();
|
|
if let Some(data) = model_state.data() {
|
|
if let Some(scene_state) = hotreload::SceneState::try_create_from_plugin(
|
|
Handle::NONE,
|
|
&mut data.scene,
|
|
&self.serialization_context,
|
|
plugin.as_loaded_ref(),
|
|
)? {
|
|
prefab_scenes.push((model.clone(), scene_state));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
drop(rm_state);
|
|
|
|
// Search for script constructors, that belongs to dynamic plugins and remove them.
|
|
let mut constructors = FxHashSet::default();
|
|
for (type_uuid, constructor) in self.serialization_context.script_constructors.map().iter()
|
|
{
|
|
if constructor.assembly_name == plugin_assembly_name {
|
|
constructors.insert(*type_uuid);
|
|
}
|
|
}
|
|
for type_uuid in constructors.iter() {
|
|
self.serialization_context
|
|
.script_constructors
|
|
.remove(*type_uuid);
|
|
}
|
|
|
|
// Search for node constructors, that belongs to dynamic plugins and remove them.
|
|
let mut constructors = FxHashSet::default();
|
|
for (type_uuid, constructor) in self.serialization_context.node_constructors.map().iter() {
|
|
if constructor.assembly_name == plugin_assembly_name {
|
|
constructors.insert(*type_uuid);
|
|
}
|
|
}
|
|
for type_uuid in constructors.iter() {
|
|
self.serialization_context
|
|
.node_constructors
|
|
.remove(*type_uuid);
|
|
}
|
|
|
|
// Search for widget constructors, that belongs to dynamic plugins and remove them.
|
|
let mut constructors = FxHashSet::default();
|
|
for (type_uuid, constructor) in self.widget_constructors.map().iter() {
|
|
if constructor.assembly_name == plugin_assembly_name {
|
|
constructors.insert(*type_uuid);
|
|
}
|
|
}
|
|
for type_uuid in constructors.iter() {
|
|
self.widget_constructors.remove(*type_uuid);
|
|
}
|
|
|
|
// Search for dyn type constructors, that belongs to dynamic plugins and remove them.
|
|
let mut dyn_type_constructors = FxHashSet::default();
|
|
for (type_uuid, constructor) in self.dyn_type_constructors.inner().iter() {
|
|
if constructor.assembly_name == plugin_assembly_name {
|
|
dyn_type_constructors.insert(*type_uuid);
|
|
}
|
|
}
|
|
for type_uuid in dyn_type_constructors.iter() {
|
|
self.dyn_type_constructors.remove(type_uuid);
|
|
}
|
|
|
|
// Reload resources, that belongs to the plugin.
|
|
{
|
|
let mut resources_to_reload = FxHashSet::default();
|
|
let mut state = self.resource_manager.state();
|
|
for resource in state.resources().iter() {
|
|
let data = resource.lock();
|
|
if let ResourceState::Ok { ref data, .. } = data.state {
|
|
if data.type_info_ref().assembly_name == plugin_assembly_name {
|
|
resources_to_reload.insert(resource.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
for resource_to_reload in resources_to_reload.iter() {
|
|
Log::info(format!(
|
|
"Reloading {:?} resource, because it is used in plugin {plugin_assembly_name}",
|
|
state.resource_path(resource_to_reload)
|
|
));
|
|
|
|
state.reload_resource(resource_to_reload.clone());
|
|
}
|
|
|
|
drop(state);
|
|
|
|
block_on(join_all(resources_to_reload));
|
|
}
|
|
|
|
// Unload custom render passes (if any).
|
|
if let GraphicsContext::Initialized(ref mut graphics_context) = self.graphics_context {
|
|
let render_passes = graphics_context.renderer.render_passes().to_vec();
|
|
for render_pass in render_passes {
|
|
if render_pass.borrow().source_type_id() == plugin_type_id {
|
|
graphics_context.renderer.remove_render_pass(render_pass);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut visitor = hotreload::make_writing_visitor();
|
|
plugin
|
|
.as_loaded_mut()
|
|
.visit("Plugin", &mut visitor)
|
|
.map_err(|e| e.to_string())?;
|
|
let mut binary_blob = Cursor::new(Vec::<u8>::new());
|
|
visitor
|
|
.save_binary_to_memory(&mut binary_blob)
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
Log::info(format!(
|
|
"Plugin {plugin_index} was serialized successfully!"
|
|
));
|
|
|
|
// Explicitly drop the visitor to prevent any destructors from the previous version of
|
|
// the plugin to run at the end of the scope. This could happen, because the visitor
|
|
// manages serialized smart pointers and if they'll be kept alive longer than the plugin
|
|
// there's a very high chance of hard crash.
|
|
drop(visitor);
|
|
|
|
let binary_blob = binary_blob.into_inner();
|
|
|
|
plugin.reload(&mut |plugin| {
|
|
// Re-register the plugin. This is needed, because it might contain new script/node/widget
|
|
// types (or removed ones too). This is done right before deserialization, because plugin
|
|
// might contain some entities, that have dynamic registration.
|
|
Self::register_plugin_internal(
|
|
&self.serialization_context,
|
|
&self.widget_constructors,
|
|
&self.dyn_type_constructors,
|
|
&self.resource_manager,
|
|
plugin,
|
|
&mut self.error_queue,
|
|
);
|
|
|
|
// New plugins may add custom resources and we must re-scan the data folder to include
|
|
// such resources in the registry.
|
|
self.resource_manager.update_or_load_registry();
|
|
|
|
let mut visitor = hotreload::make_reading_visitor(
|
|
&binary_blob,
|
|
&self.serialization_context,
|
|
&self.resource_manager,
|
|
&self.widget_constructors,
|
|
&self.dyn_type_constructors,
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
plugin
|
|
.visit("Plugin", &mut visitor)
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
})?;
|
|
|
|
// Deserialize prefab scene content.
|
|
for (model, scene_state) in prefab_scenes {
|
|
Log::info(format!(
|
|
"Deserializing {} prefab content...",
|
|
model.resource_uuid()
|
|
));
|
|
|
|
scene_state.deserialize_into_prefab_scene(
|
|
&model,
|
|
&self.serialization_context,
|
|
&self.resource_manager,
|
|
&self.widget_constructors,
|
|
&self.dyn_type_constructors,
|
|
)?;
|
|
}
|
|
|
|
// Deserialize scene content.
|
|
for scene_state in scenes_state {
|
|
let scene = &mut self.scenes[scene_state.scene];
|
|
scene_state.deserialize_into_scene(
|
|
scene,
|
|
&self.serialization_context,
|
|
&self.resource_manager,
|
|
&self.widget_constructors,
|
|
&self.dyn_type_constructors,
|
|
)?;
|
|
}
|
|
|
|
// Call `on_loaded` for plugins, so they could restore some runtime non-serializable state.
|
|
let ctx = PluginContext {
|
|
scenes: &mut self.scenes,
|
|
resource_manager: &self.resource_manager,
|
|
user_interfaces: &mut self.user_interfaces,
|
|
graphics_context: &mut self.graphics_context,
|
|
dt,
|
|
lag,
|
|
serialization_context: &self.serialization_context,
|
|
widget_constructors: &self.widget_constructors,
|
|
dyn_type_constructors: &self.dyn_type_constructors,
|
|
performance_statistics: &Default::default(),
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
input_state: &self.input_state,
|
|
};
|
|
try_enqueue_plugin_error(
|
|
"on_loaded",
|
|
plugin.as_loaded_mut().on_loaded(ctx),
|
|
&mut self.error_queue,
|
|
);
|
|
|
|
Log::info(format!("Plugin {plugin_index} was successfully reloaded!"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns a reference to the plugins.
|
|
pub fn plugins(&self) -> &[PluginContainer] {
|
|
&self.plugins
|
|
}
|
|
|
|
/// Returns a mutable reference to the plugins.
|
|
pub fn plugins_mut(&mut self) -> &mut [PluginContainer] {
|
|
&mut self.plugins
|
|
}
|
|
|
|
/// Tries to reload all dynamic plugins registered in the engine, that needs to be reloaded.
|
|
pub fn reload_dynamic_plugins<F>(
|
|
&mut self,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
mut on_reloaded: F,
|
|
) -> Result<(), String>
|
|
where
|
|
F: FnMut(&dyn Plugin),
|
|
{
|
|
for plugin_index in 0..self.plugins.len() {
|
|
if let PluginContainer::Dynamic(plugin) = &self.plugins[plugin_index] {
|
|
if plugin.is_reload_needed_now() {
|
|
self.reload_plugin(plugin_index, dt, controller, lag)?;
|
|
|
|
on_reloaded(self.plugins[plugin_index].deref_mut());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Drop for Engine {
|
|
fn drop(&mut self) {
|
|
// Destroy all scenes first and correctly destroy all script instances.
|
|
// This will ensure that any `on_destroy` logic will be executed before
|
|
// engine destroyed.
|
|
let scenes = self
|
|
.scenes
|
|
.pair_iter()
|
|
.map(|(h, _)| h)
|
|
.collect::<Vec<Handle<Scene>>>();
|
|
|
|
for handle in scenes {
|
|
self.scenes.remove(handle);
|
|
}
|
|
|
|
// Clear everything that may potentially store a dynamic plugin entity.
|
|
if let GraphicsContext::Initialized(ref mut graphics_context) = self.graphics_context {
|
|
graphics_context.renderer.clear_render_passes();
|
|
}
|
|
self.user_interfaces.clear();
|
|
self.scenes.clear();
|
|
self.serialization_context.clear();
|
|
self.widget_constructors.clear();
|
|
self.dyn_type_constructors.clear();
|
|
|
|
// Finally disable plugins.
|
|
self.enable_plugins(
|
|
None,
|
|
false,
|
|
ApplicationLoopController::Headless {
|
|
running: &Default::default(),
|
|
},
|
|
);
|
|
|
|
// Must be last, otherwise the engine will crash on shutdown when compiled in native code
|
|
// hot reloading. This may happen if some of the dynamic plugin entities are dropped after
|
|
// the parent plugin.
|
|
self.plugins.clear();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::engine::ApplicationLoopController;
|
|
use crate::plugin::error::GameResult;
|
|
use crate::scene::pivot::Pivot;
|
|
use crate::{
|
|
asset::manager::ResourceManager,
|
|
core::{pool::Handle, reflect::prelude::*, task::TaskPool, visitor::prelude::*},
|
|
engine::{task::TaskPoolHandler, GraphicsContext, ScriptProcessor},
|
|
graph::SceneGraph,
|
|
scene::{base::BaseBuilder, node::Node, pivot::PivotBuilder, Scene, SceneContainer},
|
|
script::{
|
|
ScriptContext, ScriptDeinitContext, ScriptMessageContext, ScriptMessagePayload,
|
|
ScriptTrait,
|
|
},
|
|
};
|
|
use fyrox_resource::io::FsResourceIo;
|
|
use fyrox_ui::UiContainer;
|
|
use std::cell::Cell;
|
|
use std::sync::{
|
|
mpsc::{self, Sender, TryRecvError},
|
|
Arc,
|
|
};
|
|
|
|
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
|
struct Source {
|
|
node_handle: Handle<Node>,
|
|
script_index: usize,
|
|
}
|
|
|
|
impl Source {
|
|
fn from_ctx(ctx: &ScriptContext) -> Self {
|
|
Self {
|
|
node_handle: ctx.handle,
|
|
script_index: ctx.script_index,
|
|
}
|
|
}
|
|
|
|
fn from_deinit_ctx(ctx: &ScriptDeinitContext) -> Self {
|
|
Self {
|
|
node_handle: ctx.node_handle,
|
|
script_index: ctx.script_index,
|
|
}
|
|
}
|
|
|
|
fn from_msg_ctx(ctx: &ScriptMessageContext) -> Self {
|
|
Self {
|
|
node_handle: ctx.handle,
|
|
script_index: ctx.script_index,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::enum_variant_names)]
|
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
|
enum Event {
|
|
Initialized(Source),
|
|
Started(Source),
|
|
Updated(Source),
|
|
Destroyed(Source),
|
|
EventReceived(Source),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Reflect, Visit)]
|
|
#[reflect(type_uuid = "2569de84-d4b2-427d-969b-d5c7b31a0ba6", non_comparable)]
|
|
struct MyScript {
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
spawned: bool,
|
|
}
|
|
|
|
impl ScriptTrait for MyScript {
|
|
fn on_init(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Initialized(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
|
|
// Spawn new entity with script.
|
|
let handle = PivotBuilder::new(BaseBuilder::new().with_script(MySubScript {
|
|
sender: self.sender.clone(),
|
|
}))
|
|
.build(&mut ctx.scene.graph);
|
|
assert_eq!(handle, Handle::<Pivot>::new(2, 1));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Started(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
|
|
// Spawn new entity with script.
|
|
let handle = PivotBuilder::new(BaseBuilder::new().with_script(MySubScript {
|
|
sender: self.sender.clone(),
|
|
}))
|
|
.build(&mut ctx.scene.graph);
|
|
assert_eq!(handle, Handle::<Pivot>::new(3, 1));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn on_deinit(&mut self, ctx: &mut ScriptDeinitContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Destroyed(Source::from_deinit_ctx(ctx)))
|
|
.unwrap();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Updated(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
|
|
if !self.spawned {
|
|
// Spawn new entity with script.
|
|
PivotBuilder::new(BaseBuilder::new().with_script(MySubScript {
|
|
sender: self.sender.clone(),
|
|
}))
|
|
.build(&mut ctx.scene.graph);
|
|
|
|
self.spawned = true;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Reflect, Visit)]
|
|
#[reflect(type_uuid = "1cebacd9-b500-4753-93be-39db344add21", non_comparable)]
|
|
struct MySubScript {
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
}
|
|
|
|
impl ScriptTrait for MySubScript {
|
|
fn on_init(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Initialized(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Started(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn on_deinit(&mut self, ctx: &mut ScriptDeinitContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Destroyed(Source::from_deinit_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Updated(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_order() {
|
|
let resource_manager =
|
|
ResourceManager::new(Arc::new(FsResourceIo), Arc::new(Default::default()));
|
|
let mut scene = Scene::new();
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
let node_handle = PivotBuilder::new(
|
|
BaseBuilder::new()
|
|
.with_script(MyScript {
|
|
sender: tx.clone(),
|
|
spawned: false,
|
|
})
|
|
.with_script(MySubScript { sender: tx }),
|
|
)
|
|
.build(&mut scene.graph)
|
|
.to_base();
|
|
assert_eq!(node_handle, Handle::<Pivot>::new(1, 1));
|
|
|
|
let node_handle_0 = Source {
|
|
node_handle,
|
|
script_index: 0,
|
|
};
|
|
let node_handle_1 = Source {
|
|
node_handle,
|
|
script_index: 1,
|
|
};
|
|
|
|
let mut scene_container = SceneContainer::new(Default::default());
|
|
|
|
let scene_handle = scene_container.add(scene);
|
|
|
|
let mut script_processor = ScriptProcessor::default();
|
|
|
|
script_processor.register_scripted_scene(scene_handle, &resource_manager);
|
|
|
|
let handle_on_init = Source {
|
|
node_handle: Handle::new(2, 1),
|
|
script_index: 0,
|
|
};
|
|
let handle_on_start = Source {
|
|
node_handle: Handle::new(3, 1),
|
|
script_index: 0,
|
|
};
|
|
let handle_on_update1 = Source {
|
|
node_handle: Handle::new(4, 1),
|
|
script_index: 0,
|
|
};
|
|
let mut task_pool = TaskPoolHandler::new(Arc::new(TaskPool::new()));
|
|
let mut gc = GraphicsContext::Uninitialized(Default::default());
|
|
let mut user_interfaces = UiContainer::default();
|
|
|
|
for iteration in 0..3 {
|
|
script_processor.handle_scripts(
|
|
&mut scene_container,
|
|
&mut Vec::new(),
|
|
&resource_manager,
|
|
&mut task_pool,
|
|
&mut gc,
|
|
&mut user_interfaces,
|
|
0.0,
|
|
0.0,
|
|
&Default::default(),
|
|
&mut Default::default(),
|
|
);
|
|
|
|
match iteration {
|
|
0 => {
|
|
assert_eq!(rx.try_recv(), Ok(Event::Initialized(node_handle_0)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Initialized(node_handle_1)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Initialized(handle_on_init)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Started(node_handle_0)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Started(node_handle_1)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Started(handle_on_init)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Initialized(handle_on_start)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Started(handle_on_start)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Updated(node_handle_0)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Updated(node_handle_1)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Updated(handle_on_init)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Updated(handle_on_start)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Initialized(handle_on_update1)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Started(handle_on_update1)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Updated(handle_on_update1)));
|
|
}
|
|
1 => {
|
|
assert_eq!(rx.try_recv(), Ok(Event::Updated(node_handle_0)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Updated(node_handle_1)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Updated(handle_on_init)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Updated(handle_on_start)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Updated(handle_on_update1)));
|
|
assert_eq!(rx.try_recv(), Err(TryRecvError::Empty));
|
|
|
|
// Now destroy every node with script, next iteration should correctly destroy attached scripts.
|
|
let graph = &mut scene_container[scene_handle].graph;
|
|
graph.remove_node(node_handle);
|
|
graph.remove_node(handle_on_init.node_handle);
|
|
graph.remove_node(handle_on_start.node_handle);
|
|
graph.remove_node(handle_on_update1.node_handle);
|
|
}
|
|
2 => {
|
|
assert_eq!(rx.try_recv(), Ok(Event::Destroyed(node_handle_0)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Destroyed(node_handle_1)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Destroyed(handle_on_init)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Destroyed(handle_on_start)));
|
|
assert_eq!(rx.try_recv(), Ok(Event::Destroyed(handle_on_update1)));
|
|
|
|
// Every instance holding sender died, so receiver is disconnected from sender.
|
|
assert_eq!(rx.try_recv(), Err(TryRecvError::Disconnected));
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, ScriptMessagePayload)]
|
|
enum MyMessage {
|
|
Foo(usize),
|
|
Bar(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Reflect, Visit)]
|
|
#[reflect(type_uuid = "bf2976ad-f41d-4de6-9a32-b1a293956058", non_comparable)]
|
|
struct ScriptListeningToMessages {
|
|
index: u32,
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
}
|
|
|
|
impl ScriptTrait for ScriptListeningToMessages {
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
ctx.message_dispatcher.subscribe_to::<MyMessage>(ctx.handle);
|
|
Ok(())
|
|
}
|
|
|
|
fn on_message(
|
|
&mut self,
|
|
message: &mut dyn ScriptMessagePayload,
|
|
ctx: &mut ScriptMessageContext,
|
|
) -> GameResult {
|
|
let typed_message = message.downcast_ref::<MyMessage>().unwrap();
|
|
match self.index {
|
|
0 => {
|
|
if let MyMessage::Foo(num) = typed_message {
|
|
assert_eq!(*num, 123);
|
|
self.sender
|
|
.send(Event::EventReceived(Source::from_msg_ctx(ctx)))
|
|
.unwrap();
|
|
} else {
|
|
unreachable!()
|
|
}
|
|
}
|
|
1 => {
|
|
if let MyMessage::Bar(string) = typed_message {
|
|
assert_eq!(string, "Foobar");
|
|
self.sender
|
|
.send(Event::EventReceived(Source::from_msg_ctx(ctx)))
|
|
.unwrap();
|
|
} else {
|
|
unreachable!()
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
self.index += 1;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Reflect, Visit)]
|
|
#[reflect(type_uuid = "6bcbf9b4-9546-42d3-965a-de055ab85475")]
|
|
struct ScriptSendingMessages {
|
|
index: u32,
|
|
}
|
|
|
|
impl ScriptTrait for ScriptSendingMessages {
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
match self.index {
|
|
0 => ctx.message_sender.send_global(MyMessage::Foo(123)),
|
|
1 => ctx
|
|
.message_sender
|
|
.send_global(MyMessage::Bar("Foobar".to_string())),
|
|
_ => (),
|
|
}
|
|
self.index += 1;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_messages() {
|
|
let resource_manager =
|
|
ResourceManager::new(Arc::new(FsResourceIo), Arc::new(Default::default()));
|
|
let mut scene = Scene::new();
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
PivotBuilder::new(BaseBuilder::new().with_script(ScriptSendingMessages { index: 0 }))
|
|
.build(&mut scene.graph);
|
|
|
|
let receiver_messages =
|
|
PivotBuilder::new(BaseBuilder::new().with_script(ScriptListeningToMessages {
|
|
sender: tx,
|
|
index: 0,
|
|
}))
|
|
.build(&mut scene.graph)
|
|
.to_base();
|
|
let receiver_messages_source = Source {
|
|
node_handle: receiver_messages,
|
|
script_index: 0,
|
|
};
|
|
|
|
let mut scene_container = SceneContainer::new(Default::default());
|
|
|
|
let scene_handle = scene_container.add(scene);
|
|
|
|
let mut script_processor = ScriptProcessor::default();
|
|
let mut task_pool = TaskPoolHandler::new(Arc::new(TaskPool::new()));
|
|
let mut gc = GraphicsContext::Uninitialized(Default::default());
|
|
let mut user_interfaces = UiContainer::default();
|
|
|
|
script_processor.register_scripted_scene(scene_handle, &resource_manager);
|
|
|
|
for iteration in 0..2 {
|
|
script_processor.handle_scripts(
|
|
&mut scene_container,
|
|
&mut Vec::new(),
|
|
&resource_manager,
|
|
&mut task_pool,
|
|
&mut gc,
|
|
&mut user_interfaces,
|
|
0.0,
|
|
0.0,
|
|
&Default::default(),
|
|
&mut Default::default(),
|
|
);
|
|
|
|
match iteration {
|
|
0 => {
|
|
assert_eq!(
|
|
rx.try_recv(),
|
|
Ok(Event::EventReceived(receiver_messages_source))
|
|
);
|
|
assert_eq!(rx.try_recv(), Err(TryRecvError::Empty));
|
|
}
|
|
1 => {
|
|
assert_eq!(
|
|
rx.try_recv(),
|
|
Ok(Event::EventReceived(receiver_messages_source))
|
|
);
|
|
assert_eq!(rx.try_recv(), Err(TryRecvError::Empty));
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Reflect, Visit)]
|
|
#[reflect(type_uuid = "7bcbf9b4-9546-42d3-965a-de055ab85475")]
|
|
pub struct ScriptSpawningAsyncTasks {
|
|
num: Option<u32>,
|
|
}
|
|
|
|
impl ScriptTrait for ScriptSpawningAsyncTasks {
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
ctx.task_pool.spawn_script_task(
|
|
ctx.scene_handle,
|
|
ctx.handle,
|
|
ctx.script_index,
|
|
async move { 123u32 },
|
|
|result, script: &mut ScriptSpawningAsyncTasks, _ctx| {
|
|
assert_eq!(result, 123u32);
|
|
script.num = Some(result);
|
|
Ok(())
|
|
},
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Reflect, Visit)]
|
|
#[reflect(type_uuid = "8bcbf9b4-9546-42d3-965a-de055ab85475")]
|
|
pub struct ScriptWithoutAsyncTasks {}
|
|
|
|
impl ScriptTrait for ScriptWithoutAsyncTasks {}
|
|
|
|
#[test]
|
|
#[cfg(not(target_os = "macos"))] // This fails on macOS for some reason.
|
|
fn test_async_script_tasks() {
|
|
use crate::engine::{Engine, EngineInitParams};
|
|
|
|
let task_pool = Arc::new(TaskPool::default());
|
|
let mut engine = Engine::new(EngineInitParams {
|
|
graphics_context_params: Default::default(),
|
|
serialization_context: Arc::new(Default::default()),
|
|
widget_constructors: Arc::new(Default::default()),
|
|
dyn_type_constructors: Arc::new(Default::default()),
|
|
resource_manager: ResourceManager::new(Arc::new(FsResourceIo), task_pool.clone()),
|
|
task_pool,
|
|
})
|
|
.unwrap();
|
|
|
|
let is_running = Cell::new(true);
|
|
|
|
engine.enable_plugins(
|
|
None,
|
|
true,
|
|
ApplicationLoopController::Headless {
|
|
running: &is_running,
|
|
},
|
|
);
|
|
|
|
let mut scene = Scene::new();
|
|
|
|
let handle = PivotBuilder::new(
|
|
BaseBuilder::new()
|
|
.with_script(ScriptSpawningAsyncTasks { num: None })
|
|
.with_script(ScriptWithoutAsyncTasks {}),
|
|
)
|
|
.build(&mut scene.graph);
|
|
|
|
let scene_handle = engine.scenes.add(scene);
|
|
|
|
engine.register_scripted_scene(scene_handle);
|
|
|
|
// Spin for some time.
|
|
let mut time = 0.0;
|
|
let dt = 1.0 / 60.0;
|
|
let mut lag = 0.0;
|
|
while time <= 10.0 {
|
|
engine.update(
|
|
dt,
|
|
ApplicationLoopController::Headless {
|
|
running: &is_running,
|
|
},
|
|
&mut lag,
|
|
Default::default(),
|
|
);
|
|
time += dt;
|
|
}
|
|
|
|
// Ensure that the tasks are finished and correctly handled.
|
|
let mut scripts = engine.scenes[scene_handle].graph[handle].scripts();
|
|
assert_eq!(
|
|
scripts
|
|
.next()
|
|
.and_then(|s| s.cast::<ScriptSpawningAsyncTasks>()),
|
|
Some(&ScriptSpawningAsyncTasks { num: Some(123) })
|
|
);
|
|
assert_eq!(
|
|
scripts
|
|
.next()
|
|
.and_then(|s| s.cast::<ScriptWithoutAsyncTasks>()),
|
|
Some(&ScriptWithoutAsyncTasks {})
|
|
);
|
|
}
|
|
|
|
#[derive(Clone, Debug, Reflect, Visit)]
|
|
#[reflect(type_uuid = "9bcbf9b4-9546-42d3-965a-de055ab85475", non_comparable)]
|
|
pub struct ScriptThatDeletesItself {
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
}
|
|
|
|
impl ScriptTrait for ScriptThatDeletesItself {
|
|
fn on_init(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Initialized(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Started(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn on_deinit(&mut self, ctx: &mut ScriptDeinitContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Destroyed(Source::from_deinit_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Updated(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
|
|
let node = &mut ctx.scene.graph[ctx.handle];
|
|
node.remove_script(ctx.script_index);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Reflect, Visit)]
|
|
#[reflect(type_uuid = "9bcbf9b4-9546-42d3-965a-de055ab85475", non_comparable)]
|
|
pub struct ScriptThatAddsScripts {
|
|
num: usize,
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
}
|
|
|
|
impl ScriptTrait for ScriptThatAddsScripts {
|
|
fn on_init(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Initialized(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Started(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
|
|
for i in 0..self.num {
|
|
ctx.scene.graph[ctx.handle].add_script(SimpleScript {
|
|
stuff: i,
|
|
sender: self.sender.clone(),
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn on_deinit(&mut self, ctx: &mut ScriptDeinitContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Destroyed(Source::from_deinit_ctx(ctx)))
|
|
.unwrap();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Updated(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Reflect, Visit)]
|
|
#[reflect(type_uuid = "9bcbf9b4-9546-42d3-965a-de055ab85475", non_comparable)]
|
|
pub struct SimpleScript {
|
|
stuff: usize,
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
}
|
|
|
|
impl ScriptTrait for SimpleScript {
|
|
fn on_init(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Initialized(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Started(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn on_deinit(&mut self, ctx: &mut ScriptDeinitContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Destroyed(Source::from_deinit_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) -> GameResult {
|
|
self.sender
|
|
.send(Event::Updated(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_script_adding_removing() {
|
|
let resource_manager =
|
|
ResourceManager::new(Arc::new(FsResourceIo), Arc::new(Default::default()));
|
|
let mut scene = Scene::new();
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
let node_handle = PivotBuilder::new(
|
|
BaseBuilder::new()
|
|
.with_script(ScriptThatDeletesItself { sender: tx.clone() })
|
|
.with_script(ScriptThatAddsScripts { num: 2, sender: tx }),
|
|
)
|
|
.build(&mut scene.graph)
|
|
.to_base();
|
|
assert_eq!(node_handle, Handle::<Node>::new(1, 1));
|
|
|
|
let mut scene_container = SceneContainer::new(Default::default());
|
|
|
|
let scene_handle = scene_container.add(scene);
|
|
|
|
let mut script_processor = ScriptProcessor::default();
|
|
|
|
script_processor.register_scripted_scene(scene_handle, &resource_manager);
|
|
|
|
let mut task_pool = TaskPoolHandler::new(Arc::new(TaskPool::new()));
|
|
let mut gc = GraphicsContext::Uninitialized(Default::default());
|
|
let mut user_interfaces = UiContainer::default();
|
|
|
|
for iteration in 0..2 {
|
|
script_processor.handle_scripts(
|
|
&mut scene_container,
|
|
&mut Vec::new(),
|
|
&resource_manager,
|
|
&mut task_pool,
|
|
&mut gc,
|
|
&mut user_interfaces,
|
|
0.0,
|
|
0.0,
|
|
&Default::default(),
|
|
&mut Default::default(),
|
|
);
|
|
|
|
match iteration {
|
|
0 => {
|
|
for i in 0..2 {
|
|
assert_eq!(
|
|
rx.try_recv(),
|
|
Ok(Event::Initialized(Source {
|
|
node_handle,
|
|
script_index: i,
|
|
}))
|
|
);
|
|
}
|
|
for i in 0..2 {
|
|
assert_eq!(
|
|
rx.try_recv(),
|
|
Ok(Event::Started(Source {
|
|
node_handle,
|
|
script_index: i,
|
|
}))
|
|
);
|
|
}
|
|
for i in 2..4 {
|
|
assert_eq!(
|
|
rx.try_recv(),
|
|
Ok(Event::Initialized(Source {
|
|
node_handle,
|
|
script_index: i,
|
|
}))
|
|
);
|
|
}
|
|
for i in 2..4 {
|
|
assert_eq!(
|
|
rx.try_recv(),
|
|
Ok(Event::Started(Source {
|
|
node_handle,
|
|
script_index: i,
|
|
}))
|
|
);
|
|
}
|
|
for i in 0..4 {
|
|
assert_eq!(
|
|
rx.try_recv(),
|
|
Ok(Event::Updated(Source {
|
|
node_handle,
|
|
script_index: i,
|
|
}))
|
|
);
|
|
}
|
|
assert_eq!(
|
|
rx.try_recv(),
|
|
Ok(Event::Destroyed(Source {
|
|
node_handle,
|
|
script_index: 0,
|
|
}))
|
|
);
|
|
|
|
assert_eq!(rx.try_recv(), Err(TryRecvError::Empty));
|
|
}
|
|
1 => {
|
|
for i in 0..3 {
|
|
assert_eq!(
|
|
rx.try_recv(),
|
|
Ok(Event::Updated(Source {
|
|
node_handle,
|
|
script_index: i,
|
|
}))
|
|
);
|
|
}
|
|
|
|
assert_eq!(rx.try_recv(), Err(TryRecvError::Empty));
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
}
|