ADD: AnimationSystem + Tests

This commit is contained in:
sebastian 2026-08-05 06:41:08 +02:00
parent dedc37f6ac
commit befd8b3cd9
14 changed files with 254 additions and 57 deletions

View File

@ -67,6 +67,18 @@ CPMAddPackage(
GIT_TAG v1.15.3 GIT_TAG v1.15.3
) )
# --- GoogleTest ---
CPMAddPackage(
NAME googletest
GITHUB_REPOSITORY google/googletest
GIT_TAG v1.15.2
OPTIONS
"INSTALL_GTEST OFF"
"gtest_force_shared_crt ON"
)
enable_testing()
add_library(imgui STATIC add_library(imgui STATIC
${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp
@ -203,15 +215,6 @@ add_library(Engine STATIC
engine/src/core/animation/components/TransformAnimationComponent.h engine/src/core/animation/components/TransformAnimationComponent.h
engine/src/core/animation/AnimationSystem.cpp engine/src/core/animation/AnimationSystem.cpp
engine/src/core/animation/AnimationSystem.h engine/src/core/animation/AnimationSystem.h
)
target_include_directories(Engine PUBLIC engine/src ${dr_libs_SOURCE_DIR})
target_link_libraries(Engine PUBLIC OpenGL::GL glfw glad glm::glm imgui spdlog::spdlog tinyobjloader stb_image OpenAL::OpenAL)
# --- Executable ---
add_executable(ColorRace
game/src/main.cpp
game/src/ColorRaceApp.cpp
game/src/ColorRaceApp.h
engine/src/renderer/shader/StaticShader.cpp engine/src/renderer/shader/StaticShader.cpp
engine/src/renderer/shader/StaticShader.h engine/src/renderer/shader/StaticShader.h
engine/src/renderer/entities/Camera.cpp engine/src/renderer/entities/Camera.cpp
@ -236,14 +239,22 @@ add_executable(ColorRace
engine/src/renderer/MeshRenderer.h engine/src/renderer/MeshRenderer.h
engine/src/layer/Layer.cpp engine/src/layer/Layer.cpp
engine/src/layer/Layer.h engine/src/layer/Layer.h
game/src/GameLayer.cpp
game/src/GameLayer.h
engine/src/layer/SceneManager.cpp
engine/src/layer/SceneManager.h
game/src/GameScene.cpp
game/src/GameScene.h
engine/src/core/inputsOutputs/context/InputContext.cpp engine/src/core/inputsOutputs/context/InputContext.cpp
engine/src/core/inputsOutputs/context/InputContext.h engine/src/core/inputsOutputs/context/InputContext.h
engine/src/layer/SceneManager.cpp
engine/src/layer/SceneManager.h
)
target_include_directories(Engine PUBLIC engine/src ${dr_libs_SOURCE_DIR})
target_link_libraries(Engine PUBLIC OpenGL::GL glfw glad glm::glm imgui spdlog::spdlog tinyobjloader stb_image OpenAL::OpenAL)
# --- GameLib: Spiel-Logik, unabhängig von main.cpp testbar ---
add_library(GameLib STATIC
game/src/ColorRaceApp.cpp
game/src/ColorRaceApp.h
game/src/GameLayer.cpp
game/src/GameLayer.h
game/src/GameScene.cpp
game/src/GameScene.h
game/src/ludo/LudoGameState.cpp game/src/ludo/LudoGameState.cpp
game/src/ludo/LudoGameState.h game/src/ludo/LudoGameState.h
game/src/ludo/LudoTypes.h game/src/ludo/LudoTypes.h
@ -259,9 +270,34 @@ add_executable(ColorRace
game/src/ludo/ecs/highlight/HighlightSystem.h game/src/ludo/ecs/highlight/HighlightSystem.h
game/src/AudioLayer.cpp game/src/AudioLayer.cpp
game/src/AudioLayer.h game/src/AudioLayer.h
game/src/ludo/DiceSource.cpp
game/src/ludo/DiceSource.h
)
target_include_directories(GameLib PUBLIC game/src)
target_link_libraries(GameLib PUBLIC Engine)
# --- Executable ---
add_executable(ColorRace
game/src/main.cpp
) )
target_link_libraries(ColorRace PRIVATE target_link_libraries(ColorRace PRIVATE
Engine Engine
) GameLib
)
# --- Tests ---
add_executable(ColorRaceTests
tests/LoduGameMode.cpp
tests/helpers/SequenceDiceSource.cpp
tests/helpers/SequenceDiceSource.h
)
target_link_libraries(ColorRaceTests PRIVATE
GameLib
GTest::gtest
GTest::gtest_main
)
include(GoogleTest)
gtest_discover_tests(ColorRaceTests)

View File

@ -30,24 +30,4 @@ namespace engine::animation {
} }
} }
} }
glm::vec3 AnimationSystem::evaluate(const AnimationTrack &track, float time) {
Keyframe a;
Keyframe b;
findSurroundingKeys(track, time, a, b);
float localT = (time - a.time) / (b.time - a.time);
float easedT = a.easingFunction(localT);
return glm::mix(a.value, b.value, easedT);
}
void AnimationSystem::findSurroundingKeys(const AnimationTrack &track, float time, Keyframe &a, Keyframe &b) {
for (int i = 1; i < track.keys.size(); i++) {
if (track.keys[i-1].time < 0.f && track.keys[i].time > 0.f) {
a = track.keys[i-1];
b = track.keys[i];
return;
}
}
}
} // engine } // engine

