mirror of
https://github.com/FyroxEngine/Fyrox.git
synced 2026-10-12 02:02:20 +00:00
3486 lines
132 KiB
Rust
3486 lines
132 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 task;
|
|
|
|
mod hotreload;
|
|
|
|
use crate::{
|
|
asset::{
|
|
event::ResourceEvent,
|
|
manager::{ResourceManager, ResourceWaitContext},
|
|
state::ResourceState,
|
|
untyped::{ResourceKind, UntypedResource},
|
|
Resource,
|
|
},
|
|
core::{
|
|
algebra::Vector2,
|
|
futures::{executor::block_on, future::join_all},
|
|
instant,
|
|
log::Log,
|
|
pool::Handle,
|
|
reflect::Reflect,
|
|
task::TaskPool,
|
|
variable::try_inherit_properties,
|
|
},
|
|
engine::{error::EngineError, task::TaskPoolHandler},
|
|
event::Event,
|
|
graph::{BaseSceneGraph, NodeMapping, SceneGraph},
|
|
gui::{
|
|
constructor::WidgetConstructorContainer,
|
|
font::{loader::FontLoader, Font, BUILT_IN_FONT},
|
|
loader::UserInterfaceLoader,
|
|
style::{self, resource::StyleLoader, Style},
|
|
UiContainer, UiUpdateSwitches, UserInterface,
|
|
},
|
|
material::{
|
|
self,
|
|
loader::MaterialLoader,
|
|
shader::{loader::ShaderLoader, Shader, ShaderResource, ShaderResourceExtension},
|
|
Material,
|
|
},
|
|
plugin::{
|
|
dylib::DyLibDynamicPlugin, DynamicPlugin, Plugin, PluginContainer, PluginContext,
|
|
PluginRegistrationContext,
|
|
},
|
|
renderer::{framework::error::FrameworkError, Renderer},
|
|
resource::{
|
|
curve::{loader::CurveLoader, CurveResourceState},
|
|
model::{loader::ModelLoader, Model, ModelResource},
|
|
texture::{
|
|
self, loader::TextureLoader, CompressionOptions, Texture, TextureImportOptions,
|
|
TextureKind, TextureMinificationFilter, TextureResource, TextureResourceExtension,
|
|
},
|
|
},
|
|
scene::{
|
|
base::NodeScriptMessage,
|
|
camera::SkyBoxKind,
|
|
graph::{GraphUpdateSwitches, NodePool},
|
|
mesh::surface::{self, SurfaceData, SurfaceDataLoader},
|
|
navmesh,
|
|
node::{
|
|
constructor::{new_node_constructor_container, NodeConstructorContainer},
|
|
Node,
|
|
},
|
|
sound::SoundEngine,
|
|
tilemap::{
|
|
brush::{TileMapBrush, TileMapBrushLoader},
|
|
tileset::{TileSet, TileSetLoader},
|
|
},
|
|
tilemap::{CustomTileCollider, TileMapData},
|
|
Scene, SceneContainer, SceneLoader,
|
|
},
|
|
script::{
|
|
constructor::ScriptConstructorContainer, PluginsRefMut, RoutingStrategy, Script,
|
|
ScriptContext, ScriptDeinitContext, ScriptMessage, ScriptMessageContext, ScriptMessageKind,
|
|
ScriptMessageSender, UniversalScriptContext,
|
|
},
|
|
window::{Window, WindowBuilder},
|
|
};
|
|
use fxhash::{FxHashMap, FxHashSet};
|
|
use fyrox_animation::AnimationTracksData;
|
|
use fyrox_core::visitor::error::VisitError;
|
|
use fyrox_graphics::server::SharedGraphicsServer;
|
|
use fyrox_graphics_gl::server::GlGraphicsServer;
|
|
use fyrox_sound::{
|
|
buffer::{loader::SoundBufferLoader, SoundBuffer},
|
|
renderer::hrtf::{HrirSphereLoader, HrirSphereResourceData},
|
|
};
|
|
use std::{
|
|
any::TypeId,
|
|
cell::Cell,
|
|
collections::{HashSet, VecDeque},
|
|
fmt::{Display, Formatter},
|
|
io::Cursor,
|
|
ops::{Deref, DerefMut},
|
|
path::{Path, PathBuf},
|
|
rc::Rc,
|
|
sync::{
|
|
mpsc::{channel, Receiver, Sender},
|
|
Arc,
|
|
},
|
|
time::Duration,
|
|
};
|
|
use uuid::Uuid;
|
|
use winit::{
|
|
dpi::{Position, Size},
|
|
event_loop::EventLoopWindowTarget,
|
|
window::Icon,
|
|
window::WindowAttributes,
|
|
};
|
|
|
|
/// Serialization context holds runtime type information that allows to create unknown types using
|
|
/// their UUIDs and a respective constructors.
|
|
pub struct SerializationContext {
|
|
/// A node constructor container.
|
|
pub node_constructors: NodeConstructorContainer,
|
|
/// A script constructor container.
|
|
pub script_constructors: ScriptConstructorContainer,
|
|
}
|
|
|
|
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(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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!")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SceneLoadingOptions {
|
|
derived: bool,
|
|
}
|
|
|
|
/// A helper that is used to load scenes asynchronously.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```rust
|
|
/// use fyrox_impl::{
|
|
/// core::{color::Color, visitor::prelude::*, reflect::prelude::*, log::Log, pool::Handle},
|
|
/// plugin::{Plugin, PluginContext},
|
|
/// scene::Scene,
|
|
/// };
|
|
/// use std::path::Path;
|
|
///
|
|
/// #[derive(Visit, Reflect, Debug)]
|
|
/// #[reflect(non_cloneable)]
|
|
/// struct MyGame {
|
|
/// scene: Handle<Scene>,
|
|
/// }
|
|
///
|
|
/// impl MyGame {
|
|
/// pub fn new(scene_path: Option<&str>, context: PluginContext) -> Self {
|
|
/// context
|
|
/// .async_scene_loader
|
|
/// .request(scene_path.unwrap_or("data/scene.rgs"));
|
|
///
|
|
/// Self {
|
|
/// scene: Handle::NONE,
|
|
/// }
|
|
/// }
|
|
/// }
|
|
///
|
|
/// impl Plugin for MyGame {
|
|
/// fn on_scene_begin_loading(&mut self, path: &Path, _context: &mut PluginContext) {
|
|
/// Log::info(format!("{} scene has started loading.", path.display()));
|
|
///
|
|
/// // Use this method if you need to so something when a scene started loading.
|
|
/// }
|
|
///
|
|
/// fn on_scene_loaded(
|
|
/// &mut self,
|
|
/// path: &Path,
|
|
/// scene: Handle<Scene>,
|
|
/// data: &[u8],
|
|
/// context: &mut PluginContext,
|
|
/// ) {
|
|
/// // Optionally remove previous scene.
|
|
/// if self.scene.is_some() {
|
|
/// context.scenes.remove(self.scene);
|
|
/// }
|
|
///
|
|
/// // Remember new scene handle.
|
|
/// self.scene = scene;
|
|
///
|
|
/// Log::info(format!("{} scene was loaded!", path.display()));
|
|
///
|
|
/// // Do something with a newly loaded scene.
|
|
/// let scene_ref = &mut context.scenes[scene];
|
|
///
|
|
/// scene_ref.rendering_options.ambient_lighting_color = Color::opaque(20, 20, 20);
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// This example shows a typical usage of the loader, an instance of which is available in the
|
|
/// plugin context. `Game::new` requests a new scene, which internally asks a resource manager to
|
|
/// load the scene. Then, when the scene is fully loaded, the engine calls `Plugin::on_scene_loaded`
|
|
/// method which allows you to do something with the newly loaded scene by taking a reference of it.
|
|
pub struct AsyncSceneLoader {
|
|
resource_manager: ResourceManager,
|
|
serialization_context: Arc<SerializationContext>,
|
|
receiver: Receiver<SceneLoadingResult>,
|
|
sender: Sender<SceneLoadingResult>,
|
|
loading_scenes: FxHashMap<PathBuf, LoadingScene>,
|
|
}
|
|
|
|
struct LoadingScene {
|
|
reported: bool,
|
|
path: PathBuf,
|
|
options: SceneLoadingOptions,
|
|
}
|
|
|
|
struct SceneLoadingResult {
|
|
path: PathBuf,
|
|
result: Result<(Scene, Vec<u8>), VisitError>,
|
|
}
|
|
|
|
impl AsyncSceneLoader {
|
|
fn new(
|
|
resource_manager: ResourceManager,
|
|
serialization_context: Arc<SerializationContext>,
|
|
) -> Self {
|
|
let (sender, receiver) = channel();
|
|
Self {
|
|
resource_manager,
|
|
serialization_context,
|
|
receiver,
|
|
sender,
|
|
loading_scenes: Default::default(),
|
|
}
|
|
}
|
|
|
|
fn request_with_options<P: AsRef<Path>>(&mut self, path: P, opts: SceneLoadingOptions) {
|
|
let path = path.as_ref().to_path_buf();
|
|
|
|
if self.loading_scenes.contains_key(&path) {
|
|
Log::warn(format!("A scene {} is already loading!", path.display()))
|
|
} else {
|
|
// Register a new request.
|
|
self.loading_scenes.insert(
|
|
path.clone(),
|
|
LoadingScene {
|
|
reported: false,
|
|
path: path.clone(),
|
|
options: opts,
|
|
},
|
|
);
|
|
|
|
// Start loading in a separate off-thread task.
|
|
let sender = self.sender.clone();
|
|
let serialization_context = self.serialization_context.clone();
|
|
let resource_manager = self.resource_manager.clone();
|
|
|
|
// Aquire the resource IO from the resource manager
|
|
let io = resource_manager.resource_io();
|
|
|
|
let future = async move {
|
|
match SceneLoader::from_file(
|
|
path.clone(),
|
|
io.as_ref(),
|
|
serialization_context,
|
|
resource_manager.clone(),
|
|
)
|
|
.await
|
|
{
|
|
Ok((loader, data)) => {
|
|
let scene = loader.finish().await;
|
|
Log::verify(sender.send(SceneLoadingResult {
|
|
path,
|
|
result: Ok((scene, data)),
|
|
}));
|
|
}
|
|
Err(e) => {
|
|
Log::verify(sender.send(SceneLoadingResult {
|
|
path,
|
|
result: Err(e),
|
|
}));
|
|
}
|
|
}
|
|
};
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
std::thread::spawn(move || block_on(future));
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
{
|
|
crate::core::wasm_bindgen_futures::spawn_local(future);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Requests a scene for loading as derived scene. See [`AsyncSceneLoader`] for usage example.
|
|
///
|
|
/// ## Raw vs Derived Scene
|
|
///
|
|
/// Derived scene means its nodes will derive their properties from the nodes from the source
|
|
/// scene. Derived scene is useful for saved games - you can serialize your scene as usual and
|
|
/// it will only contain a "difference" between the original scene and yours. To load the same
|
|
/// scene as raw scene use [`Self::request_raw`] method.
|
|
///
|
|
/// Raw scene, on other hand, loads the scene as-is without any additional markings for the
|
|
/// scene nodes. It could be useful to load saved games.
|
|
pub fn request<P: AsRef<Path>>(&mut self, path: P) {
|
|
self.request_with_options(path, SceneLoadingOptions { derived: true });
|
|
}
|
|
|
|
/// Requests a scene for loading in raw mode. See [`Self::request`] docs for more info.
|
|
pub fn request_raw<P: AsRef<Path>>(&mut self, path: P) {
|
|
self.request_with_options(path, SceneLoadingOptions { derived: false });
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
|
|
/// An instance of the async scene loader. See [`AsyncSceneLoader`] docs for usage example.
|
|
pub async_scene_loader: AsyncSceneLoader,
|
|
|
|
/// Task pool for asynchronous task management.
|
|
pub task_pool: TaskPoolHandler,
|
|
|
|
performance_statistics: PerformanceStatistics,
|
|
|
|
model_events_receiver: Receiver<ResourceEvent>,
|
|
|
|
#[allow(dead_code)] // Keep engine instance alive.
|
|
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,
|
|
|
|
/// 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>,
|
|
|
|
/// Script processor is used to run script methods in a strict order.
|
|
pub script_processor: ScriptProcessor,
|
|
}
|
|
|
|
/// Performs dispatch of script messages.
|
|
pub struct ScriptMessageDispatcher {
|
|
type_groups: FxHashMap<TypeId, FxHashSet<Handle<Node>>>,
|
|
message_receiver: Receiver<ScriptMessage>,
|
|
}
|
|
|
|
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.type_groups
|
|
.entry(TypeId::of::<T>())
|
|
.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>) {
|
|
if let Some(group) = self.type_groups.get_mut(&TypeId::of::<T>()) {
|
|
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,
|
|
) {
|
|
while let Ok(message) = self.message_receiver.try_recv() {
|
|
let receivers = self.type_groups.get(&message.payload.deref().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,
|
|
};
|
|
|
|
process_node_scripts(&mut context, &mut |s, ctx| {
|
|
s.on_message(&mut *payload, ctx)
|
|
})
|
|
}
|
|
}
|
|
ScriptMessageKind::Hierarchical { root, routing } => match routing {
|
|
RoutingStrategy::Up => {
|
|
let mut node = root;
|
|
while let Some(node_ref) = scene.graph.try_get(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,
|
|
};
|
|
|
|
if receivers.contains(&node) {
|
|
process_node_scripts(&mut context, &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,
|
|
};
|
|
|
|
if receivers.contains(&node) {
|
|
process_node_scripts(&mut context, &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,
|
|
};
|
|
|
|
process_node_scripts(&mut context, &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,
|
|
) {
|
|
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,
|
|
};
|
|
|
|
'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(
|
|
script_index,
|
|
&mut context,
|
|
&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));
|
|
},
|
|
);
|
|
}
|
|
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(
|
|
script_index,
|
|
&mut context,
|
|
&mut |script, context| {
|
|
if script.initialized && !script.started {
|
|
script.on_start(context);
|
|
script.started = true;
|
|
|
|
update_queue.push_back((handle, script_index));
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
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(script_index, &mut context, &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,
|
|
);
|
|
|
|
// 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,
|
|
};
|
|
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.
|
|
script.on_deinit(&mut context);
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
};
|
|
|
|
// 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(&mut context, &mut |script, context| {
|
|
if script.initialized {
|
|
script.on_deinit(context)
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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.kind()
|
|
));
|
|
|
|
// 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,
|
|
&EventLoopWindowTarget<()>,
|
|
WindowBuilder,
|
|
) -> 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| {
|
|
GlGraphicsServer::new(
|
|
params.vsync,
|
|
params.msaa_sample_count,
|
|
window_target,
|
|
window_builder,
|
|
)
|
|
}))
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
}
|
|
|
|
impl Default for GraphicsContextParams {
|
|
fn default() -> Self {
|
|
Self {
|
|
window_attributes: Default::default(),
|
|
vsync: true,
|
|
msaa_sample_count: None,
|
|
graphics_server_constructor: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 resource manager.
|
|
pub resource_manager: ResourceManager,
|
|
/// Task pool for asynchronous task management.
|
|
pub task_pool: Arc<TaskPool>,
|
|
}
|
|
|
|
fn process_node_script<T, C>(index: usize, context: &mut C, func: &mut T) -> bool
|
|
where
|
|
T: FnMut(&mut Script, &mut C),
|
|
C: UniversalScriptContext,
|
|
{
|
|
let Some(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;
|
|
};
|
|
|
|
func(&mut script, context);
|
|
|
|
match context.node() {
|
|
Some(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);
|
|
}
|
|
}
|
|
None => {
|
|
// 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>(context: &mut C, func: &mut T)
|
|
where
|
|
T: FnMut(&mut Script, &mut C),
|
|
C: UniversalScriptContext,
|
|
{
|
|
let mut index = 0;
|
|
loop {
|
|
context.set_script_index(index);
|
|
|
|
if !process_node_script(index, context, func) {
|
|
return;
|
|
}
|
|
|
|
// Advance to the next script.
|
|
index += 1;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn process_scripts<T>(
|
|
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,
|
|
mut func: T,
|
|
) where
|
|
T: FnMut(&mut Script, &mut ScriptContext),
|
|
{
|
|
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,
|
|
};
|
|
|
|
for node_index in 0..context.scene.graph.capacity() {
|
|
context.handle = context.scene.graph.handle_from_index(node_index);
|
|
|
|
process_node_scripts(&mut context, &mut func);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn initialize_resource_manager_loaders(
|
|
resource_manager: &ResourceManager,
|
|
serialization_context: Arc<SerializationContext>,
|
|
) {
|
|
let model_loader = ModelLoader {
|
|
resource_manager: resource_manager.clone(),
|
|
serialization_context,
|
|
default_import_options: Default::default(),
|
|
};
|
|
|
|
let mut state = resource_manager.state();
|
|
|
|
for shader in ShaderResource::standard_shaders() {
|
|
state.built_in_resources.add((*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(texture::PLACEHOLDER.clone());
|
|
state.built_in_resources.add(style::DEFAULT_STYLE.clone());
|
|
|
|
for material in [
|
|
&*material::STANDARD,
|
|
&*material::STANDARD_2D,
|
|
&*material::STANDARD_SPRITE,
|
|
&*material::STANDARD_TERRAIN,
|
|
&*material::STANDARD_TWOSIDES,
|
|
&*material::STANDARD_PARTICLE_SYSTEM,
|
|
] {
|
|
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.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::default());
|
|
loaders.set(UserInterfaceLoader {
|
|
resource_manager: resource_manager.clone(),
|
|
});
|
|
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.
|
|
WindowTarget(&'a EventLoopWindowTarget<()>),
|
|
}
|
|
|
|
impl ApplicationLoopController<'_> {
|
|
/// Asks the loop controller to end the application execution.
|
|
pub fn exit(&self) {
|
|
match self {
|
|
ApplicationLoopController::Headless { running } => running.set(false),
|
|
ApplicationLoopController::WindowTarget(window_target) => window_target.exit(),
|
|
}
|
|
}
|
|
}
|
|
|
|
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::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()
|
|
/// };
|
|
/// let task_pool = Arc::new(TaskPool::new());
|
|
///
|
|
/// Engine::new(EngineInitParams {
|
|
/// graphics_context_params,
|
|
/// resource_manager: ResourceManager::new(Arc::new(FsResourceIo), task_pool.clone()),
|
|
/// 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,
|
|
resource_manager,
|
|
task_pool,
|
|
} = params;
|
|
|
|
initialize_resource_manager_loaders(&resource_manager, serialization_context.clone());
|
|
|
|
let (rx, tx) = channel();
|
|
resource_manager.state().event_broadcaster.add(rx);
|
|
|
|
let sound_engine = SoundEngine::without_device();
|
|
|
|
let user_interfaces =
|
|
UiContainer::new_with_ui(UserInterface::new(Vector2::new(100.0, 100.0)));
|
|
|
|
resource_manager.update_or_load_registry();
|
|
|
|
Ok(Self {
|
|
graphics_context: GraphicsContext::Uninitialized(graphics_context_params),
|
|
model_events_receiver: tx,
|
|
async_scene_loader: AsyncSceneLoader::new(
|
|
resource_manager.clone(),
|
|
serialization_context.clone(),
|
|
),
|
|
resource_manager,
|
|
scenes: SceneContainer::new(sound_engine.clone()),
|
|
sound_engine,
|
|
user_interfaces,
|
|
performance_statistics: Default::default(),
|
|
plugins: Default::default(),
|
|
serialization_context,
|
|
widget_constructors,
|
|
script_processor: Default::default(),
|
|
plugins_enabled: false,
|
|
elapsed_time: 0.0,
|
|
task_pool: TaskPoolHandler::new(task_pool),
|
|
})
|
|
}
|
|
|
|
/// 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,
|
|
window_target: &EventLoopWindowTarget<()>,
|
|
) -> Result<(), EngineError> {
|
|
if let GraphicsContext::Uninitialized(params) = &self.graphics_context {
|
|
let mut window_builder = WindowBuilder::new();
|
|
if let Some(inner_size) = params.window_attributes.inner_size {
|
|
window_builder = window_builder.with_inner_size(inner_size);
|
|
}
|
|
if let Some(min_inner_size) = params.window_attributes.min_inner_size {
|
|
window_builder = window_builder.with_min_inner_size(min_inner_size);
|
|
}
|
|
if let Some(max_inner_size) = params.window_attributes.max_inner_size {
|
|
window_builder = window_builder.with_min_inner_size(max_inner_size);
|
|
}
|
|
if let Some(position) = params.window_attributes.position {
|
|
window_builder = window_builder.with_position(position);
|
|
}
|
|
if let Some(resize_increments) = params.window_attributes.resize_increments {
|
|
window_builder = window_builder.with_resize_increments(resize_increments);
|
|
}
|
|
unsafe {
|
|
window_builder = window_builder
|
|
.with_parent_window(params.window_attributes.parent_window().cloned());
|
|
}
|
|
window_builder = window_builder
|
|
.with_resizable(params.window_attributes.resizable)
|
|
.with_enabled_buttons(params.window_attributes.enabled_buttons)
|
|
.with_title(params.window_attributes.title.clone())
|
|
.with_fullscreen(params.window_attributes.fullscreen().cloned())
|
|
.with_maximized(params.window_attributes.maximized)
|
|
.with_visible(params.window_attributes.visible)
|
|
.with_transparent(params.window_attributes.transparent)
|
|
.with_decorations(params.window_attributes.decorations)
|
|
.with_window_icon(params.window_attributes.window_icon.clone())
|
|
.with_theme(params.window_attributes.preferred_theme)
|
|
.with_content_protected(params.window_attributes.content_protected)
|
|
.with_window_level(params.window_attributes.window_level)
|
|
.with_active(params.window_attributes.active);
|
|
|
|
let (window, server) =
|
|
params.graphics_server_constructor.0(params, window_target, window_builder)?;
|
|
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(),
|
|
))
|
|
}
|
|
}
|
|
|
|
/// 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(),
|
|
});
|
|
|
|
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.handle_async_scene_loading(dt, lag, controller);
|
|
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}"
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_async_scene_loading(
|
|
&mut self,
|
|
dt: f32,
|
|
lag: &mut f32,
|
|
controller: ApplicationLoopController,
|
|
) {
|
|
let len = self.async_scene_loader.loading_scenes.len();
|
|
let mut n = 0;
|
|
while n < len {
|
|
if let Some(request) = self.async_scene_loader.loading_scenes.values_mut().nth(n) {
|
|
if !request.reported {
|
|
request.reported = true;
|
|
|
|
// Notify plugins about a scene, that started loading.
|
|
if self.plugins_enabled {
|
|
let path = request.path.clone();
|
|
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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
};
|
|
|
|
for plugin in self.plugins.iter_mut() {
|
|
plugin.on_scene_begin_loading(&path, &mut context);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
n += 1;
|
|
}
|
|
|
|
while let Ok(loading_result) = self.async_scene_loader.receiver.try_recv() {
|
|
if let Some(request) = self
|
|
.async_scene_loader
|
|
.loading_scenes
|
|
.remove(&loading_result.path)
|
|
{
|
|
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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
};
|
|
|
|
match loading_result.result {
|
|
Ok((mut scene, data)) => {
|
|
if request.options.derived {
|
|
// Create a resource, that will point to the scene we've loaded the
|
|
// scene from and force scene nodes to inherit data from them.
|
|
let model = Resource::new_ok(
|
|
Uuid::new_v4(),
|
|
ResourceKind::External,
|
|
Model {
|
|
mapping: NodeMapping::UseHandles,
|
|
// We have to create a full copy of the scene, because otherwise
|
|
// some methods (`Base::root_resource` in particular) won't work
|
|
// correctly.
|
|
scene: scene
|
|
.clone_ex(
|
|
scene.graph.get_root(),
|
|
&mut |_, _| true,
|
|
&mut |_, _| {},
|
|
&mut |_, _, _| {},
|
|
)
|
|
.0,
|
|
},
|
|
);
|
|
|
|
Log::verify(
|
|
self.resource_manager
|
|
.register(model.clone().into_untyped(), request.path.clone()),
|
|
);
|
|
|
|
for (handle, node) in scene.graph.pair_iter_mut() {
|
|
node.set_inheritance_data(handle, model.clone());
|
|
}
|
|
|
|
// Reset modified flags in every inheritable property of the scene.
|
|
// Except nodes, they're inherited in a separate place.
|
|
(&mut scene as &mut dyn Reflect).apply_recursively_mut(
|
|
&mut |object| {
|
|
let type_id = (*object).type_id();
|
|
if type_id != TypeId::of::<NodePool>() {
|
|
object.as_inheritable_variable_mut(&mut |variable| {
|
|
if let Some(variable) = variable {
|
|
variable.reset_modified_flag();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
&[
|
|
TypeId::of::<UntypedResource>(),
|
|
TypeId::of::<navmesh::Container>(),
|
|
],
|
|
)
|
|
} else {
|
|
// Take scene data from the source scene.
|
|
if let Some(source_asset) =
|
|
scene.graph[scene.graph.get_root()].root_resource()
|
|
{
|
|
let source_asset_ref = source_asset.data_ref();
|
|
let source_scene_ref = &source_asset_ref.scene;
|
|
Log::verify(try_inherit_properties(
|
|
&mut scene,
|
|
source_scene_ref,
|
|
&[
|
|
TypeId::of::<NodePool>(),
|
|
TypeId::of::<UntypedResource>(),
|
|
TypeId::of::<navmesh::Container>(),
|
|
],
|
|
));
|
|
}
|
|
}
|
|
|
|
let scene_handle = context.scenes.add(scene);
|
|
|
|
// Notify plugins about newly loaded scene.
|
|
if self.plugins_enabled {
|
|
for plugin in self.plugins.iter_mut() {
|
|
Log::info(format!(
|
|
"Scene {} was loaded successfully!",
|
|
loading_result.path.display()
|
|
));
|
|
|
|
plugin.on_scene_loaded(
|
|
&request.path,
|
|
scene_handle,
|
|
&data,
|
|
&mut context,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Err(error) => {
|
|
// Notify plugins about a scene, that is failed to load.
|
|
if self.plugins_enabled {
|
|
Log::err(format!(
|
|
"Unable to load scene {}. Reason: {:?}",
|
|
loading_result.path.display(),
|
|
error
|
|
));
|
|
|
|
for plugin in self.plugins.iter_mut() {
|
|
plugin.on_scene_loading_failed(&request.path, &error, &mut context);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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(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,
|
|
) {
|
|
if let GraphicsContext::Initialized(ref ctx) = 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);
|
|
|
|
let time = instant::Instant::now();
|
|
for ui in self.user_interfaces.iter_mut() {
|
|
ui.update(window_size, dt, ui_update_switches);
|
|
}
|
|
self.performance_statistics.ui_time = instant::Instant::now() - time;
|
|
self.elapsed_time += dt;
|
|
|
|
self.post_update_plugins(dt, controller, lag);
|
|
}
|
|
}
|
|
|
|
/// 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.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.
|
|
(plugin_task_handler)(
|
|
result.payload,
|
|
&mut self.plugins,
|
|
&mut 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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
},
|
|
)
|
|
} 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 Some(scene) = self.scenes.try_get_mut(node_task_handler.scene_handle) {
|
|
if let Some(node) = scene.graph.try_get_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())
|
|
{
|
|
(node_task_handler.closure)(
|
|
payload,
|
|
script.deref_mut(),
|
|
&mut 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,
|
|
},
|
|
);
|
|
|
|
if let Some(node) =
|
|
scene.graph.try_get_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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
};
|
|
|
|
for plugin in self.plugins.iter_mut() {
|
|
plugin.update(&mut context);
|
|
}
|
|
|
|
let mut uis = self
|
|
.user_interfaces
|
|
.pair_iter()
|
|
.map(|(h, _)| h)
|
|
.collect::<VecDeque<_>>();
|
|
|
|
while let Some(ui) = uis.pop_front() {
|
|
while let Some(message) = self
|
|
.user_interfaces
|
|
.try_get_mut(ui)
|
|
.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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
};
|
|
|
|
for plugin in self.plugins.iter_mut() {
|
|
plugin.on_ui_message(&mut context, &message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
};
|
|
|
|
for plugin in self.plugins.iter_mut() {
|
|
plugin.post_update(&mut context);
|
|
}
|
|
}
|
|
|
|
self.performance_statistics.plugins_time += instant::Instant::now() - time;
|
|
}
|
|
|
|
pub(crate) fn handle_os_event_by_plugins(
|
|
&mut self,
|
|
event: &Event<()>,
|
|
dt: f32,
|
|
controller: ApplicationLoopController,
|
|
lag: &mut f32,
|
|
) {
|
|
if self.plugins_enabled {
|
|
for plugin in self.plugins.iter_mut() {
|
|
plugin.on_os_event(
|
|
event,
|
|
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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
plugin.on_graphics_context_initialized(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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
plugin.on_graphics_context_destroyed(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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
plugin.before_rendering(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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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(
|
|
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,
|
|
|script, context| {
|
|
if script.initialized && script.started {
|
|
script.on_os_event(event, context);
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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.kind()
|
|
));
|
|
|
|
// 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);
|
|
ui.draw();
|
|
}
|
|
|
|
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().map(|ui| &ui.drawing_context),
|
|
&ctx.window,
|
|
)?;
|
|
}
|
|
|
|
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() {
|
|
plugin.init(
|
|
scene_path,
|
|
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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
},
|
|
);
|
|
}
|
|
} else {
|
|
self.handle_scripts(0.0);
|
|
|
|
for mut plugin in self.plugins.drain(..) {
|
|
// Deinit plugin first.
|
|
plugin.on_deinit(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,
|
|
performance_statistics: &self.performance_statistics,
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn register_plugin_internal(
|
|
serialization_context: &Arc<SerializationContext>,
|
|
widget_constructors: &Arc<WidgetConstructorContainer>,
|
|
resource_manager: &ResourceManager,
|
|
plugin: &dyn Plugin,
|
|
) {
|
|
plugin.register(PluginRegistrationContext {
|
|
serialization_context,
|
|
widget_constructors,
|
|
resource_manager,
|
|
});
|
|
}
|
|
|
|
fn register_plugin(&self, plugin: &dyn Plugin) {
|
|
Self::register_plugin_internal(
|
|
&self.serialization_context,
|
|
&self.widget_constructors,
|
|
&self.resource_manager,
|
|
plugin,
|
|
)
|
|
}
|
|
|
|
/// 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().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);
|
|
}
|
|
|
|
// 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.0.lock();
|
|
if let ResourceState::Ok { ref data, .. } = data.state {
|
|
data.as_reflect(&mut |reflect| {
|
|
if reflect.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.resource_manager,
|
|
plugin,
|
|
);
|
|
|
|
let mut visitor = hotreload::make_reading_visitor(
|
|
&binary_blob,
|
|
&self.serialization_context,
|
|
&self.resource_manager,
|
|
&self.widget_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.kind()));
|
|
|
|
scene_state.deserialize_into_prefab_scene(
|
|
&model,
|
|
&self.serialization_context,
|
|
&self.resource_manager,
|
|
&self.widget_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,
|
|
)?;
|
|
}
|
|
|
|
// Call `on_loaded` for plugins, so they could restore some runtime non-serializable state.
|
|
plugin.as_loaded_mut().on_loaded(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,
|
|
performance_statistics: &Default::default(),
|
|
elapsed_time: self.elapsed_time,
|
|
script_processor: &self.script_processor,
|
|
async_scene_loader: &mut self.async_scene_loader,
|
|
loop_controller: controller,
|
|
task_pool: &mut self.task_pool,
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
// Finally disable plugins.
|
|
self.enable_plugins(
|
|
None,
|
|
false,
|
|
ApplicationLoopController::Headless {
|
|
running: &Default::default(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::engine::ApplicationLoopController;
|
|
use crate::{
|
|
asset::manager::ResourceManager,
|
|
core::{
|
|
pool::Handle, reflect::prelude::*, task::TaskPool, type_traits::prelude::*,
|
|
visitor::prelude::*,
|
|
},
|
|
engine::{task::TaskPoolHandler, GraphicsContext, ScriptProcessor},
|
|
graph::BaseSceneGraph,
|
|
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, TypeUuidProvider, ComponentProvider)]
|
|
#[type_uuid(id = "2569de84-d4b2-427d-969b-d5c7b31a0ba6")]
|
|
struct MyScript {
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
spawned: bool,
|
|
}
|
|
|
|
impl ScriptTrait for MyScript {
|
|
fn on_init(&mut self, ctx: &mut ScriptContext) {
|
|
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::new(2, 1));
|
|
}
|
|
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) {
|
|
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::new(3, 1));
|
|
}
|
|
|
|
fn on_deinit(&mut self, ctx: &mut ScriptDeinitContext) {
|
|
self.sender
|
|
.send(Event::Destroyed(Source::from_deinit_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider)]
|
|
#[type_uuid(id = "1cebacd9-b500-4753-93be-39db344add21")]
|
|
struct MySubScript {
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
}
|
|
|
|
impl ScriptTrait for MySubScript {
|
|
fn on_init(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Initialized(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Started(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_deinit(&mut self, ctx: &mut ScriptDeinitContext) {
|
|
self.sender
|
|
.send(Event::Destroyed(Source::from_deinit_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Updated(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
assert_eq!(node_handle, Handle::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,
|
|
);
|
|
|
|
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)]
|
|
enum MyMessage {
|
|
Foo(usize),
|
|
Bar(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider)]
|
|
#[type_uuid(id = "bf2976ad-f41d-4de6-9a32-b1a293956058")]
|
|
struct ScriptListeningToMessages {
|
|
index: u32,
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
}
|
|
|
|
impl ScriptTrait for ScriptListeningToMessages {
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) {
|
|
ctx.message_dispatcher.subscribe_to::<MyMessage>(ctx.handle);
|
|
}
|
|
|
|
fn on_message(
|
|
&mut self,
|
|
message: &mut dyn ScriptMessagePayload,
|
|
ctx: &mut ScriptMessageContext,
|
|
) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider)]
|
|
#[type_uuid(id = "6bcbf9b4-9546-42d3-965a-de055ab85475")]
|
|
struct ScriptSendingMessages {
|
|
index: u32,
|
|
}
|
|
|
|
impl ScriptTrait for ScriptSendingMessages {
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
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,
|
|
);
|
|
|
|
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, TypeUuidProvider, ComponentProvider)]
|
|
#[type_uuid(id = "7bcbf9b4-9546-42d3-965a-de055ab85475")]
|
|
pub struct ScriptSpawningAsyncTasks {
|
|
num: Option<u32>,
|
|
}
|
|
|
|
impl ScriptTrait for ScriptSpawningAsyncTasks {
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) {
|
|
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);
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Reflect, Visit, TypeUuidProvider, ComponentProvider)]
|
|
#[type_uuid(id = "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()),
|
|
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, TypeUuidProvider, ComponentProvider)]
|
|
#[type_uuid(id = "9bcbf9b4-9546-42d3-965a-de055ab85475")]
|
|
pub struct ScriptThatDeletesItself {
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
}
|
|
|
|
impl ScriptTrait for ScriptThatDeletesItself {
|
|
fn on_init(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Initialized(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Started(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_deinit(&mut self, ctx: &mut ScriptDeinitContext) {
|
|
self.sender
|
|
.send(Event::Destroyed(Source::from_deinit_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Updated(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
|
|
let node = &mut ctx.scene.graph[ctx.handle];
|
|
node.remove_script(ctx.script_index);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Reflect, Visit, TypeUuidProvider, ComponentProvider)]
|
|
#[type_uuid(id = "9bcbf9b4-9546-42d3-965a-de055ab85475")]
|
|
pub struct ScriptThatAddsScripts {
|
|
num: usize,
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
}
|
|
|
|
impl ScriptTrait for ScriptThatAddsScripts {
|
|
fn on_init(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Initialized(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) {
|
|
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(),
|
|
});
|
|
}
|
|
}
|
|
|
|
fn on_deinit(&mut self, ctx: &mut ScriptDeinitContext) {
|
|
self.sender
|
|
.send(Event::Destroyed(Source::from_deinit_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Updated(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Reflect, Visit, TypeUuidProvider, ComponentProvider)]
|
|
#[type_uuid(id = "9bcbf9b4-9546-42d3-965a-de055ab85475")]
|
|
pub struct SimpleScript {
|
|
stuff: usize,
|
|
#[reflect(hidden)]
|
|
#[visit(skip)]
|
|
sender: Sender<Event>,
|
|
}
|
|
|
|
impl ScriptTrait for SimpleScript {
|
|
fn on_init(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Initialized(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_start(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Started(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_deinit(&mut self, ctx: &mut ScriptDeinitContext) {
|
|
self.sender
|
|
.send(Event::Destroyed(Source::from_deinit_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
|
|
fn on_update(&mut self, ctx: &mut ScriptContext) {
|
|
self.sender
|
|
.send(Event::Updated(Source::from_ctx(ctx)))
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
assert_eq!(node_handle, Handle::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,
|
|
);
|
|
|
|
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));
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
}
|