Not so well working game mode

This commit is contained in:
2026-04-20 20:21:27 +03:00
parent 4313d190f9
commit 6d7fcb1157
23 changed files with 1451 additions and 17 deletions

View File

@@ -25,6 +25,8 @@ set(EDITSCENE_SOURCES
systems/CellGridSystem.cpp
systems/RoomLayoutSystem.cpp
systems/FurnitureLibrary.cpp
systems/StartupMenuSystem.cpp
systems/PlayerControllerSystem.cpp
systems/CharacterSlotSystem.cpp
systems/AnimationTreeSystem.cpp
systems/CharacterSystem.cpp
@@ -55,6 +57,8 @@ set(EDITSCENE_SOURCES
ui/ClearAreaEditor.cpp
ui/FurnitureTemplateEditor.cpp
ui/StartupMenuEditor.cpp
ui/PlayerControllerEditor.cpp
ui/ComponentRegistration.cpp
components/LightModule.cpp
components/CameraModule.cpp
@@ -71,6 +75,8 @@ set(EDITSCENE_SOURCES
components/CellGridModule.cpp
components/CellGridEditorsModule.cpp
components/CellGrid.cpp
components/StartupMenuModule.cpp
components/PlayerControllerModule.cpp
camera/EditorCamera.cpp
gizmo/Gizmo.cpp
physics/physics.cpp
@@ -98,6 +104,10 @@ set(EDITSCENE_HEADERS
components/AnimationTree.hpp
components/Character.hpp
components/CellGrid.hpp
components/StartupMenu.hpp
components/PlayerController.hpp
systems/StartupMenuSystem.hpp
systems/PlayerControllerSystem.hpp
systems/EditorUISystem.hpp
systems/CellGridSystem.hpp
systems/RoomLayoutSystem.hpp
@@ -144,6 +154,8 @@ set(EDITSCENE_HEADERS
ui/ClearAreaEditor.hpp
ui/FurnitureTemplateEditor.hpp
ui/StartupMenuEditor.hpp
ui/PlayerControllerEditor.hpp
camera/EditorCamera.hpp
gizmo/Gizmo.hpp
physics/physics.h

View File

@@ -14,6 +14,9 @@
#include "systems/CharacterSystem.hpp"
#include "systems/CellGridSystem.hpp"
#include "systems/RoomLayoutSystem.hpp"
#include "systems/StartupMenuSystem.hpp"
#include "systems/PlayerControllerSystem.hpp"
#include "systems/SceneSerializer.hpp"
#include "camera/EditorCamera.hpp"
#include "components/EntityName.hpp"
#include "components/Transform.hpp"
@@ -35,6 +38,8 @@
#include "components/CharacterSlots.hpp"
#include "components/AnimationTree.hpp"
#include "components/Character.hpp"
#include "components/StartupMenu.hpp"
#include "components/PlayerController.hpp"
#include "components/CellGrid.hpp"
#include "components/CellGridModule.hpp"
#include <OgreRTShaderSystem.h>
@@ -46,10 +51,12 @@
ImGuiRenderListener::ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay,
EditorUISystem *uiSystem,
Ogre::RenderWindow *renderWindow)
Ogre::RenderWindow *renderWindow,
EditorApp *editorApp)
: m_imguiOverlay(imguiOverlay)
, m_uiSystem(uiSystem)
, m_renderWindow(renderWindow)
, m_editorApp(editorApp)
{
m_lastTime = m_timer.getMilliseconds();
}
@@ -71,6 +78,14 @@ void ImGuiRenderListener::preViewportUpdate(
if (m_uiSystem) {
m_uiSystem->update(m_deltaTime);
}
// Render startup menu in game mode (inside ImGui frame scope)
if (m_editorApp && m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
m_editorApp->getGamePlayState() == EditorApp::GamePlayState::Menu) {
StartupMenuSystem *sms = m_editorApp->getStartupMenuSystem();
if (sms)
sms->update(m_deltaTime);
}
}
void ImGuiRenderListener::postViewportUpdate(
@@ -100,6 +115,8 @@ EditorApp::EditorApp()
, m_overlaySystem(nullptr)
, m_imguiOverlay(nullptr)
, m_currentModifiers(0)
, m_gameMode(GameMode::Editor)
, m_gamePlayState(GamePlayState::Menu)
{
}
@@ -125,6 +142,8 @@ EditorApp::~EditorApp()
}
// Release all systems
m_playerControllerSystem.reset();
m_startupMenuSystem.reset();
m_characterSlotSystem.reset();
m_animationTreeSystem.reset();
m_characterSystem.reset();
@@ -172,7 +191,6 @@ void EditorApp::setup()
m_imguiOverlay = initialiseImGui();
if (m_imguiOverlay) {
m_imguiOverlay->setZOrder(300);
m_imguiOverlay->show();
ImGui::StyleColorsDark();
}
@@ -218,13 +236,13 @@ void EditorApp::setup()
// Setup ProceduralTexture system
m_proceduralTextureSystem =
std::make_unique<ProceduralTextureSystem>(m_world,
m_sceneMgr);
m_sceneMgr);
m_proceduralTextureSystem->initialize();
// Setup ProceduralMaterial system
m_proceduralMaterialSystem =
std::make_unique<ProceduralMaterialSystem>(m_world,
m_sceneMgr);
m_sceneMgr);
m_proceduralMaterialSystem->initialize();
// Setup ProceduralMesh system
@@ -257,6 +275,34 @@ void EditorApp::setup()
std::make_unique<RoomLayoutSystem>(m_world, m_sceneMgr);
m_roomLayoutSystem->initialize();
// Setup game systems
m_startupMenuSystem =
std::make_unique<StartupMenuSystem>(m_world, m_sceneMgr,
this);
m_playerControllerSystem =
std::make_unique<PlayerControllerSystem>(
m_world, m_sceneMgr, this);
if (m_gameMode == GameMode::Game) {
// Load startup menu scene configured in editor
SceneSerializer serializer(m_world, m_sceneMgr);
if (!serializer.loadFromFile("startup_menu.json",
m_uiSystem.get())) {
Ogre::LogManager::getSingleton().logMessage(
"Game mode: Failed to load startup_menu.json: " +
serializer.getLastError());
}
}
// Pre-load menu font before showing overlay
// (OGRE builds the atlas in createFontTexture() during show())
if (m_startupMenuSystem)
m_startupMenuSystem->prepareFont();
// Now show the overlay — font atlas will be built with our font
if (m_imguiOverlay)
m_imguiOverlay->show();
// Add default entities to UI cache
for (auto &e : m_defaultEntities) {
m_uiSystem->addEntity(e);
@@ -264,13 +310,16 @@ void EditorApp::setup()
// Create and register ImGui render listener
m_imguiListener = std::make_unique<ImGuiRenderListener>(
m_imguiOverlay, m_uiSystem.get(), getRenderWindow());
m_imguiOverlay, m_uiSystem.get(), getRenderWindow(), this);
getRenderWindow()->addListener(m_imguiListener.get());
// Register input listeners
addInputListener(this);
addInputListener(getImGuiInputListener());
// Game mode can be set externally before setup() is called
m_setupComplete = true;
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"Setup failed: " + Ogre::String(e.what()));
@@ -278,6 +327,71 @@ void EditorApp::setup()
}
}
void EditorApp::setGameMode(GameMode mode)
{
if (m_setupComplete) {
Ogre::LogManager::getSingleton().logMessage(
"setGameMode ignored: cannot change mode after setup");
return;
}
m_gameMode = mode;
if (m_gameMode == GameMode::Game) {
m_gamePlayState = GamePlayState::Menu;
if (m_uiSystem)
m_uiSystem->setEditorUIEnabled(false);
} else {
m_gamePlayState = GamePlayState::Menu;
if (m_uiSystem)
m_uiSystem->setEditorUIEnabled(true);
}
}
void EditorApp::setGamePlayState(GamePlayState state)
{
m_gamePlayState = state;
// Grab/ungrab mouse based on gameplay state
if (m_gameMode == GameMode::Game) {
if (state == GamePlayState::Playing) {
setWindowGrab(true);
} else if (state == GamePlayState::Menu) {
setWindowGrab(false);
}
}
}
void EditorApp::clearScene()
{
// Destroy all entities with EditorMarkerComponent
std::vector<flecs::entity> entitiesToDelete;
m_world.query<EditorMarkerComponent>().each(
[&](flecs::entity e, EditorMarkerComponent) {
entitiesToDelete.push_back(e);
});
for (auto &e : entitiesToDelete) {
if (e.is_alive()) {
e.destruct();
}
}
if (m_uiSystem) {
m_uiSystem->clearEntityCache();
}
}
void EditorApp::startNewGame(const Ogre::String &scenePath)
{
clearScene();
SceneSerializer serializer(m_world, m_sceneMgr);
if (serializer.loadFromFile(scenePath, m_uiSystem.get())) {
m_gamePlayState = GamePlayState::Playing;
Ogre::LogManager::getSingleton().logMessage(
"Game started: loaded scene " + scenePath);
} else {
Ogre::LogManager::getSingleton().logMessage(
"Failed to load scene: " + serializer.getLastError());
}
}
void EditorApp::setupECS()
{
// Register components
@@ -322,6 +436,10 @@ void EditorApp::setupECS()
// Register Character component
m_world.component<CharacterComponent>();
// Register game components
m_world.component<StartupMenuComponent>();
m_world.component<PlayerControllerComponent>();
// Register CellGrid/Town components
CellGridModule::registerComponents(m_world);
}
@@ -442,9 +560,18 @@ void EditorApp::createAxes()
bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
{
// Update camera
if (m_camera) {
m_camera->update(evt.timeSinceLastFrame);
if (m_gameMode == GameMode::Editor) {
// Update camera
if (m_camera) {
m_camera->update(evt.timeSinceLastFrame);
}
} else if (m_gameMode == GameMode::Game) {
if (m_gamePlayState == GamePlayState::Playing) {
if (m_playerControllerSystem) {
m_playerControllerSystem->update(
evt.timeSinceLastFrame);
}
}
}
/* --- Visual mesh setup (must run before animation) --- */
@@ -498,12 +625,22 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
m_proceduralMaterialSystem->update();
}
// Reset per-frame input state
m_gameInput.resetPerFrame();
// Don't call base class - it crashes when iterating input listeners
return true;
}
bool EditorApp::mouseMoved(const OgreBites::MouseMotionEvent &evt)
{
if (m_gameMode == GameMode::Game) {
m_gameInput.mouseMoved = true;
m_gameInput.mouseDeltaX += evt.xrel;
m_gameInput.mouseDeltaY += evt.yrel;
return true;
}
// Skip if ImGui wants to capture mouse (for gizmo, we still want to process even if over UI)
// But we need to update hover state
if (m_camera && m_uiSystem) {
@@ -526,6 +663,10 @@ bool EditorApp::mouseMoved(const OgreBites::MouseMotionEvent &evt)
bool EditorApp::mousePressed(const OgreBites::MouseButtonEvent &evt)
{
if (m_gameMode == GameMode::Game) {
return true;
}
// Get mouse ray for gizmo interaction FIRST (before ImGui check)
// This allows clicking on 3D gizmo even when mouse is over empty UI areas
if (m_camera && m_uiSystem) {
@@ -552,6 +693,10 @@ bool EditorApp::mousePressed(const OgreBites::MouseButtonEvent &evt)
bool EditorApp::mouseReleased(const OgreBites::MouseButtonEvent &evt)
{
if (m_gameMode == GameMode::Game) {
return true;
}
// Handle gizmo mouse release (always process to end dragging)
if (m_uiSystem) {
if (m_uiSystem->onMouseReleased()) {
@@ -571,6 +716,42 @@ bool EditorApp::keyPressed(const OgreBites::KeyboardEvent &evt)
{
m_currentModifiers = evt.keysym.mod;
if (m_gameMode == GameMode::Game) {
bool pressed = true;
switch (evt.keysym.sym) {
case 'w':
case 'W':
m_gameInput.w = pressed;
break;
case 's':
case 'S':
m_gameInput.s = pressed;
break;
case 'a':
case 'A':
m_gameInput.a = pressed;
break;
case 'd':
case 'D':
m_gameInput.d = pressed;
break;
case OgreBites::SDLK_LSHIFT:
m_gameInput.shift = pressed;
break;
case 'e':
case 'E':
m_gameInput.e = pressed;
m_gameInput.ePressed = true;
break;
case 'f':
case 'F':
m_gameInput.f = pressed;
m_gameInput.fPressed = true;
break;
}
return true;
}
// Forward to camera for FPS movement
if (m_camera) {
m_camera->handleKeyboard(evt);
@@ -601,6 +782,40 @@ bool EditorApp::keyReleased(const OgreBites::KeyboardEvent &evt)
{
m_currentModifiers = evt.keysym.mod;
if (m_gameMode == GameMode::Game) {
bool pressed = false;
switch (evt.keysym.sym) {
case 'w':
case 'W':
m_gameInput.w = pressed;
break;
case 's':
case 'S':
m_gameInput.s = pressed;
break;
case 'a':
case 'A':
m_gameInput.a = pressed;
break;
case 'd':
case 'D':
m_gameInput.d = pressed;
break;
case OgreBites::SDLK_LSHIFT:
m_gameInput.shift = pressed;
break;
case 'e':
case 'E':
m_gameInput.e = pressed;
break;
case 'f':
case 'F':
m_gameInput.f = pressed;
break;
}
return true;
}
// Forward to camera for FPS movement
if (m_camera) {
m_camera->handleKeyboard(evt);
@@ -632,4 +847,4 @@ void EditorApp::locateResources()
Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
"./characters/female", "FileSystem", "Characters", false, true);
OgreBites::ApplicationContext::locateResources();
}
}

View File

@@ -26,6 +26,36 @@ class AnimationTreeSystem;
class CharacterSystem;
class CellGridSystem;
class RoomLayoutSystem;
class StartupMenuSystem;
class PlayerControllerSystem;
class EditorApp;
/**
* Shared input state for game mode
*/
struct GameInputState {
bool w = false;
bool a = false;
bool s = false;
bool d = false;
bool shift = false;
bool e = false;
bool f = false;
bool ePressed = false;
bool fPressed = false;
float mouseDeltaX = 0.0f;
float mouseDeltaY = 0.0f;
bool mouseMoved = false;
void resetPerFrame()
{
mouseMoved = false;
mouseDeltaX = 0.0f;
mouseDeltaY = 0.0f;
ePressed = false;
fPressed = false;
}
};
/**
* RenderTargetListener for ImGui frame management
@@ -35,7 +65,8 @@ class ImGuiRenderListener : public Ogre::RenderTargetListener {
public:
ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay,
EditorUISystem *uiSystem,
Ogre::RenderWindow *renderWindow);
Ogre::RenderWindow *renderWindow,
EditorApp *editorApp);
void
preViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override;
@@ -46,6 +77,7 @@ private:
Ogre::ImGuiOverlay *m_imguiOverlay;
EditorUISystem *m_uiSystem;
Ogre::RenderWindow *m_renderWindow;
EditorApp *m_editorApp;
// Timer for delta time calculation
Ogre::Timer m_timer;
@@ -57,11 +89,14 @@ private:
};
/**
* Main application class for the scene editor
* Main application class for the scene editor / game
*/
class EditorApp : public OgreBites::ApplicationContext,
public OgreBites::InputListener {
public:
enum class GameMode { Editor, Game };
enum class GamePlayState { Menu, Playing, Paused };
EditorApp();
virtual ~EditorApp();
@@ -86,6 +121,17 @@ public:
void setupECS();
void createDefaultEntities();
// Game mode management
void setGameMode(GameMode mode);
GameMode getGameMode() const { return m_gameMode; }
GamePlayState getGamePlayState() const { return m_gamePlayState; }
void setGamePlayState(GamePlayState state);
void startNewGame(const Ogre::String &scenePath);
void clearScene();
// Input access
GameInputState &getGameInputState() { return m_gameInput; }
// Getters
flecs::entity getSelectedEntity() const;
Ogre::SceneManager *getSceneManager() const
@@ -96,6 +142,20 @@ public:
{
return &m_world;
}
EditorCamera *getEditorCamera() const { return m_camera.get(); }
AnimationTreeSystem *getAnimationTreeSystem() const
{
return m_animationTreeSystem.get();
}
CharacterSlotSystem *getCharacterSlotSystem() const
{
return m_characterSlotSystem.get();
}
StartupMenuSystem *getStartupMenuSystem() const
{
return m_startupMenuSystem.get();
}
Ogre::ImGuiOverlay *getImGuiOverlay() const { return m_imguiOverlay; }
private:
// Ogre objects
@@ -125,8 +185,16 @@ private:
std::unique_ptr<CellGridSystem> m_cellGridSystem;
std::unique_ptr<RoomLayoutSystem> m_roomLayoutSystem;
// Game systems
std::unique_ptr<StartupMenuSystem> m_startupMenuSystem;
std::unique_ptr<PlayerControllerSystem> m_playerControllerSystem;
// State
uint16_t m_currentModifiers;
GameMode m_gameMode = GameMode::Editor;
GamePlayState m_gamePlayState = GamePlayState::Menu;
GameInputState m_gameInput;
bool m_setupComplete = false;
};
#endif // EDITSCENE_EDITORAPP_HPP

View File

@@ -0,0 +1,26 @@
#ifndef EDITSCENE_PLAYERCONTROLLER_HPP
#define EDITSCENE_PLAYERCONTROLLER_HPP
#pragma once
#include <Ogre.h>
/**
* Player controller component.
* Only active in game mode. Editable in editor mode.
*/
struct PlayerControllerComponent {
enum CameraMode { TPS = 0, FPS = 1 };
int cameraMode = TPS;
Ogre::String targetCharacterName = "";
Ogre::String fpsBoneName = "Head";
float tpsDistance = 3.0f;
float tpsHeight = 2.0f;
float mouseSensitivity = 0.2f;
/* Animation state machine configuration */
Ogre::String locomotionStateMachine = "locomotion";
Ogre::String idleState = "idle";
Ogre::String walkState = "walking";
Ogre::String runState = "running";
};
#endif // EDITSCENE_PLAYERCONTROLLER_HPP

View File

@@ -0,0 +1,24 @@
#include "../ui/ComponentRegistration.hpp"
#include "../ui/PlayerControllerEditor.hpp"
#include "PlayerController.hpp"
REGISTER_COMPONENT_GROUP("Player Controller", "Game",
PlayerControllerComponent, PlayerControllerEditor)
{
registry.registerComponent<PlayerControllerComponent>(
PlayerControllerComponent_name,
PlayerControllerComponent_group,
std::make_unique<PlayerControllerEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<PlayerControllerComponent>()) {
e.set<PlayerControllerComponent>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<PlayerControllerComponent>()) {
e.remove<PlayerControllerComponent>();
}
});
}

View File

@@ -0,0 +1,20 @@
#ifndef EDITSCENE_STARTUPMENU_HPP
#define EDITSCENE_STARTUPMENU_HPP
#pragma once
#include <Ogre.h>
/**
* Configurable startup menu component.
* Only active in game mode. Editable in editor mode.
*/
struct StartupMenuComponent {
Ogre::String fontName = "Kenney Bold.ttf";
float fontSize = 36.0f;
Ogre::String newGameScene = "scene.json";
bool showLoadGame = true;
bool showOptions = true;
bool showQuit = true;
};
#endif // EDITSCENE_STARTUPMENU_HPP

View File

@@ -0,0 +1,23 @@
#include "../ui/ComponentRegistration.hpp"
#include "../ui/StartupMenuEditor.hpp"
#include "StartupMenu.hpp"
REGISTER_COMPONENT_GROUP("Startup Menu", "Game", StartupMenuComponent,
StartupMenuEditor)
{
registry.registerComponent<StartupMenuComponent>(
StartupMenuComponent_name, StartupMenuComponent_group,
std::make_unique<StartupMenuEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<StartupMenuComponent>()) {
e.set<StartupMenuComponent>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<StartupMenuComponent>()) {
e.remove<StartupMenuComponent>();
}
});
}

View File

@@ -6,17 +6,34 @@ int main(int argc, char *argv[])
{
try {
EditorApp app;
// Parse command line arguments
bool gameMode = false;
Ogre::String sceneFile;
for (int i = 1; i < argc; i++) {
Ogre::String arg = argv[i];
if (arg == "--game") {
gameMode = true;
} else if (arg.length() > 0 && arg[0] != '-') {
sceneFile = arg;
}
}
if (gameMode) {
app.setGameMode(EditorApp::GameMode::Game);
}
app.initApp();
// Auto-load scene if provided as argument
if (argc > 1) {
std::cout << "Auto-loading scene: " << argv[1] << std::endl;
// Auto-load scene if provided as argument (editor mode only)
if (!sceneFile.empty() && app.getGameMode() == EditorApp::GameMode::Editor) {
std::cout << "Auto-loading scene: " << sceneFile << std::endl;
SceneSerializer serializer(*app.getWorld(), app.getSceneManager());
if (!serializer.loadFromFile(argv[1])) {
if (!serializer.loadFromFile(sceneFile, nullptr)) {
std::cerr << "Failed to load scene: " << serializer.getLastError() << std::endl;
}
}
app.getRoot()->startRendering();
app.closeApp();
} catch (const std::exception &e) {

View File

@@ -10,6 +10,7 @@ FileSystem=resources/buildings
FileSystem=resources/buildings/parts/pier
FileSystem=resources/buildings/parts/furniture
FileSystem=resources/vehicles
FileSystem=resources/fonts
[Popular]
FileSystem=resources/materials/programs

View File

@@ -281,3 +281,25 @@ void CharacterSlotSystem::destroyCharacterParts(flecs::entity e)
it->second.parts.clear();
m_entities.erase(it);
}
Ogre::Entity *CharacterSlotSystem::getSlotEntity(flecs::entity e,
const Ogre::String &slot)
{
auto it = m_entities.find(e.id());
if (it == m_entities.end())
return nullptr;
auto pit = it->second.parts.find(slot);
if (pit == it->second.parts.end())
return nullptr;
return pit->second;
}
void CharacterSlotSystem::setSlotVisible(flecs::entity e,
const Ogre::String &slot,
bool visible)
{
Ogre::Entity *ent = getSlotEntity(e, slot);
if (ent) {
ent->setVisible(visible);
}
}

View File

@@ -35,6 +35,12 @@ public:
const Ogre::String &sex,
const Ogre::String &slot);
/* Slot visibility helpers */
Ogre::Entity *getSlotEntity(flecs::entity e,
const Ogre::String &slot);
void setSlotVisible(flecs::entity e, const Ogre::String &slot,
bool visible);
private:
static bool s_catalogLoaded;
static nlohmann::json s_bodyParts;

View File

@@ -19,6 +19,8 @@
#include "../components/CharacterSlots.hpp"
#include "../components/Character.hpp"
#include "../components/AnimationTree.hpp"
#include "../components/StartupMenu.hpp"
#include "../components/PlayerController.hpp"
#include "../components/CellGrid.hpp"
#include "../ui/TransformEditor.hpp"
#include "../ui/RenderableEditor.hpp"
@@ -182,6 +184,12 @@ void EditorUISystem::registerModularComponents()
void EditorUISystem::update(float deltaTime)
{
if (!m_editorUIEnabled) {
// Only render FPS overlay when editor UI is disabled
renderFPSOverlay(deltaTime);
return;
}
// Render UI windows
// Note: NewFrame() is called by ImGuiRenderListener::preViewportUpdate
renderHierarchyWindow();
@@ -651,6 +659,20 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
componentCount++;
}
// Render StartupMenu if present
if (entity.has<StartupMenuComponent>()) {
auto &sm = entity.get_mut<StartupMenuComponent>();
m_componentRegistry.render<StartupMenuComponent>(entity, sm);
componentCount++;
}
// Render PlayerController if present
if (entity.has<PlayerControllerComponent>()) {
auto &pc = entity.get_mut<PlayerControllerComponent>();
m_componentRegistry.render<PlayerControllerComponent>(entity, pc);
componentCount++;
}
// Render CellGrid if present
if (entity.has<CellGridComponent>()) {
auto &grid = entity.get_mut<CellGridComponent>();

View File

@@ -52,6 +52,14 @@ public:
*/
void addEntity(flecs::entity entity) { m_allEntities.push_back(entity); }
/**
* Clear entity cache and deselect
*/
void clearEntityCache() {
m_allEntities.clear();
setSelectedEntity(flecs::entity::null());
}
/**
* Get the currently selected entity
*/
@@ -88,6 +96,11 @@ public:
* Set physics system for debug toggle
*/
void setPhysicsSystem(EditorPhysicsSystem* physics) { m_physicsSystem = physics; }
/**
* Enable/disable editor UI rendering
*/
void setEditorUIEnabled(bool enabled) { m_editorUIEnabled = enabled; }
/**
* Set last frame batch count (called from render listener)
@@ -167,6 +180,9 @@ private:
// Physics system reference (for debug toggle)
EditorPhysicsSystem* m_physicsSystem = nullptr;
// Editor UI enabled flag
bool m_editorUIEnabled = true;
// Render window reference (for viewport access)
Ogre::RenderWindow* m_renderWindow = nullptr;

View File

@@ -0,0 +1,382 @@
#include "PlayerControllerSystem.hpp"
#include "../EditorApp.hpp"
#include "../components/PlayerController.hpp"
#include "../components/Character.hpp"
#include "../components/Transform.hpp"
#include "../components/EntityName.hpp"
#include "AnimationTreeSystem.hpp"
#include "CharacterSlotSystem.hpp"
#include "../camera/EditorCamera.hpp"
#include <OgreEntity.h>
#include <OgreCamera.h>
#include <OgreLogManager.h>
#include <OgreSceneNode.h>
#include <OgreSkeletonInstance.h>
#include <cmath>
PlayerControllerSystem::PlayerControllerSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr,
EditorApp *editorApp)
: m_world(world)
, m_sceneMgr(sceneMgr)
, m_editorApp(editorApp)
{
}
PlayerControllerSystem::~PlayerControllerSystem()
{
for (auto &pair : m_states) {
shutdownController(pair.second);
}
m_states.clear();
}
void PlayerControllerSystem::shutdownController(ControllerState &state)
{
if (state.pivotNode) {
m_sceneMgr->destroySceneNode(state.pivotNode);
state.pivotNode = nullptr;
}
if (state.goalNode) {
m_sceneMgr->destroySceneNode(state.goalNode);
state.goalNode = nullptr;
}
state.initialized = false;
}
flecs::entity PlayerControllerSystem::findTargetEntity(const Ogre::String &name)
{
if (name.empty())
return flecs::entity::null();
flecs::entity result = flecs::entity::null();
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e, EntityNameComponent &en) {
if (result.is_alive())
return;
if (en.name == name)
result = e;
});
return result;
}
void PlayerControllerSystem::initController(flecs::entity controllerEntity,
PlayerControllerComponent &pc,
ControllerState &state)
{
(void)controllerEntity;
state.targetEntity = findTargetEntity(pc.targetCharacterName);
if (!state.targetEntity.is_alive())
return;
if (!state.pivotNode) {
state.pivotNode =
m_sceneMgr->getRootSceneNode()->createChildSceneNode();
}
if (!state.goalNode) {
state.goalNode =
m_sceneMgr->getRootSceneNode()->createChildSceneNode();
}
state.initialized = true;
state.faceHidden = false;
}
void PlayerControllerSystem::update(float deltaTime)
{
if (!m_editorApp ||
m_editorApp->getGameMode() != EditorApp::GameMode::Game ||
m_editorApp->getGamePlayState() != EditorApp::GamePlayState::Playing)
return;
m_world.query<PlayerControllerComponent>().each(
[&](flecs::entity e, PlayerControllerComponent &pc) {
auto it = m_states.find(e.id());
if (it == m_states.end()) {
ControllerState newState;
initController(e, pc, newState);
it = m_states.insert({ e.id(), newState }).first;
}
ControllerState &state = it->second;
// Re-resolve target if name changed or entity invalid
if (!state.targetEntity.is_alive()) {
initController(e, pc, state);
}
if (!state.targetEntity.is_alive())
return;
if (!state.targetEntity.has<TransformComponent>())
return;
auto &transform =
state.targetEntity.get<TransformComponent>();
if (!transform.node)
return;
Ogre::Vector3 charPos =
transform.node->_getDerivedPosition();
// Update camera
if (pc.cameraMode == PlayerControllerComponent::TPS) {
updateTPSCamera(pc, state, charPos, deltaTime);
} else {
updateFPSCamera(pc, state, charPos);
}
// Update locomotion
updateLocomotion(pc, state, deltaTime);
});
}
void PlayerControllerSystem::updateTPSCamera(PlayerControllerComponent &pc,
ControllerState &state,
const Ogre::Vector3 &charPos,
float deltaTime)
{
if (!state.pivotNode || !state.goalNode)
return;
EditorCamera *editorCam = m_editorApp->getEditorCamera();
if (!editorCam)
return;
Ogre::Camera *cam = editorCam->getCamera();
if (!cam)
return;
Ogre::SceneNode *camNode = cam->getParentSceneNode();
if (!camNode)
return;
// Restore face visibility when switching from FPS
if (state.faceHidden) {
CharacterSlotSystem *css =
m_editorApp->getCharacterSlotSystem();
if (css) {
css->setSlotVisible(state.targetEntity, "face",
true);
}
state.faceHidden = false;
}
// Read mouse input
GameInputState &input = m_editorApp->getGameInputState();
if (input.mouseMoved) {
state.yaw -= input.mouseDeltaX * pc.mouseSensitivity;
state.pitch -= input.mouseDeltaY * pc.mouseSensitivity;
// Clamp pitch
if (state.pitch > 25.0f)
state.pitch = 25.0f;
if (state.pitch < -60.0f)
state.pitch = -60.0f;
}
// Position pivot at character shoulder height
Ogre::Vector3 pivotPos = charPos + Ogre::Vector3(0, pc.tpsHeight, 0);
state.pivotNode->setPosition(pivotPos);
// Compute goal position based on yaw/pitch/distance
Ogre::Quaternion yawRot(Ogre::Degree(state.yaw),
Ogre::Vector3::UNIT_Y);
Ogre::Quaternion pitchRot(Ogre::Degree(state.pitch),
Ogre::Vector3::UNIT_X);
Ogre::Vector3 offset = yawRot * pitchRot *
Ogre::Vector3(0, 0, pc.tpsDistance);
Ogre::Vector3 goalPos = pivotPos + offset;
state.goalNode->setPosition(goalPos);
// Smoothly interpolate camera to goal
Ogre::Vector3 currentPos = camNode->getPosition();
Ogre::Vector3 newPos = Ogre::Math::lerp(currentPos, goalPos,
deltaTime * 9.0f);
camNode->setPosition(newPos);
camNode->lookAt(pivotPos, Ogre::Node::TS_WORLD);
}
void PlayerControllerSystem::updateFPSCamera(PlayerControllerComponent &pc,
ControllerState &state,
const Ogre::Vector3 &charPos)
{
(void)charPos;
EditorCamera *editorCam = m_editorApp->getEditorCamera();
if (!editorCam)
return;
Ogre::Camera *cam = editorCam->getCamera();
if (!cam)
return;
Ogre::SceneNode *camNode = cam->getParentSceneNode();
if (!camNode)
return;
// Find animated entity for bone access
AnimationTreeSystem *ats =
m_editorApp->getAnimationTreeSystem();
if (!ats)
return;
Ogre::Entity *ent = ats->findAnimatedEntity(state.targetEntity);
if (!ent || !ent->hasSkeleton()) {
// Fallback to TPS if no skeleton
pc.cameraMode = PlayerControllerComponent::TPS;
return;
}
Ogre::SkeletonInstance *skel = ent->getSkeleton();
Ogre::Bone *bone = nullptr;
try {
bone = skel->getBone(pc.fpsBoneName);
} catch (...) {
// Try common alternatives
const char *alternatives[] = { "Head", "head", "Neck", "neck",
"Camera", "camera" };
for (const char *name : alternatives) {
try {
bone = skel->getBone(name);
break;
} catch (...) {
}
}
}
if (!bone) {
Ogre::LogManager::getSingleton().logMessage(
"PlayerControllerSystem: Bone " + pc.fpsBoneName +
" not found, falling back to TPS");
pc.cameraMode = PlayerControllerComponent::TPS;
return;
}
// Hide face slot
if (!state.faceHidden) {
CharacterSlotSystem *css =
m_editorApp->getCharacterSlotSystem();
if (css) {
css->setSlotVisible(state.targetEntity, "face",
false);
}
state.faceHidden = true;
}
// Get character scene node
auto &transform =
state.targetEntity.get<TransformComponent>();
Ogre::SceneNode *charNode = transform.node;
// Compute bone world transform
Ogre::Vector3 boneWorldPos =
charNode->_getDerivedOrientation() *
bone->_getDerivedPosition() +
charNode->_getDerivedPosition();
Ogre::Quaternion boneWorldRot =
charNode->_getDerivedOrientation() *
bone->_getDerivedOrientation();
// Offset slightly forward
Ogre::Vector3 offset = boneWorldRot * Ogre::Vector3(0, 0, 0.15f);
camNode->setPosition(boneWorldPos + offset);
// Apply mouse look
GameInputState &input = m_editorApp->getGameInputState();
if (input.mouseMoved) {
state.yaw -= input.mouseDeltaX * pc.mouseSensitivity;
state.pitch -= input.mouseDeltaY * pc.mouseSensitivity;
if (state.pitch > 89.0f)
state.pitch = 89.0f;
if (state.pitch < -89.0f)
state.pitch = -89.0f;
}
Ogre::Quaternion yawRot(Ogre::Degree(state.yaw),
Ogre::Vector3::UNIT_Y);
Ogre::Quaternion pitchRot(Ogre::Degree(state.pitch),
Ogre::Vector3::UNIT_X);
camNode->setOrientation(yawRot * pitchRot);
}
void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc,
ControllerState &state,
float deltaTime)
{
(void)deltaTime;
if (!state.targetEntity.has<CharacterComponent>())
return;
GameInputState &input = m_editorApp->getGameInputState();
auto &cc = state.targetEntity.get_mut<CharacterComponent>();
// Get camera yaw for relative movement
Ogre::Quaternion yawRot(Ogre::Degree(state.yaw),
Ogre::Vector3::UNIT_Y);
Ogre::Vector3 forward = yawRot * Ogre::Vector3::NEGATIVE_UNIT_Z;
Ogre::Vector3 right = yawRot * Ogre::Vector3::UNIT_X;
// Flatten to horizontal plane
forward.y = 0;
right.y = 0;
if (forward.squaredLength() > 0.0001f)
forward.normalise();
if (right.squaredLength() > 0.0001f)
right.normalise();
Ogre::Vector3 desiredVel = Ogre::Vector3::ZERO;
if (input.w)
desiredVel += forward;
if (input.s)
desiredVel -= forward;
if (input.a)
desiredVel -= right;
if (input.d)
desiredVel += right;
bool isMoving = desiredVel.squaredLength() > 0.0001f;
float speed = input.shift ? 5.0f : 2.5f;
if (isMoving) {
desiredVel.normalise();
cc.linearVelocity = desiredVel * speed;
// Rotate character to face movement direction
auto &transform =
state.targetEntity.get_mut<TransformComponent>();
if (transform.node) {
Ogre::Vector3 flatForward = desiredVel;
flatForward.y = 0;
if (flatForward.squaredLength() > 0.0001f) {
flatForward.normalise();
Ogre::Quaternion targetRot = Ogre::Vector3::NEGATIVE_UNIT_Z.getRotationTo(
flatForward);
Ogre::Quaternion currentRot = transform.node->getOrientation();
transform.node->setOrientation(
Ogre::Quaternion::Slerp(deltaTime * 10.0f,
currentRot,
targetRot,
true));
}
}
} else {
cc.linearVelocity = Ogre::Vector3::ZERO;
}
// Update animation state
AnimationTreeSystem *ats =
m_editorApp->getAnimationTreeSystem();
if (!ats)
return;
Ogre::String animState;
if (!isMoving) {
animState = pc.idleState;
} else if (input.shift) {
animState = pc.runState;
} else {
animState = pc.walkState;
}
if (!animState.empty()) {
ats->setState(state.targetEntity,
pc.locomotionStateMachine, animState);
}
}

View File

@@ -0,0 +1,61 @@
#ifndef EDITSCENE_PLAYERCONTROLLERSYSTEM_HPP
#define EDITSCENE_PLAYERCONTROLLERSYSTEM_HPP
#pragma once
#include <flecs.h>
#include <Ogre.h>
#include <memory>
#include "../components/PlayerController.hpp"
class EditorApp;
class AnimationTreeSystem;
class CharacterSlotSystem;
/**
* System that handles player input, camera control (FPS/TPS),
* character locomotion, and animation state setting in game mode.
* Only active when EditorApp is in GameMode::Game and GamePlayState::Playing.
*/
class PlayerControllerSystem {
public:
PlayerControllerSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr,
EditorApp *editorApp);
~PlayerControllerSystem();
void update(float deltaTime);
private:
struct ControllerState {
flecs::entity targetEntity = flecs::entity::null();
float yaw = 0.0f;
float pitch = 0.0f;
Ogre::SceneNode *pivotNode = nullptr;
Ogre::SceneNode *goalNode = nullptr;
bool initialized = false;
bool faceHidden = false;
};
void initController(flecs::entity controllerEntity,
PlayerControllerComponent &pc,
ControllerState &state);
void shutdownController(ControllerState &state);
flecs::entity findTargetEntity(const Ogre::String &name);
void updateTPSCamera(PlayerControllerComponent &pc,
ControllerState &state,
const Ogre::Vector3 &charPos, float deltaTime);
void updateFPSCamera(PlayerControllerComponent &pc,
ControllerState &state,
const Ogre::Vector3 &charPos);
void updateLocomotion(PlayerControllerComponent &pc,
ControllerState &state, float deltaTime);
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
EditorApp *m_editorApp;
std::unordered_map<flecs::entity_t, ControllerState> m_states;
};
#endif // EDITSCENE_PLAYERCONTROLLERSYSTEM_HPP

View File

@@ -18,6 +18,8 @@
#include "../components/Character.hpp"
#include "../components/CharacterSlots.hpp"
#include "../components/AnimationTree.hpp"
#include "../components/StartupMenu.hpp"
#include "../components/PlayerController.hpp"
#include "../components/CellGrid.hpp"
#include "../components/GeneratedPhysicsTag.hpp"
#include "EditorUISystem.hpp"
@@ -192,6 +194,14 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
if (entity.has<AnimationTreeComponent>()) {
json["animationTree"] = serializeAnimationTree(entity);
}
if (entity.has<StartupMenuComponent>()) {
json["startupMenu"] = serializeStartupMenu(entity);
}
if (entity.has<PlayerControllerComponent>()) {
json["playerController"] = serializePlayerController(entity);
}
// CellGrid/Town components
if (entity.has<CellGridComponent>()) {
@@ -321,6 +331,14 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit
deserializeAnimationTree(entity, json["animationTree"]);
}
if (json.contains("startupMenu")) {
deserializeStartupMenu(entity, json["startupMenu"]);
}
if (json.contains("playerController")) {
deserializePlayerController(entity, json["playerController"]);
}
if (json.contains("triangleBuffer")) {
deserializeTriangleBuffer(entity, json["triangleBuffer"]);
}
@@ -1960,3 +1978,67 @@ void SceneSerializer::deserializeClearArea(flecs::entity entity, const nlohmann:
clearArea.markDirty();
entity.set<ClearAreaComponent>(clearArea);
}
nlohmann::json SceneSerializer::serializeStartupMenu(flecs::entity entity)
{
auto &sm = entity.get<StartupMenuComponent>();
nlohmann::json json;
json["fontName"] = sm.fontName;
json["fontSize"] = sm.fontSize;
json["newGameScene"] = sm.newGameScene;
json["showLoadGame"] = sm.showLoadGame;
json["showOptions"] = sm.showOptions;
json["showQuit"] = sm.showQuit;
return json;
}
nlohmann::json SceneSerializer::serializePlayerController(flecs::entity entity)
{
auto &pc = entity.get<PlayerControllerComponent>();
nlohmann::json json;
json["cameraMode"] = pc.cameraMode;
json["targetCharacterName"] = pc.targetCharacterName;
json["fpsBoneName"] = pc.fpsBoneName;
json["tpsDistance"] = pc.tpsDistance;
json["tpsHeight"] = pc.tpsHeight;
json["mouseSensitivity"] = pc.mouseSensitivity;
json["locomotionStateMachine"] = pc.locomotionStateMachine;
json["idleState"] = pc.idleState;
json["walkState"] = pc.walkState;
json["runState"] = pc.runState;
return json;
}
void SceneSerializer::deserializeStartupMenu(flecs::entity entity,
const nlohmann::json &json)
{
StartupMenuComponent sm;
sm.fontName = json.value("fontName", sm.fontName);
sm.fontSize = json.value("fontSize", sm.fontSize);
sm.newGameScene = json.value("newGameScene", sm.newGameScene);
sm.showLoadGame = json.value("showLoadGame", sm.showLoadGame);
sm.showOptions = json.value("showOptions", sm.showOptions);
sm.showQuit = json.value("showQuit", sm.showQuit);
entity.set<StartupMenuComponent>(sm);
}
void SceneSerializer::deserializePlayerController(flecs::entity entity,
const nlohmann::json &json)
{
PlayerControllerComponent pc;
pc.cameraMode = json.value("cameraMode", pc.cameraMode);
pc.targetCharacterName = json.value("targetCharacterName",
pc.targetCharacterName);
pc.fpsBoneName = json.value("fpsBoneName", pc.fpsBoneName);
pc.tpsDistance = json.value("tpsDistance", pc.tpsDistance);
pc.tpsHeight = json.value("tpsHeight", pc.tpsHeight);
pc.mouseSensitivity = json.value("mouseSensitivity",
pc.mouseSensitivity);
pc.locomotionStateMachine = json.value("locomotionStateMachine",
pc.locomotionStateMachine);
pc.idleState = json.value("idleState", pc.idleState);
pc.walkState = json.value("walkState", pc.walkState);
pc.runState = json.value("runState", pc.runState);
entity.set<PlayerControllerComponent>(pc);
}

View File

@@ -57,6 +57,8 @@ private:
nlohmann::json serializeCharacter(flecs::entity entity);
nlohmann::json serializeCharacterSlots(flecs::entity entity);
nlohmann::json serializeAnimationTree(flecs::entity entity);
nlohmann::json serializeStartupMenu(flecs::entity entity);
nlohmann::json serializePlayerController(flecs::entity entity);
// CellGrid/Town component serialization
nlohmann::json serializeCellGrid(flecs::entity entity);
@@ -87,6 +89,8 @@ private:
void deserializeCharacter(flecs::entity entity, const nlohmann::json& json);
void deserializeCharacterSlots(flecs::entity entity, const nlohmann::json& json);
void deserializeAnimationTree(flecs::entity entity, const nlohmann::json& json);
void deserializeStartupMenu(flecs::entity entity, const nlohmann::json& json);
void deserializePlayerController(flecs::entity entity, const nlohmann::json& json);
// CellGrid/Town component deserialization
void deserializeCellGrid(flecs::entity entity, const nlohmann::json& json);

View File

@@ -0,0 +1,217 @@
#include "StartupMenuSystem.hpp"
#include "../EditorApp.hpp"
#include "../components/StartupMenu.hpp"
#include <imgui.h>
#include <OgreFontManager.h>
#include <OgreImGuiOverlay.h>
#include <OgreLogManager.h>
#include <OgreOverlayManager.h>
StartupMenuSystem::StartupMenuSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr,
EditorApp *editorApp)
: m_world(world)
, m_sceneMgr(sceneMgr)
, m_editorApp(editorApp)
{
}
StartupMenuSystem::~StartupMenuSystem() = default;
void StartupMenuSystem::ensureFontLoaded(const Ogre::String &fontName,
float fontSize)
{
if (m_fontLoaded && m_currentFontName == fontName &&
m_currentFontSize == fontSize)
return;
Ogre::ImGuiOverlay *overlay = m_editorApp->getImGuiOverlay();
if (!overlay)
return;
// Try to load via Ogre font manager
Ogre::FontPtr font;
try {
if (Ogre::FontManager::getSingleton().resourceExists(
"StartupMenuFont", "General")) {
Ogre::FontManager::getSingleton().remove(
"StartupMenuFont", "General");
}
font = Ogre::FontManager::getSingleton().create(
"StartupMenuFont", "General");
font->setType(Ogre::FontType::FT_TRUETYPE);
font->setSource(fontName);
font->setTrueTypeSize(fontSize);
font->setTrueTypeResolution(75);
font->addCodePointRange(Ogre::Font::CodePointRange(32, 255));
font->load();
} catch (...) {
Ogre::LogManager::getSingleton().logMessage(
"StartupMenuSystem: Failed to load font " + fontName);
m_menuFont = nullptr;
m_fontLoaded = false;
return;
}
m_menuFont = overlay->addFont("StartupMenuFont", "General");
m_currentFontName = fontName;
m_currentFontSize = fontSize;
m_fontLoaded = true;
}
void StartupMenuSystem::prepareFont()
{
// Must be called BEFORE Ogre::ImGuiOverlay::show() so that the font
// is added to the atlas before OGRE builds it in createFontTexture().
if (!m_editorApp)
return;
// Find an entity with StartupMenuComponent
flecs::entity menuEntity = flecs::entity::null();
m_world.query<StartupMenuComponent>().each(
[&](flecs::entity e, StartupMenuComponent &) {
if (!menuEntity.is_alive())
menuEntity = e;
});
if (menuEntity.is_alive()) {
auto &sm = menuEntity.get_mut<StartupMenuComponent>();
ensureFontLoaded(sm.fontName, sm.fontSize);
}
}
void StartupMenuSystem::update(float deltaTime)
{
(void)deltaTime;
if (!m_editorApp ||
m_editorApp->getGamePlayState() != EditorApp::GamePlayState::Menu)
return;
// Find an entity with StartupMenuComponent
flecs::entity menuEntity = flecs::entity::null();
m_world.query<StartupMenuComponent>().each(
[&](flecs::entity e, StartupMenuComponent &) {
if (!menuEntity.is_alive())
menuEntity = e;
});
if (!menuEntity.is_alive()) {
// No startup menu entity configured
renderMissingMenuError();
return;
}
auto &sm = menuEntity.get_mut<StartupMenuComponent>();
renderMenu(sm);
}
void StartupMenuSystem::renderMenu(StartupMenuComponent &sm)
{
ImVec2 size = ImGui::GetMainViewport()->Size;
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(size.x, size.y), ImGuiCond_Always);
ImVec4 solidColor = ImVec4(0.0f, 0.0f, 0.0f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, solidColor);
ImGui::Begin("StartupMenu", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoInputs);
if (m_menuFont)
ImGui::PushFont(m_menuFont);
struct ButtonData {
const char *label;
std::function<void()> action;
};
std::vector<ButtonData> buttons;
buttons.push_back({ "NEW GAME", [&]() {
m_editorApp->startNewGame(sm.newGameScene);
} });
if (sm.showLoadGame)
buttons.push_back({ "LOAD GAME", [&]() {
Ogre::LogManager::getSingleton().logMessage(
"Load game not implemented");
} });
if (sm.showOptions)
buttons.push_back({ "OPTIONS", [&]() {
Ogre::LogManager::getSingleton().logMessage(
"Options not implemented");
} });
if (sm.showQuit)
buttons.push_back({ "QUIT", [&]() {
Ogre::Root::getSingleton().queueEndRendering();
} });
// Calculate button dimensions
float buttonWidth = 0.0f;
float buttonsHeight = 0.0f;
float extraPixels = 20.0f;
for (const auto &b : buttons) {
ImVec2 textSize = ImGui::CalcTextSize(b.label);
float bwidth = textSize.x +
(ImGui::GetStyle().FramePadding.x * 2.0f) +
extraPixels;
float bheight = textSize.y +
(ImGui::GetStyle().FramePadding.y * 2.0f);
if (buttonWidth < bwidth)
buttonWidth = bwidth;
buttonsHeight += bheight +
ImGui::GetStyle().ItemSpacing.y;
}
if (!buttons.empty())
buttonsHeight -= ImGui::GetStyle().ItemSpacing.y;
ImGui::SetCursorPosY((size.y - buttonsHeight) * 0.5f);
for (const auto &b : buttons) {
ImGui::SetCursorPosX((size.x - buttonWidth) * 0.5f);
if (ImGui::Button(b.label, ImVec2(buttonWidth, 0)))
b.action();
}
if (m_menuFont)
ImGui::PopFont();
ImGui::End();
ImGui::PopStyleColor();
}
void StartupMenuSystem::renderMissingMenuError()
{
ImVec2 size = ImGui::GetMainViewport()->Size;
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(size.x, size.y), ImGuiCond_Always);
ImVec4 solidColor = ImVec4(0.0f, 0.0f, 0.0f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, solidColor);
ImGui::Begin("StartupMenuError", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoInputs);
const char *msg = "Error: No StartupMenuComponent entity found.\n"
"Make sure startup_menu.json is loaded and contains\n"
"an entity with a StartupMenuComponent.";
ImVec2 textSize = ImGui::CalcTextSize(msg);
ImGui::SetCursorPosX((size.x - textSize.x) * 0.5f);
ImGui::SetCursorPosY((size.y - textSize.y) * 0.5f);
ImGui::TextUnformatted(msg);
ImGui::End();
ImGui::PopStyleColor();
}

View File

@@ -0,0 +1,47 @@
#ifndef EDITSCENE_STARTUPMENUSYSTEM_HPP
#define EDITSCENE_STARTUPMENUSYSTEM_HPP
#pragma once
#include <flecs.h>
#include <Ogre.h>
#include <imgui.h>
#include <memory>
#include "../components/StartupMenu.hpp"
class EditorApp;
/**
* System that renders the full-screen startup menu in game mode.
* Only active when EditorApp is in GameMode::Game and GamePlayState::Menu.
*/
class StartupMenuSystem {
public:
StartupMenuSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
EditorApp *editorApp);
~StartupMenuSystem();
void update(float deltaTime);
/**
* Pre-load the menu font before ImGui NewFrame().
* Must be called outside an active ImGui frame (before NewFrame).
*/
void prepareFont();
private:
void renderMenu(StartupMenuComponent &sm);
void renderMissingMenuError();
void ensureFontLoaded(const Ogre::String &fontName, float fontSize);
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
EditorApp *m_editorApp;
bool m_fontLoaded = false;
Ogre::String m_currentFontName;
float m_currentFontSize = 0.0f;
ImFont *m_menuFont = nullptr;
};
#endif // EDITSCENE_STARTUPMENUSYSTEM_HPP

View File

@@ -0,0 +1,85 @@
#include "PlayerControllerEditor.hpp"
#include <imgui.h>
bool PlayerControllerEditor::renderComponent(flecs::entity entity,
PlayerControllerComponent &pc)
{
(void)entity;
bool modified = false;
if (ImGui::CollapsingHeader("Player Controller",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent();
const char *modes[] = { "Third Person", "First Person" };
int mode = pc.cameraMode;
if (ImGui::Combo("Camera Mode", &mode, modes, 2)) {
pc.cameraMode = mode;
modified = true;
}
char nameBuf[256];
snprintf(nameBuf, sizeof(nameBuf), "%s",
pc.targetCharacterName.c_str());
if (ImGui::InputText("Target Character Name", nameBuf,
sizeof(nameBuf))) {
pc.targetCharacterName = nameBuf;
modified = true;
}
char boneBuf[256];
snprintf(boneBuf, sizeof(boneBuf), "%s",
pc.fpsBoneName.c_str());
if (ImGui::InputText("FPS Bone Name", boneBuf,
sizeof(boneBuf))) {
pc.fpsBoneName = boneBuf;
modified = true;
}
if (ImGui::DragFloat("TPS Distance", &pc.tpsDistance, 0.1f,
0.5f, 20.0f))
modified = true;
if (ImGui::DragFloat("TPS Height", &pc.tpsHeight, 0.1f,
0.0f, 10.0f))
modified = true;
if (ImGui::DragFloat("Mouse Sensitivity", &pc.mouseSensitivity,
0.01f, 0.01f, 2.0f))
modified = true;
char smBuf[256];
snprintf(smBuf, sizeof(smBuf), "%s",
pc.locomotionStateMachine.c_str());
if (ImGui::InputText("Locomotion SM", smBuf, sizeof(smBuf))) {
pc.locomotionStateMachine = smBuf;
modified = true;
}
char idleBuf[256];
snprintf(idleBuf, sizeof(idleBuf), "%s",
pc.idleState.c_str());
if (ImGui::InputText("Idle State", idleBuf, sizeof(idleBuf))) {
pc.idleState = idleBuf;
modified = true;
}
char walkBuf[256];
snprintf(walkBuf, sizeof(walkBuf), "%s",
pc.walkState.c_str());
if (ImGui::InputText("Walk State", walkBuf, sizeof(walkBuf))) {
pc.walkState = walkBuf;
modified = true;
}
char runBuf[256];
snprintf(runBuf, sizeof(runBuf), "%s",
pc.runState.c_str());
if (ImGui::InputText("Run State", runBuf, sizeof(runBuf))) {
pc.runState = runBuf;
modified = true;
}
ImGui::Unindent();
}
return modified;
}

View File

@@ -0,0 +1,18 @@
#ifndef EDITSCENE_PLAYERCONTROLLEREDITOR_HPP
#define EDITSCENE_PLAYERCONTROLLEREDITOR_HPP
#pragma once
#include "ComponentEditor.hpp"
#include "../components/PlayerController.hpp"
/**
* Editor for PlayerControllerComponent
*/
class PlayerControllerEditor : public ComponentEditor<PlayerControllerComponent> {
public:
bool renderComponent(flecs::entity entity,
PlayerControllerComponent &pc) override;
const char *getName() const override { return "Player Controller"; }
};
#endif // EDITSCENE_PLAYERCONTROLLEREDITOR_HPP

View File

@@ -0,0 +1,48 @@
#include "StartupMenuEditor.hpp"
#include <imgui.h>
bool StartupMenuEditor::renderComponent(flecs::entity entity,
StartupMenuComponent &sm)
{
(void)entity;
bool modified = false;
if (ImGui::CollapsingHeader("Startup Menu",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent();
char fontNameBuf[256];
snprintf(fontNameBuf, sizeof(fontNameBuf), "%s",
sm.fontName.c_str());
if (ImGui::InputText("Font Name", fontNameBuf,
sizeof(fontNameBuf))) {
sm.fontName = fontNameBuf;
modified = true;
}
if (ImGui::DragFloat("Font Size", &sm.fontSize, 0.5f, 8.0f,
128.0f)) {
modified = true;
}
char sceneBuf[256];
snprintf(sceneBuf, sizeof(sceneBuf), "%s",
sm.newGameScene.c_str());
if (ImGui::InputText("New Game Scene", sceneBuf,
sizeof(sceneBuf))) {
sm.newGameScene = sceneBuf;
modified = true;
}
if (ImGui::Checkbox("Show Load Game", &sm.showLoadGame))
modified = true;
if (ImGui::Checkbox("Show Options", &sm.showOptions))
modified = true;
if (ImGui::Checkbox("Show Quit", &sm.showQuit))
modified = true;
ImGui::Unindent();
}
return modified;
}

View File

@@ -0,0 +1,18 @@
#ifndef EDITSCENE_STARTUPMENUEDITOR_HPP
#define EDITSCENE_STARTUPMENUEDITOR_HPP
#pragma once
#include "ComponentEditor.hpp"
#include "../components/StartupMenu.hpp"
/**
* Editor for StartupMenuComponent
*/
class StartupMenuEditor : public ComponentEditor<StartupMenuComponent> {
public:
bool renderComponent(flecs::entity entity,
StartupMenuComponent &sm) override;
const char *getName() const override { return "Startup Menu"; }
};
#endif // EDITSCENE_STARTUPMENUEDITOR_HPP