ADD: Enable AnimationSystem

This commit is contained in:
sebastian 2026-08-05 09:01:18 +02:00
parent 12c617587f
commit 97f8e71e18
11 changed files with 191 additions and 11 deletions

View File

@ -279,6 +279,7 @@ target_link_libraries(GameLib PUBLIC Engine)
# --- Executable --- # --- Executable ---
add_executable(ColorRace add_executable(ColorRace
game/src/main.cpp game/src/main.cpp
engine/src/core/animation/components/BlockingAnimationComponent.h
) )
target_link_libraries(ColorRace PRIVATE target_link_libraries(ColorRace PRIVATE

View File

@ -4,19 +4,28 @@
#include "AnimationSystem.h" #include "AnimationSystem.h"
#include <map>
#include "components/TransformAnimationComponent.h" #include "components/TransformAnimationComponent.h"
#include "ecs/standardComponents/TransformComponent.h" #include "ecs/standardComponents/TransformComponent.h"
namespace engine::animation { namespace engine::animation {
void animation::AnimationSystem::update(float deltaTime) { void animation::AnimationSystem::update(float deltaTime) {
std::vector<engine::Entity> toRemove;
for (auto& [entity, animation] : m_registry.getPool<TransformAnimationComponent>()) { for (auto& [entity, animation] : m_registry.getPool<TransformAnimationComponent>()) {
animation.time += deltaTime; animation.time += deltaTime;
auto& transform = m_registry.getComponent<TransformComponent>(entity); auto& transform = m_registry.getComponent<TransformComponent>(entity);
if (!animation.position.keys.empty()) {
transform.m_position = evaluate(animation.position, animation.time); transform.m_position = evaluate(animation.position, animation.time);
}
if (!animation.rotation.keys.empty()) {
transform.m_rotation = evaluate(animation.rotation, animation.time); transform.m_rotation = evaluate(animation.rotation, animation.time);
}
if (!animation.scale.keys.empty()) {
transform.m_scale = evaluate(animation.scale, animation.time); transform.m_scale = evaluate(animation.scale, animation.time);
}
if (animation.time >= animation.duration) { if (animation.time >= animation.duration) {
if (animation.loop) { if (animation.loop) {
@ -26,8 +35,13 @@ namespace engine::animation {
animation.onFinished(); animation.onFinished();
} }
animation.finished = true; animation.finished = true;
toRemove.push_back(entity);
} }
} }
} }
for (auto entity : toRemove) {
m_registry.removeComponent<TransformAnimationComponent>(entity);
}
} }
} // engine } // engine

View File

@ -16,11 +16,13 @@ namespace engine::animation {
EntityRegistry& m_registry; EntityRegistry& m_registry;
template<typename T> template<typename T>
glm::vec3 evaluate(const AnimationTrack<T>& track, float time) { T evaluate(const AnimationTrack<T>& track, float time) {
Keyframe<T> a; Keyframe<T> a;
Keyframe<T> b; Keyframe<T> b;
findSurroundingKeys(track, time, a, b); findSurroundingKeys(track, time, a, b);
if (a.time == b.time) return a.value;
float localT = (time - a.time) / (b.time - a.time); float localT = (time - a.time) / (b.time - a.time);
float easedT = a.easingFunction(localT); float easedT = a.easingFunction(localT);
return glm::mix(a.value, b.value, easedT); return glm::mix(a.value, b.value, easedT);

View File

@ -0,0 +1,11 @@
//
// Created by sebastian on 05.08.26.
//
#ifndef COLORRACE_BLOCKINGANIMATIONCOMPONENT_H
#define COLORRACE_BLOCKINGANIMATIONCOMPONENT_H
namespace engine::animation {
struct BlockingAnimationComponent {
};
}
#endif //COLORRACE_BLOCKINGANIMATIONCOMPONENT_H

View File

@ -12,6 +12,7 @@ namespace engine {
public: public:
virtual ~IComponentPool() = default; virtual ~IComponentPool() = default;
virtual void remove(Entity entity) = 0; virtual void remove(Entity entity) = 0;
virtual size_t size() = 0;
}; };
template<typename T> template<typename T>
@ -33,6 +34,10 @@ namespace engine {
return m_components.at(entity); return m_components.at(entity);
} }
size_t size() override {
return m_components.size();
}
// Für Iteration in Systemen // Für Iteration in Systemen
auto begin() { return m_components.begin(); } auto begin() { return m_components.begin(); }
auto end() { return m_components.end(); } auto end() { return m_components.end(); }

View File

@ -7,6 +7,7 @@
#include "ecs/standardComponents/HighlightComponent.h" #include "ecs/standardComponents/HighlightComponent.h"
#include "ecs/standardComponents/MeshComponent.h" #include "ecs/standardComponents/MeshComponent.h"
#include "ecs/standardComponents/TransformComponent.h" #include "ecs/standardComponents/TransformComponent.h"
#include "spdlog/spdlog.h"
void engine::MeshRenderSystem::update(engine::EntityRegistry &registry, engine::RenderQueue &renderQueue) { void engine::MeshRenderSystem::update(engine::EntityRegistry &registry, engine::RenderQueue &renderQueue) {
@ -20,6 +21,8 @@ void engine::MeshRenderSystem::update(engine::EntityRegistry &registry, engine::
continue; continue;
auto& transform = transforms.get(entity); auto& transform = transforms.get(entity);
// spdlog::info("Entity {}: pos=({}, {}, {})", entity.value(),
// transform.m_position.x, transform.m_position.y, transform.m_position.z);
RenderCommand renderCommand(mesh.model, transform.computeModelMatrix()); RenderCommand renderCommand(mesh.model, transform.computeModelMatrix());
if (registry.hasComponent<HighlightComponent>(entity)) { if (registry.hasComponent<HighlightComponent>(entity)) {

View File

@ -15,6 +15,7 @@
#define GLM_ENABLE_EXPERIMENTAL #define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/string_cast.hpp> #include <glm/gtx/string_cast.hpp>
#include "core/animation/components/BlockingAnimationComponent.h"
#include "core/audio/SoundSource.h" #include "core/audio/SoundSource.h"
#include "GLFW/glfw3.h" #include "GLFW/glfw3.h"
using namespace engine; using namespace engine;
@ -55,9 +56,16 @@ void GameLayer::renderHud() {
} }
bool GameLayer::isAnimationBlocking() {
return m_entityRegistry.getPool<animation::BlockingAnimationComponent>().size() > 0;
}
void GameLayer::onUpdate(float deltaTime) { void GameLayer::onUpdate(float deltaTime) {
Layer::onUpdate(deltaTime); Layer::onUpdate(deltaTime);
m_cameraController->update(getKeyboard(), *m_camera, deltaTime); m_cameraController->update(getKeyboard(), *m_camera, deltaTime);
m_animationSystem.update(deltaTime);
if (isAnimationBlocking()) return;
for (auto& ai : aiControllers) { for (auto& ai : aiControllers) {
ai.update(deltaTime); ai.update(deltaTime);
} }

View File

@ -6,6 +6,7 @@
#define COLORRACE_GAMELAYER_H #define COLORRACE_GAMELAYER_H
#include <utility> #include <utility>
#include "core/animation/AnimationSystem.h"
#include "core/audio/AudioDevice.h" #include "core/audio/AudioDevice.h"
#include "core/audio/SoundBuffer.h" #include "core/audio/SoundBuffer.h"
#include "core/inputsOutputs/controller/CameraController.h" #include "core/inputsOutputs/controller/CameraController.h"
@ -27,7 +28,7 @@ public:
engine::Keyboard& keyboard, engine::Mouse& mouse, std::shared_ptr<engine::Model> boardModel, std::unordered_map<std::string, std::shared_ptr<engine::Model>> pieceModdels) engine::Keyboard& keyboard, engine::Mouse& mouse, std::shared_ptr<engine::Model> boardModel, std::unordered_map<std::string, std::shared_ptr<engine::Model>> pieceModdels)
: Layer(keyboard, mouse), m_gameMode(std::move(gameMode)), m_entityRegistry(entityRegistry), m_camera(std::move(cam)), m_renderer(std::make_unique<engine::MeshRenderer>()), : Layer(keyboard, mouse), m_gameMode(std::move(gameMode)), m_entityRegistry(entityRegistry), m_camera(std::move(cam)), m_renderer(std::make_unique<engine::MeshRenderer>()),
m_pointLight(std::move(light)), m_boardLayout(std::move(boardModel)), m_piecePresenter(m_entityRegistry, std::move(pieceModdels), m_boardLayout), m_pointLight(std::move(light)), m_boardLayout(std::move(boardModel)), m_piecePresenter(m_entityRegistry, std::move(pieceModdels), m_boardLayout),
m_pickingSystem(*m_camera), m_highlightSystem(*m_camera) { m_pickingSystem(*m_camera), m_highlightSystem(*m_camera), m_animationSystem(m_entityRegistry) {
for (auto playerID : m_gameMode->getPlayerOrder()) { for (auto playerID : m_gameMode->getPlayerOrder()) {
if (playerID != m_localPlayer) { if (playerID != m_localPlayer) {
aiControllers.emplace_back(m_gameMode, playerID); aiControllers.emplace_back(m_gameMode, playerID);
@ -58,12 +59,14 @@ private:
std::string m_lastEventMessage; std::string m_lastEventMessage;
std::vector<ludo::LudoAiController> aiControllers; std::vector<ludo::LudoAiController> aiControllers;
engine::animation::AnimationSystem m_animationSystem;
std::unique_ptr<engine::audio::SoundBuffer> m_soundBuffer; std::unique_ptr<engine::audio::SoundBuffer> m_soundBuffer;
void renderHud(); void renderHud();
bool isAnimationBlocking();
}; };

View File

@ -89,9 +89,10 @@ ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIn
void ludo::LudoGameMode::autoReleasePiece(int pieceIndex) { void ludo::LudoGameMode::autoReleasePiece(int pieceIndex) {
auto result = movePieceInternal(pieceIndex, m_gameState.getDiceValue()); auto result = movePieceInternal(pieceIndex, m_gameState.getDiceValue());
m_pendingEvents.emplace_back(PieceMovedEvent{pieceIndex, result.fromPosition, result.toPosition, result.captured}); m_pendingEvents.emplace_back(PieceMovedEvent::create(pieceIndex, result.fromPosition, result.toPosition, result.captured, result.startState, result.targetState));
if (result.captured) { if (result.captured) {
m_pendingEvents.emplace_back(PieceMovedEvent{result.capturedPieceIndex, result.toPosition, -1, false}); //Todo: Determine real toPosition Index dependent on home belegung
m_pendingEvents.emplace_back(PieceMovedEvent::create(result.capturedPieceIndex, result.fromPosition, -1, result.captured, result.startState, result.targetState));
} }
} }
@ -144,7 +145,7 @@ void ludo::LudoGameMode::handle(const MovePieceCommand &command) {
spdlog::info("MovePieceCommand akzeptiert: pieceIndex={}", command.pieceIndex); spdlog::info("MovePieceCommand akzeptiert: pieceIndex={}", command.pieceIndex);
auto result = movePieceInternal(command.pieceIndex, m_gameState.m_diceValue); auto result = movePieceInternal(command.pieceIndex, m_gameState.m_diceValue);
m_pendingEvents.emplace_back(PieceMovedEvent{command.pieceIndex, result.fromPosition, result.toPosition, result.captured}); m_pendingEvents.emplace_back(PieceMovedEvent{command.pieceIndex, result.fromPosition, result.toPosition, result.captured, result.targetState, result.startState});
if (result.captured) { if (result.captured) {
spdlog::info("MovePieceCommand: gegnerische Figur auf {} geworfen", result.toPosition); spdlog::info("MovePieceCommand: gegnerische Figur auf {} geworfen", result.toPosition);
m_pendingEvents.emplace_back(PieceMovedEvent{result.capturedPieceIndex, result.toPosition, -1, false}); m_pendingEvents.emplace_back(PieceMovedEvent{result.capturedPieceIndex, result.toPosition, -1, false});
@ -220,7 +221,69 @@ bool ludo::LudoGameMode::isBlockedByOwnPiece(PlayerColor owner, PieceState state
return false; return false;
} }
namespace {
int stateOrder(ludo::PieceState state) {
switch (state) {
case ludo::PieceState::AtHome: return 0;
case ludo::PieceState::OnTrack: return 1;
case ludo::PieceState::InHomeStretch: return 2;
case ludo::PieceState::Finished: return 3;
}
throw std::invalid_argument("computePath: unbekannter PieceState");
}
}
std::vector<ludo::PathNode> ludo::LudoGameMode::computePath(PlayerColor color, PieceState startingState, int startPosition, PieceState targetState, int targetPosition) { std::vector<ludo::PathNode> ludo::LudoGameMode::computePath(PlayerColor color, PieceState startingState, int startPosition, PieceState targetState, int targetPosition) {
if (startingState == targetState && startPosition == targetPosition) {
return {};
}
// --- Nur Vorwärtsbewegungen sind erlaubt ---
if (stateOrder(targetState) < stateOrder(startingState)) {
throw std::invalid_argument(
"computePath: targetState liegt vor startingState (Rueckwaerts nicht unterstuetzt)");
}
if (targetState == PieceState::AtHome) {
throw std::invalid_argument(
"computePath: AtHome kann kein Zielzustand sein (ausser Start==Ziel)");
}
if (startingState == PieceState::Finished) {
// hier nur erreichbar, wenn targetState/targetPosition abweicht (early return greift sonst)
throw std::invalid_argument(
"computePath: startingState ist bereits Finished, aber Ziel weicht ab");
}
if (startingState == PieceState::InHomeStretch && targetState == PieceState::InHomeStretch
&& targetPosition < startPosition) {
throw std::invalid_argument(
"computePath: targetPosition liegt in InHomeStretch vor startPosition");
}
// --- Positionsbereiche ---
if (targetState == PieceState::OnTrack
&& (targetPosition < 0 || targetPosition >= kTrackLength)) {
throw std::invalid_argument("computePath: targetPosition ausserhalb OnTrack-Bereich");
}
if (targetState == PieceState::InHomeStretch
&& (targetPosition < 0 || targetPosition >= kHomeStretchLength)) {
throw std::invalid_argument("computePath: targetPosition ausserhalb InHomeStretch-Bereich");
}
if (targetState == PieceState::Finished && targetPosition != kHomeStretchLength - 1) {
// <-- vermutlich dein aktueller Bug
throw std::invalid_argument(
"computePath: targetPosition fuer Finished muss kHomeStretchLength - 1 sein");
}
if (startingState == PieceState::OnTrack
&& (startPosition < 0 || startPosition >= kTrackLength)) {
throw std::invalid_argument("computePath: startPosition ausserhalb OnTrack-Bereich");
}
if (startingState == PieceState::InHomeStretch
&& (startPosition < 0 || startPosition >= kHomeStretchLength)) {
throw std::invalid_argument("computePath: startPosition ausserhalb InHomeStretch-Bereich");
}
std::vector<PathNode> path; std::vector<PathNode> path;
if (startPosition == targetPosition && startingState == targetState) return path; if (startPosition == targetPosition && startingState == targetState) return path;
@ -230,7 +293,17 @@ std::vector<ludo::PathNode> ludo::LudoGameMode::computePath(PlayerColor color, P
path.push_back({state, position}); path.push_back({state, position});
// Defense in depth: falls die Validierung oben eine Kombination übersieht
// (z.B. nach zukünftigen Erweiterungen), lieber laut abstürzen als RAM fressen.
const int maxSteps = kTrackLength + kHomeStretchLength + 4;
int steps = 0;
while (state != targetState || position != targetPosition) { while (state != targetState || position != targetPosition) {
if (++steps > maxSteps) {
throw std::logic_error(
"computePath: Iterationslimit ueberschritten - unreachable Ziel trotz Validierung");
}
switch (state) { switch (state) {
case PieceState::AtHome: case PieceState::AtHome:
state = PieceState::OnTrack; state = PieceState::OnTrack;

View File

@ -8,6 +8,7 @@
#include <array> #include <array>
#include <cassert> #include <cassert>
#include <cstdint> #include <cstdint>
#include <stdexcept>
#include <variant> #include <variant>
#include "game/TurnBasedGameMode.h" #include "game/TurnBasedGameMode.h"
@ -66,7 +67,34 @@ namespace ludo {
struct RollDiceCommand { engine::PlayerID player; }; struct RollDiceCommand { engine::PlayerID player; };
using LudoCommand = std::variant<MovePieceCommand, RollDiceCommand>; using LudoCommand = std::variant<MovePieceCommand, RollDiceCommand>;
struct PieceMovedEvent { int pieceIndex; int fromPosition; int toPosition; bool captured; PieceState targetState; PieceState startState; }; struct PieceMovedEvent {
int pieceIndex;
int fromPosition;
int toPosition;
bool captured;
PieceState targetState;
PieceState startState;
static PieceMovedEvent create(int pieceIndex, int fromPosition, int toPosition,
bool captured, PieceState startState, PieceState targetState) {
validate(startState, fromPosition, targetState, toPosition, captured);
return PieceMovedEvent{pieceIndex, fromPosition, toPosition, captured, targetState, startState};
}
private:
static void validate(PieceState startState, int fromPosition,
PieceState targetState, int toPosition, bool captured) {
if (captured && targetState != PieceState::AtHome) {
throw std::invalid_argument(
"PieceMovedEvent: captured==true erwartet targetState==AtHome");
}
if (!captured && targetState == PieceState::AtHome && startState != PieceState::AtHome) {
throw std::invalid_argument(
"PieceMovedEvent: targetState==AtHome ohne captured-Flag ist nicht vorgesehen");
}
// weitere Regeln nach Bedarf, z.B. Bereichschecks für fromPosition/toPosition
}
};
struct DiceRolledEvent { engine::PlayerID player; int diceValue; }; struct DiceRolledEvent { engine::PlayerID player; int diceValue; };
struct TurnChangedEvent {engine::PlayerID newPlayer; }; struct TurnChangedEvent {engine::PlayerID newPlayer; };
using LudoEvent = std::variant<PieceMovedEvent, DiceRolledEvent, TurnChangedEvent>; using LudoEvent = std::variant<PieceMovedEvent, DiceRolledEvent, TurnChangedEvent>;

View File

@ -6,6 +6,7 @@
#include "LudoGameMode.h" #include "LudoGameMode.h"
#include "core/animation/components/BlockingAnimationComponent.h"
#include "core/animation/components/TransformAnimationComponent.h" #include "core/animation/components/TransformAnimationComponent.h"
#include "ecs/standardComponents/MeshComponent.h" #include "ecs/standardComponents/MeshComponent.h"
#include "ecs/standardComponents/PickableComponent.h" #include "ecs/standardComponents/PickableComponent.h"
@ -30,8 +31,39 @@ void ludo::PiecePresenter::onPieceMoved(const ludo::PieceMovedEvent &event, cons
const auto& piece = state.getPieces()[event.pieceIndex]; const auto& piece = state.getPieces()[event.pieceIndex];
auto target = m_layout.getWorldPosition(piece.color, piece.state, piece.position); auto target = m_layout.getWorldPosition(piece.color, piece.state, piece.position);
auto entity = m_pieceEntities[event.pieceIndex]; auto entity = m_pieceEntities[event.pieceIndex];
auto& transform = m_registry.getComponent<engine::TransformComponent>(entity);
transform.m_position = target; engine::animation::TransformAnimationComponent animationComponent;
std::vector<PathNode> path = LudoGameMode::computePath(piece.color, event.startState, event.fromPosition, event.targetState, event.toPosition);
std::vector<glm::vec3> pathPositions;
for (const auto& node : path) {
pathPositions.push_back(m_layout.getWorldPosition(piece.color, node.state, node.position));
}
std::vector<engine::animation::Keyframe<glm::vec3>> keyframes;
float time = 0;
for (const auto& position : pathPositions) {
keyframes.emplace_back(time, position);
time += 0.5f; // Todo: Check if this is in ms or s
}
engine::animation::AnimationTrack<glm::vec3> positionTrack;
engine::animation::AnimationTrack<glm::vec3> rotationTrack;
engine::animation::AnimationTrack<glm::vec3> scaleTrack;
positionTrack.keys = keyframes;
animationComponent.position = positionTrack;
animationComponent.rotation = rotationTrack;
animationComponent.scale = scaleTrack;
animationComponent.loop = false;
animationComponent.duration = time;
animationComponent.onFinished = [this, entity]() {
m_registry.removeComponent<engine::animation::BlockingAnimationComponent>(entity);
};
m_registry.addComponent<engine::animation::TransformAnimationComponent>(entity, animationComponent);
m_registry.addComponent<engine::animation::BlockingAnimationComponent>(entity, engine::animation::BlockingAnimationComponent());
// auto& transform = m_registry.getComponent<engine::TransformComponent>(entity);
// transform.m_position = target;
} }
std::optional<int> ludo::PiecePresenter::getPieceIndex(std::optional<engine::Entity>::value_type entity) { std::optional<int> ludo::PiecePresenter::getPieceIndex(std::optional<engine::Entity>::value_type entity) {