View File

@ -14,8 +14,43 @@ namespace engine::animation {
void update(float deltaTime); void update(float deltaTime);
private: private:
EntityRegistry& m_registry; EntityRegistry& m_registry;
glm::vec3 evaluate(const AnimationTrack& track, float time);
void findSurroundingKeys(const AnimationTrack& track, float time, Keyframe& a, Keyframe& b); template<typename T>
glm::vec3 evaluate(const AnimationTrack<T>& track, float time) {
Keyframe<T> a;
Keyframe<T> b;
findSurroundingKeys(track, time, a, b);
float localT = (time - a.time) / (b.time - a.time);
float easedT = a.easingFunction(localT);
return glm::mix(a.value, b.value, easedT);
}
template<typename T>
void findSurroundingKeys(const AnimationTrack<T>& track, float time, Keyframe<T>& a, Keyframe<T>& b) {
if (track.keys.size() == 1) {
a = b = track.keys.front();
return;
}
if (time <= track.keys.front().time) {
a = b = track.keys.front();
return;
}
if (time >= track.keys.back().time) {
a = b = track.keys.back();
return;
}
for (size_t i = 1; i < track.keys.size(); ++i) {
if (track.keys[i - 1].time <= time && track.keys[i].time >= time) {
a = track.keys[i - 1];
b = track.keys[i];
return;
}
}
}
}; };
} // engine } // engine

View File

