diff --git a/examples/hello/hello.fs b/examples/hello/hello.fs index f626a48..c3bfc70 100644 --- a/examples/hello/hello.fs +++ b/examples/hello/hello.fs @@ -43,6 +43,10 @@ type Msg = let game: Game = GameBuilder.local Model.initial +let update model msg = + printfn "Running update" + (model, Effect.none()) + let tick model (tick: Time.FrameTime) = let applyVelocity (tick: Time.FrameTime) ball = @@ -111,5 +115,6 @@ let init (_args: array) = |> Transform.translateZ ((sin (frameTime.tts * 5.0f)) * 1.0f) |]) ) + |> GameBuilder.update update |> GameBuilder.tick tick |> Runtime.runGame \ No newline at end of file diff --git a/runtime/functor-runtime-common/src/effect.rs b/runtime/functor-runtime-common/src/effect.rs index 01e209d..1588d82 100644 --- a/runtime/functor-runtime-common/src/effect.rs +++ b/runtime/functor-runtime-common/src/effect.rs @@ -1,4 +1,4 @@ -use fable_library_rust::Native_::Func1; +use fable_library_rust::{NativeArray_, Native_::Func1}; #[derive(Clone)] pub enum Effect { @@ -7,6 +7,13 @@ pub enum Effect { } impl Effect { + pub fn is_none(effect: &Effect) -> bool { + match effect { + Effect::None => true, + _ => false, + } + } + pub fn none() -> Effect { Effect::None } @@ -21,4 +28,11 @@ impl Effect { Effect::Wrapped(v) => Effect::Wrapped(mapping(v)), } } + + pub fn run(effect: Effect) -> NativeArray_::Array { + match effect { + Effect::None => NativeArray_::array_from(vec![]), + Effect::Wrapped(v) => NativeArray_::array_from(vec![v]), + } + } } diff --git a/runtime/functor-runtime-common/src/effect_queue.rs b/runtime/functor-runtime-common/src/effect_queue.rs new file mode 100644 index 0000000..c8f9934 --- /dev/null +++ b/runtime/functor-runtime-common/src/effect_queue.rs @@ -0,0 +1,38 @@ +use std::{cell::RefCell, collections::VecDeque, fmt, sync::Arc}; + +use crate::Effect; + +#[derive(Clone)] +pub struct EffectQueue { + queue: Arc>>>, +} + +impl EffectQueue { + pub fn new() -> EffectQueue { + EffectQueue { + queue: Arc::new(RefCell::new(VecDeque::new())), + } + } + + pub fn count(effect_queue: &EffectQueue) -> i32 { + effect_queue.queue.borrow().len() as i32 + } + + pub fn enqueue(effect_queue: &EffectQueue, effect: Effect) { + if !Effect::is_none(&effect) { + effect_queue.queue.borrow_mut().push_front(effect); + } + } + + pub fn dequeue(effect_queue: &EffectQueue) -> Option> { + effect_queue.queue.borrow_mut().pop_back() + } +} + +// Implement Debug manually +impl fmt::Debug for EffectQueue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Build the debug representation + f.debug_struct("EffectQueue").finish() + } +} diff --git a/runtime/functor-runtime-common/src/lib.rs b/runtime/functor-runtime-common/src/lib.rs index c90cc0c..53ed73c 100644 --- a/runtime/functor-runtime-common/src/lib.rs +++ b/runtime/functor-runtime-common/src/lib.rs @@ -51,6 +51,7 @@ impl OpaqueState { pub mod animation; pub mod asset; mod effect; +mod effect_queue; mod frame_time; pub mod geometry; pub mod io; @@ -65,6 +66,7 @@ mod shader_program; pub mod texture; pub use effect::*; +pub use effect_queue::*; pub use frame_time::*; pub use render_context::*; pub use scene3d::*; diff --git a/runtime/functor-runtime-desktop/src/game.rs b/runtime/functor-runtime-desktop/src/game.rs index 671deff..07077ac 100644 --- a/runtime/functor-runtime-desktop/src/game.rs +++ b/runtime/functor-runtime-desktop/src/game.rs @@ -3,6 +3,8 @@ use functor_runtime_common::{FrameTime, Scene3D}; pub trait Game { fn check_hot_reload(&mut self, frame_time: FrameTime); + fn tick(&mut self, frame_time: FrameTime); + fn render(&mut self, frame_time: FrameTime) -> Scene3D; fn quit(&mut self); diff --git a/runtime/functor-runtime-desktop/src/hot_reload_game.rs b/runtime/functor-runtime-desktop/src/hot_reload_game.rs index ddde8d1..6c01df8 100644 --- a/runtime/functor-runtime-desktop/src/hot_reload_game.rs +++ b/runtime/functor-runtime-desktop/src/hot_reload_game.rs @@ -44,6 +44,14 @@ impl Game for HotReloadGame { } } + fn tick(&mut self, frame_time: FrameTime) { + unsafe { + let tick_func: Symbol = + self.library.as_ref().unwrap().get(b"tick").unwrap(); + tick_func(frame_time) + } + } + fn quit(&mut self) { if let Some(handle) = self.watcher_thread.take() { handle.join().expect("Failed to join watcher thread"); diff --git a/runtime/functor-runtime-desktop/src/main.rs b/runtime/functor-runtime-desktop/src/main.rs index e34b610..f91003b 100644 --- a/runtime/functor-runtime-desktop/src/main.rs +++ b/runtime/functor-runtime-desktop/src/main.rs @@ -138,6 +138,8 @@ pub async fn main() { } } + game.tick(time.clone()); + gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT); let radius = 5.0; let view_matrix: Matrix4 = Matrix4::look_at_rh( diff --git a/runtime/functor-runtime-desktop/src/static_game.rs b/runtime/functor-runtime-desktop/src/static_game.rs index 4602c68..3f89008 100644 --- a/runtime/functor-runtime-desktop/src/static_game.rs +++ b/runtime/functor-runtime-desktop/src/static_game.rs @@ -21,6 +21,13 @@ impl Game for StaticGame { } } + fn tick(&mut self, frame_time: FrameTime) { + unsafe { + let tick_func: Symbol = self.library.get(b"tick").unwrap(); + tick_func(frame_time) + } + } + fn quit(&mut self) { // Noop - nothing to do yet } diff --git a/runtime/functor-runtime-web/index.html b/runtime/functor-runtime-web/index.html index 024170a..2840968 100644 --- a/runtime/functor-runtime-web/index.html +++ b/runtime/functor-runtime-web/index.html @@ -6,7 +6,7 @@ diff --git a/runtime/functor-runtime-web/src/lib.rs b/runtime/functor-runtime-web/src/lib.rs index 7b9a0e3..7e0dd66 100644 --- a/runtime/functor-runtime-web/src/lib.rs +++ b/runtime/functor-runtime-web/src/lib.rs @@ -41,6 +41,9 @@ fn request_animation_frame(f: &Closure) { extern "C" { #[wasm_bindgen(js_namespace = game, js_name = render)] fn game_render(frameTimeJs: JsValue) -> JsValue; + + #[wasm_bindgen(js_namespace = game, js_name = tick)] + fn game_tick(frameTimeJs: JsValue); } #[wasm_bindgen(start)] @@ -206,6 +209,8 @@ async fn run_async() -> Result<(), JsValue> { // let scene = Scene3D::cube(); + game_tick(functor_runtime_common::to_js_value(&frame_time)); + let val = game_render(functor_runtime_common::to_js_value(&frame_time)); web_sys::console::log_2(&JsValue::from_str("calling render"), &val); diff --git a/src/Functor.Game/Effect.fs b/src/Functor.Game/Effect.fs index 3819404..2089eb1 100644 --- a/src/Functor.Game/Effect.fs +++ b/src/Functor.Game/Effect.fs @@ -13,4 +13,10 @@ module Effect = let wrapped (a: 'a) : effect<'a> = nativeOnly [] - let map (fn: 'a -> 'b) (eff: effect<'a>) : effect<'b> = nativeOnly \ No newline at end of file + let map (fn: 'a -> 'b) (eff: effect<'a>) : effect<'b> = nativeOnly + + + // TODO: These should live elsewhere because they aren't user space + + [] + let run (eff: effect<'a>) : 'a array = nativeOnly \ No newline at end of file diff --git a/src/Functor.Game/EffectQueue.fs b/src/Functor.Game/EffectQueue.fs new file mode 100644 index 0000000..bb28850 --- /dev/null +++ b/src/Functor.Game/EffectQueue.fs @@ -0,0 +1,19 @@ +namespace Functor +open Fable.Core + +[")>] type EffectQueue<'msg> = | Noop + + +module EffectQueue = + + [] + let empty (): EffectQueue<_> = nativeOnly + + [] + let count (effectQueue: EffectQueue<'a>): int = nativeOnly + + [] + let enqueue (eff: effect<'a>) (effectQueue: EffectQueue<'a>) : unit = nativeOnly + + [] + let dequeue (effectQueue: EffectQueue<'a>): Option> = nativeOnly \ No newline at end of file diff --git a/src/Functor.Game/Functor.Game.fsproj b/src/Functor.Game/Functor.Game.fsproj index d7b7d2a..f172751 100644 --- a/src/Functor.Game/Functor.Game.fsproj +++ b/src/Functor.Game/Functor.Game.fsproj @@ -5,6 +5,7 @@ + diff --git a/src/Functor.Game/Game.fs b/src/Functor.Game/Game.fs index bec597a..b1b013d 100644 --- a/src/Functor.Game/Game.fs +++ b/src/Functor.Game/Game.fs @@ -47,5 +47,9 @@ module GameRunner = let (newModel, effect) = game.tick model tick (newModel, effect) + let update<'model, 'msg> (game: Game<'model, 'msg>) (model: 'model) (msg: 'msg) = + let (newModel, effect) = game.update model msg + (newModel, effect) + let draw3d<'model, 'msg> (game: Game<'model, 'msg>) (model: 'model) (tick: Time.FrameTime) = game.draw3d model tick \ No newline at end of file diff --git a/src/Functor.Game/Game.fsi b/src/Functor.Game/Game.fsi index b70aca6..7da3374 100644 --- a/src/Functor.Game/Game.fsi +++ b/src/Functor.Game/Game.fsi @@ -25,4 +25,5 @@ module GameBuilder = module GameRunner = val initialState: Game<'model, 'msg> -> 'model val tick: Game<'model, 'msg> -> 'model -> Time.FrameTime -> ('model * effect<'msg>) + val update: Game<'model, 'msg> -> 'model -> 'msg -> ('model * effect<'msg>) val draw3d: Game<'model, 'msg> -> 'model -> Time.FrameTime -> Graphics.Scene3D \ No newline at end of file diff --git a/src/Functor.Game/Runtime.fs b/src/Functor.Game/Runtime.fs index 3955b2e..f32179a 100644 --- a/src/Functor.Game/Runtime.fs +++ b/src/Functor.Game/Runtime.fs @@ -14,6 +14,7 @@ module Runtime let mutable currentRunner: Option = None open Fable.Core.Rust + open System.Collections.Generic /////////////////////////////// // WebAssembly API @@ -36,6 +37,15 @@ module Runtime let from_js<'a>(obj: JsValue): 'a = nativeOnly + [] + let tick_wasm (frameTimeJs: JsValue): unit = + let frameTime = frameTimeJs |> UnsafeJsValue.from_js; + if currentRunner.IsSome then + frameTime + |> currentRunner.Value.tick + else + raise (System.Exception("No runner")) + [] let test_render_wasm (frameTimeJs: JsValue): JsValue = let frameTime = frameTimeJs |> UnsafeJsValue.from_js; @@ -87,6 +97,7 @@ module Runtime type GameExecutor<'Msg, 'Model>(game: Game<'Model, 'Msg>, initialState: 'Model) = let myGame = game let mutable state: 'Model = initialState + let mutable effectQueue: EffectQueue<'Msg> = EffectQueue.empty() do printfn "Hello from GameRunner!" interface IRunner with @@ -98,9 +109,29 @@ module Runtime // Todo: If first frame, run 'init' + printfn "Effect count: %d" (EffectQueue.count effectQueue) // Todo: Run any pending effects + // Print the number of pending effects in the queue + + let maybe_effect = EffectQueue.dequeue effectQueue; + + let finalState = + match maybe_effect with + | None -> + printfn "No effect" + state + | Some _e -> + let arr: 'Msg array = Effect.run _e + + Array.fold (fun (currentState) msg -> + let (newState, effect) = GameRunner.update myGame state msg + EffectQueue.enqueue effect effectQueue + newState + ) (state) arr + + let (newState, effect) = GameRunner.tick myGame finalState frameTime - let (newState, effects) = GameRunner.tick myGame state frameTime + EffectQueue.enqueue effect effectQueue; // Todo: Run tick effects