From 7228bdf210eef019297f572565ef577d350766a8 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Tue, 4 Feb 2025 00:52:54 -0500 Subject: [PATCH] Trying out an FSM for controlling the main loop. --- camera.cpp | 21 ++---- camera.hpp | 8 +-- fsm.hpp | 28 ++++++++ main.cpp | 194 ++++++++++++++++++++++++++++++++++++-------------- meson.build | 1 + tests/fsm.cpp | 67 +++++++++++++++++ 6 files changed, 243 insertions(+), 76 deletions(-) create mode 100644 fsm.hpp create mode 100644 tests/fsm.cpp diff --git a/camera.cpp b/camera.cpp index 14c0be6..84834d4 100644 --- a/camera.cpp +++ b/camera.cpp @@ -6,14 +6,12 @@ void CameraLOL::plan_run(Raycaster &rayview, int dir) { t = 0.0; targetX = rayview.$posX + int(rayview.$dirX * 1.5 * dir); targetY = rayview.$posY + int(rayview.$dirY * 1.5 * dir); - targetDir = dir; } -bool CameraLOL::play_run(Raycaster &rayview) { - t += moveSpeed; - rayview.$posX = std::lerp(rayview.$posX, targetX, t); - rayview.$posY = std::lerp(rayview.$posY, targetY, t); - return t >= 1.0; +void CameraLOL::plan_strafe(Raycaster &rayview, int dir) { + t = 0.0; + targetX = rayview.$posX + int(-rayview.$dirY * 1.5 * dir); + targetY = rayview.$posY + int(rayview.$dirX * 1.5 * dir); } void CameraLOL::plan_rotate(Raycaster &rayview, int dir) { @@ -25,8 +23,6 @@ void CameraLOL::plan_rotate(Raycaster &rayview, int dir) { targetPlaneX = rayview.$planeX * cos(angle_dir) - rayview.$planeY * sin(angle_dir); targetPlaneY = rayview.$planeX * sin(angle_dir) + rayview.$planeY * cos(angle_dir); - - targetDir = dir; } bool CameraLOL::play_rotate(Raycaster &rayview) { @@ -39,14 +35,7 @@ bool CameraLOL::play_rotate(Raycaster &rayview) { return t > 1.0; } -void CameraLOL::plan_strafe(Raycaster &rayview, int dir) { - t = 0.0; - - targetX = rayview.$posX + int(-rayview.$dirY * 1.5 * dir); - targetY = rayview.$posY + int(rayview.$dirX * 1.5 * dir); -} - -bool CameraLOL::play_strafe(Raycaster &rayview) { +bool CameraLOL::play_move(Raycaster &rayview) { t += moveSpeed; rayview.$posX = std::lerp(rayview.$posX, targetX, t); rayview.$posY = std::lerp(rayview.$posY, targetY, t); diff --git a/camera.hpp b/camera.hpp index b4f3ace..a1380cd 100644 --- a/camera.hpp +++ b/camera.hpp @@ -7,17 +7,15 @@ struct CameraLOL { double rotSpeed = 0.1; double targetX = 0.0; double targetY = 0.0; - int targetDir = 0; double targetDirX = 0.0; double targetDirY = 0.0; double targetPlaneX = 0.0; double targetPlaneY = 0.0; void plan_run(Raycaster &rayview, int dir); - bool play_run(Raycaster &rayview); + void plan_strafe(Raycaster &rayview, int dir); void plan_rotate(Raycaster &rayview, int dir); - bool play_rotate(Raycaster &rayview); - void plan_strafe(Raycaster &rayview, int dir); - bool play_strafe(Raycaster &rayview); + bool play_rotate(Raycaster &rayview); + bool play_move(Raycaster &rayview); }; diff --git a/fsm.hpp b/fsm.hpp new file mode 100644 index 0000000..8729dd8 --- /dev/null +++ b/fsm.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#ifndef FSM_DEBUG +#define FSM_STATE(C, S, E, ...) case C::S: S(E, ##__VA_ARGS__); break +#else +#define FSM_STATE(C, S, E, ...) case C::S: fmt::println(">> " #C " " #S " event={}, state={}", int(E), int($state)); S(E, ##__VA_ARGS__); fmt::println("<< " #C " state={}", int($state)); break +#endif + +template +class DeadSimpleFSM { +protected: + // BUG: don't put this in your class because state() won't work + S $state = S::START; + +public: + template + void event(E event, Types... args); + + void state(S next_state) { + $state = next_state; + } + + bool in_state(S state) { + return $state == state; + } +}; diff --git a/main.cpp b/main.cpp index 619c839..ce40c2c 100644 --- a/main.cpp +++ b/main.cpp @@ -9,6 +9,8 @@ #include "components.hpp" #include "camera.hpp" #include +#define FSM_DEBUG 1 +#include "fsm.hpp" using namespace components; @@ -20,7 +22,18 @@ void draw_gui(sf::RenderWindow &window, Raycaster &rayview, sf::Text &text, Stat window.draw(rect); text.setString( - fmt::format("FPS\nmean:{:>8.5}\nsdev: {:>8.5}\nmin: {:>8.5}\nmax: {:>8.5}\ncount:{:<10}\n\nVSync? {}\nFR Limit: {}\nDebug? {}\n\nHit R to reset.\n\ndir: {:>2.2},{:>2.2}\npos: {:>2.2},{:>2.2}", + fmt::format("FPS\n" + "mean:{:>8.5}\n" + "sdev: {:>8.5}\n" + "min: {:>8.5}\n" + "max: {:>8.5}\n" + "count:{:<10}\n\n" + "VSync? {}\n" + "FR Limit: {}\n" + "Debug? {}\n\n" + "Hit R to reset.\n\n" + "dir: {:>2.02},{:>2.02}\n" + "pos: {:>2.02},{:>2.02}", stats.mean(), stats.stddev(), stats.min, stats.max, stats.n, VSYNC, FRAME_LIMIT, DEBUG_BUILD, rayview.$dirX, @@ -43,56 +56,131 @@ void draw_weapon(sf::RenderWindow &window, sf::Sprite &weapon, float rotation) { window.draw(weapon); } -enum MoveState { - MOVE, - ROTATE, - STRAFE, +enum class MainState { + START, + MOVING, + ROTATING, IDLE }; -inline void handle_window_events(sf::RenderWindow &window, Raycaster &rayview, - MoveState &state, CameraLOL &camera) -{ - while(const auto event = window.pollEvent()) { - if(event->is()) { - window.close(); +enum class MainEvent { + STARTED, + TICK, + MOVE_FORWARD, + MOVE_BACK, + MOVE_LEFT, + MOVE_RIGHT, + ROTATE_LEFT, + ROTATE_RIGHT, + QUIT +}; + +class MainFSM : public DeadSimpleFSM { + public: + sf::RenderWindow& $window; + Raycaster& $rayview; + CameraLOL $camera; + + MainFSM(sf::RenderWindow &window, Raycaster &rayview) : + $window(window), + $rayview(rayview) { } + + void event(MainEvent ev) { + switch($state) { + FSM_STATE(MainState, START, ev); + FSM_STATE(MainState, MOVING, ev); + FSM_STATE(MainState, ROTATING, ev); + FSM_STATE(MainState, IDLE, ev); + } + } + + void START(MainEvent ) { + state(MainState::IDLE); } - if(const auto* key = event->getIf()) { - if(key->scancode == sf::Keyboard::Scan::W) { - camera.plan_run(rayview, 1); - state = MOVE; - } else if(key->scancode == sf::Keyboard::Scan::S) { - camera.plan_run(rayview, -1); - state = MOVE; + void MOVING(MainEvent ) { + if($camera.play_move($rayview)) { + state(MainState::IDLE); } + } - if(key->scancode == sf::Keyboard::Scan::Q) { - camera.plan_rotate(rayview, 1); - state = ROTATE; - } else if(key->scancode == sf::Keyboard::Scan::E) { - camera.plan_rotate(rayview, -1); - state = ROTATE; + void ROTATING(MainEvent ) { + if($camera.play_rotate($rayview)) { + state(MainState::IDLE); } + } - if(key->scancode == sf::Keyboard::Scan::D) { - camera.plan_strafe(rayview, -1); - state = STRAFE; - } else if(key->scancode == sf::Keyboard::Scan::A) { - camera.plan_strafe(rayview, 1); - state = STRAFE; + void IDLE(MainEvent ev) { + using FU = MainEvent; + + switch(ev) { + case FU::QUIT: + $window.close(); + break; + case FU::MOVE_FORWARD: + $camera.plan_run($rayview, 1); + state(MainState::MOVING); + break; + case FU::MOVE_BACK: + $camera.plan_run($rayview, -1); + state(MainState::MOVING); + break; + case FU::MOVE_LEFT: + $camera.plan_strafe($rayview, 1); + state(MainState::MOVING); + break; + case FU::MOVE_RIGHT: + $camera.plan_strafe($rayview, -1); + state(MainState::MOVING); + break; + case FU::ROTATE_LEFT: + $camera.plan_rotate($rayview, 1); + state(MainState::ROTATING); + break; + case FU::ROTATE_RIGHT: + $camera.plan_rotate($rayview, -1); + state(MainState::ROTATING); + break; + default: + dbc::sentinel("unhandled event in IDLE"); } } - } - if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::P)) { - if(rayview.$active_shader == nullptr) { - rayview.$active_shader = &rayview.$paused; - } else { - rayview.$active_shader = nullptr; + void keyboard() { + while(const auto keyev = $window.pollEvent()) { + if(keyev->is()) { + event(MainEvent::QUIT); + } + + if(const auto* key = keyev->getIf()) { + using KEY = sf::Keyboard::Scan; + switch(key->scancode) { + case KEY::W: + event(MainEvent::MOVE_FORWARD); + break; + case KEY::S: + event(MainEvent::MOVE_BACK); + break; + case KEY::Q: + event(MainEvent::ROTATE_LEFT); + break; + case KEY::E: + event(MainEvent::ROTATE_RIGHT); + break; + case KEY::D: + event(MainEvent::MOVE_RIGHT); + break; + case KEY::A: + event(MainEvent::MOVE_LEFT); + break; + default: + break; // ignored + } + } + } } - } -} +}; + int main() { sf::RenderWindow window(sf::VideoMode({SCREEN_WIDTH, SCREEN_HEIGHT}), "Zed's Ray Caster Game Thing"); @@ -131,8 +219,8 @@ int main() { window.setVerticalSyncEnabled(VSYNC); window.setFramerateLimit(FRAME_LIMIT); - MoveState state = IDLE; - CameraLOL camera; + MainFSM fsm(window, rayview); + fsm.event(MainEvent::STARTED); while(window.isOpen()) { auto start = std::chrono::high_resolution_clock::now(); @@ -146,22 +234,18 @@ int main() { draw_weapon(window, *weapon_sprite_ptr, rotation); window.display(); - if(state == IDLE) { - handle_window_events(window, rayview, state, camera); - } else if(state == MOVE) { - if(camera.play_run(rayview)) { - state = IDLE; - } - } else if(state == ROTATE) { - if(camera.play_rotate(rayview)) { - state = IDLE; - } - } else if(state == STRAFE) { - if(camera.play_strafe(rayview)) { - state = IDLE; + if(fsm.in_state(MainState::IDLE)) { + fsm.keyboard(); + } else{ + fsm.event(MainEvent::TICK); + } + + if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::P)) { + if(rayview.$active_shader == nullptr) { + rayview.$active_shader = &rayview.$paused; + } else { + rayview.$active_shader = nullptr; } - } else { - dbc::sentinel("invalid move state."); } if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::R)) { diff --git a/meson.build b/meson.build index 9800ea5..cbf6132 100644 --- a/meson.build +++ b/meson.build @@ -77,6 +77,7 @@ executable('runtests', sources + [ 'tests/pathing.cpp', 'tests/spatialmap.cpp', 'tests/tilemap.cpp', + 'tests/fsm.cpp', 'tests/worldbuilder.cpp', ], override_options: exe_defaults, dependencies: dependencies + [catch2]) diff --git a/tests/fsm.cpp b/tests/fsm.cpp new file mode 100644 index 0000000..4c2f0d6 --- /dev/null +++ b/tests/fsm.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include "../fsm.hpp" + +using namespace fmt; +using std::string; + +enum class MyState { + START, RUNNING, END +}; + +enum class MyEvent { + STARTED, PUSH, QUIT +}; + +class MyFSM : public DeadSimpleFSM { +public: + void event(MyEvent ev, string data="") { + switch($state) { + FSM_STATE(MyState, START, ev); + FSM_STATE(MyState, RUNNING, ev, data); + FSM_STATE(MyState, END, ev); + } + } + + void START(MyEvent ev) { + println("<<< START {}", (int)ev); + state(MyState::RUNNING); + } + + void RUNNING(MyEvent ev, string &data) { + if(ev == MyEvent::QUIT) { + println("<<< QUITTING {}", data); + state(MyState::END); + } else { + println("<<< RUN: {}", data); + state(MyState::RUNNING); + } + } + + void END(MyEvent ev) { + println("<<< STOP {}", (int)ev); + state(MyState::END); + } +}; + +TEST_CASE("confirm fsm works with optional data", "[utils]") { + MyFSM fsm; + + REQUIRE(fsm.in_state(MyState::START)); + + fsm.event(MyEvent::STARTED); + REQUIRE(fsm.in_state(MyState::RUNNING)); + + fsm.event(MyEvent::PUSH); + REQUIRE(fsm.in_state(MyState::RUNNING)); + + fsm.event(MyEvent::PUSH); + REQUIRE(fsm.in_state(MyState::RUNNING)); + + fsm.event(MyEvent::PUSH); + REQUIRE(fsm.in_state(MyState::RUNNING)); + + fsm.event(MyEvent::QUIT, "DONE!"); + REQUIRE(fsm.in_state(MyState::END)); +}