@ -11,26 +11,29 @@
namespace engine::animation { namespace engine::animation {
using EasingFunction = std::function<float(float)>; using EasingFunction = std::function<float(float)>;
template<typename T>
struct Keyframe { struct Keyframe {
float time; float time;
glm::vec3 value; T value;
EasingFunction easingFunction = [](float t) { EasingFunction easingFunction = [](float t) {
return t; return t;
}; };
}; };
template<typename T>
struct AnimationTrack { struct AnimationTrack {
std::vector<Keyframe> keys; std::vector<Keyframe<T>> keys;
}; };
struct TransformAnimationComponent { struct TransformAnimationComponent {
float time = 0.0f; float time = 0.0f;
float duration = 0.0f; float duration = 0.0f;
AnimationTrack position; AnimationTrack<glm::vec3> position;
AnimationTrack rotation; AnimationTrack<glm::vec3> rotation;
AnimationTrack scale; AnimationTrack<glm::vec3> scale;
std::function<void()> onFinished; std::function<void()> onFinished;
bool loop = false; bool loop = false;

View File

@ -5,7 +5,6 @@
#include "BoardLayout.h" #include "BoardLayout.h"
#include <format> #include <format>
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
glm::vec3 ludo::BoardLayout::getWorldPosition(ludo::PlayerColor color, ludo::PieceState state, int position) const { glm::vec3 ludo::BoardLayout::getWorldPosition(ludo::PlayerColor color, ludo::PieceState state, int position) const {
@ -27,4 +26,4 @@ glm::vec3 ludo::BoardLayout::getWorldPosition(ludo::PlayerColor color, ludo::Pie
return glm::vec3(0.0f); return glm::vec3(0.0f);
} }
return it->second; return it->second;
} }

View File

@ -0,0 +1,10 @@
//
// Created by sebastian on 05.08.26.
//
#include "DiceSource.h"
int ludo::RandomDiceSource::roll() {
std::uniform_int_distribution<int> distribution(1, 6);
return distribution(m_generator);
}

View File

@ -0,0 +1,26 @@
//
// Created by sebastian on 05.08.26.
//
#ifndef COLORRACE_DICESOURCE_H
#define COLORRACE_DICESOURCE_H
#include <random>
namespace ludo {
class IDiceSource {
public:
virtual ~IDiceSource() = default;
virtual int roll() = 0;
};
class RandomDiceSource : public IDiceSource {
public:
int roll() override;
private:
std::mt19937 m_generator{std::random_device{}()};
};
}
#endif //COLORRACE_DICESOURCE_H

View File

@ -52,6 +52,7 @@ std::optional<int> ludo::LudoGameMode::findLowestHomeSlotPiece(PlayerColor color
return i; // erste Übereinstimmung = kleinster Home-Slot, dank Erzeugungsreihenfolge return i; // erste Übereinstimmung = kleinster Home-Slot, dank Erzeugungsreihenfolge
} }
} }
return std::nullopt;
} }
ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIndex, int diceValue) { ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIndex, int diceValue) {
@ -59,10 +60,13 @@ ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIn
int fromPosition = piece.position; int fromPosition = piece.position;
Destination destination = computeDestination(piece, diceValue).value(); Destination destination = computeDestination(piece, diceValue).value();
PieceState startState = piece.state;
PieceState targetState = destination.state;
piece.state = destination.state; piece.state = destination.state;
piece.position = destination.position; piece.position = destination.position;
MoveResult result{fromPosition, piece.position, false, -1, targetState, startState};
MoveResult result{fromPosition, piece.position, false, -1};
if (piece.state == PieceState::OnTrack) { if (piece.state == PieceState::OnTrack) {
for (int i = 0; i < static_cast<int>(m_gameState.m_pieces.size()); ++i) { for (int i = 0; i < static_cast<int>(m_gameState.m_pieces.size()); ++i) {
@ -185,11 +189,10 @@ std::optional<int> ludo::LudoGameMode::wouldCapture(int pieceIndex, int diceValu
int ludo::LudoGameMode::rollDice() { int ludo::LudoGameMode::rollDice() {
std::uniform_int_distribution<int> distribution(1, 6); return m_diceSource->roll();
return distribution(m_rng);
} }
int ludo::LudoGameMode::getEntryOffset(PlayerColor color) const { int ludo::LudoGameMode::getEntryOffset(PlayerColor color) {
return static_cast<int>(color) * 10; return static_cast<int>(color) * 10;
} }
@ -201,3 +204,42 @@ bool ludo::LudoGameMode::isBlockedByOwnPiece(PlayerColor owner, PieceState state
} }
return false; return false;
} }
std::vector<ludo::PathNode> ludo::LudoGameMode::computePath(PlayerColor color, PieceState startingState, int startPosition, PieceState targetState, int targetPosition) {
std::vector<PathNode> path;
if (startPosition == targetPosition && startingState == targetState) return path;
PieceState state = startingState;
int position = startPosition;
path.push_back({state, position});
while (state != targetState || position != targetPosition) {
switch (state) {
case PieceState::AtHome:
state = PieceState::OnTrack;
position = getEntryOffset(color);
break;
case PieceState::OnTrack:
if (position == getEntryOffset(color) && (targetState == PieceState::InHomeStretch || targetState == PieceState::Finished)) {
state = PieceState::InHomeStretch;
position = 0;
} else {
position = (position + 1) % kTrackLength;
}
break;
case PieceState::InHomeStretch:
if (targetState == PieceState::Finished && position == kHomeStretchLength - 1) {
state = PieceState::Finished;
} else {
++position;
}
break;
case PieceState::Finished:
break;
}
path.push_back({state, position});
}
return path;
}

View File

@ -4,18 +4,25 @@
#ifndef COLORRACE_LUDOGAMEMODE_H #ifndef COLORRACE_LUDOGAMEMODE_H
#define COLORRACE_LUDOGAMEMODE_H #define COLORRACE_LUDOGAMEMODE_H
#include <memory>
#include <optional> #include <optional>
#include <random> #include <random>
#include "DiceSource.h"
#include "LudoGameState.h" #include "LudoGameState.h"
#include "LudoTypes.h" #include "LudoTypes.h"
#include "game/TurnBasedGameMode.h" #include "game/TurnBasedGameMode.h"
namespace ludo { namespace ludo {
struct PathNode {
PieceState state;
int position;
};
class LudoGameMode: public engine::TurnBasedGameMode<LudoCommand, LudoEvent, LudoGameState> { class LudoGameMode: public engine::TurnBasedGameMode<LudoCommand, LudoEvent, LudoGameState> {
public: public:
explicit LudoGameMode(std::vector<engine::PlayerID> players, std::optional<unsigned int> seed = std::nullopt) : TurnBasedGameMode(std::move(players)), explicit LudoGameMode(std::vector<engine::PlayerID> players, std::optional<unsigned int> seed = std::nullopt) : TurnBasedGameMode(std::move(players)),
m_rng(seed.value_or(std::random_device{}())), m_gameState(getPlayerOrder()) {} m_diceSource(std::make_unique<RandomDiceSource>()), m_gameState(getPlayerOrder()) {}
void sendCommand(const std::variant<MovePieceCommand, RollDiceCommand> &command) override; void sendCommand(const std::variant<MovePieceCommand, RollDiceCommand> &command) override;
std::vector<LudoEvent> pollEvents() override; std::vector<LudoEvent> pollEvents() override;
@ -25,12 +32,18 @@ namespace ludo {
std::vector<int> getMoveablePieceIndices(PlayerColor color, int diceValue) const; std::vector<int> getMoveablePieceIndices(PlayerColor color, int diceValue) const;
std::optional<int> wouldCapture(int pieceIndex, int diceValue) const; std::optional<int> wouldCapture(int pieceIndex, int diceValue) const;
[[nodiscard]] static int getEntryOffset(PlayerColor color);
static std::vector<PathNode> computePath(PlayerColor color, PieceState startingState, int startPosition, PieceState targetState, int targetPosition);
void setDiceSource(std::unique_ptr<IDiceSource> diceSource) { m_diceSource = std::move(diceSource); }
private: private:
struct MoveResult { struct MoveResult {
int fromPosition; int fromPosition = 0;
int toPosition; int toPosition = 0;
bool captured = false; bool captured = false;
int capturedPieceIndex = -1; int capturedPieceIndex = -1;
PieceState targetState = PieceState::OnTrack;
PieceState startState = PieceState::OnTrack;
}; };
struct Destination { struct Destination {
@ -46,8 +59,6 @@ namespace ludo {
void handle(const RollDiceCommand &command); void handle(const RollDiceCommand &command);
void handle(const MovePieceCommand &command); void handle(const MovePieceCommand &command);
void finishTurn(); void finishTurn();
@ -56,9 +67,9 @@ namespace ludo {
std::vector<LudoEvent> m_pendingEvents; std::vector<LudoEvent> m_pendingEvents;
int rollDice(); int rollDice();
std::mt19937 m_rng; std::unique_ptr<IDiceSource> m_diceSource;
[[nodiscard]] int getEntryOffset(PlayerColor color) const;
[[nodiscard]] bool isBlockedByOwnPiece(PlayerColor owner, PieceState state, int position) const; [[nodiscard]] bool isBlockedByOwnPiece(PlayerColor owner, PieceState state, int position) const;
}; };

View File

@ -66,7 +66,7 @@ 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; }; struct PieceMovedEvent { int pieceIndex; int fromPosition; int toPosition; bool captured; PieceState targetState; PieceState startState; };
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

@ -5,6 +5,8 @@
#include "PiecePresenter.h" #include "PiecePresenter.h"
#include "LudoGameMode.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"
#include "ecs/standardComponents/TransformComponent.h" #include "ecs/standardComponents/TransformComponent.h"
@ -37,3 +39,5 @@ std::optional<int> ludo::PiecePresenter::getPieceIndex(std::optional<engine::Ent
if (it == m_pieceEntities.end()) return std::nullopt; if (it == m_pieceEntities.end()) return std::nullopt;
return static_cast<int>(std::distance(m_pieceEntities.begin(), it)); return static_cast<int>(std::distance(m_pieceEntities.begin(), it));
} }

19
tests/LoduGameMode.cpp Normal file
View File

@ -0,0 +1,19 @@
#include "gtest/gtest.h"
//
// Created by sebastian on 05.08.26.
//
#include "helpers/SequenceDiceSource.h"
#include "ludo/LudoGameMode.h"
TEST(LudoGameMode, SecondSixMovesOutOfBlockedEntry) {
ludo::LudoGameMode mode({0}); // ein Spieler reicht zum Isolieren
mode.setDiceSource(std::make_unique<SequenceDiceSource>(std::vector<int>{6,6}));
mode.sendCommand(ludo::RollDiceCommand{0});
ASSERT_EQ(mode.getCurrentPlayer(), 0u) << "Spieler sollte nach Extra-Turn gleich bleiben";
ASSERT_EQ(mode.getState().getPhase(), ludo::TurnPhase::AwaitingRoll);
mode.sendCommand(ludo::RollDiceCommand{0});
EXPECT_EQ(mode.getState().getPhase(), ludo::TurnPhase::AwaitingPieceSelection)
<< "Erwartet: Figur-Auswahl, da Entry blockiert";
}

View File

@ -0,0 +1,11 @@
//
// Created by sebastian on 05.08.26.
//
#include "SequenceDiceSource.h"
int SequenceDiceSource::roll() {
int v = m_diceValues.at(m_index);
m_index = std::min(m_index +1, m_diceValues.size() -1);
return v;
}

View File

@ -0,0 +1,21 @@
//
// Created by sebastian on 05.08.26.
//
#ifndef COLORRACE_SEQUENCEDICESOURCE_H
#define COLORRACE_SEQUENCEDICESOURCE_H
#include "ludo/DiceSource.h"
class SequenceDiceSource : public ludo::IDiceSource{
public:
explicit SequenceDiceSource(const std::vector<int> &diceValues) : m_diceValues(diceValues) {}
int roll() override;
private:
std::vector<int> m_diceValues;
size_t m_index = 0;
};
#endif //COLORRACE_SEQUENCEDICESOURCE_H