← All examples
Rendering
Sprite Rotation
Transform::m_Rotation, applied for real
Showcases Transform::m_Rotation actually being applied by RenderSystem: a plain rotating sprite (the whole-texture DrawTextureAffine path) alongside a rotating animated sprite (the source-rect DrawTextureAffine overload — rotated and cropped to its current animation frame at once).
Both entities just accumulate their own angular speed into their own Transform::m_Rotation each Update() — there's no engine-side "spin" component, it's ordinary per-entity gameplay state living in the demo itself.
Controls
- LEFT / RIGHT Adjust the plain sprite's spin speed (can reverse direction)
- SPACE Pause/resume both sprites
- R Reset

Run it yourself
build just this example
cmake --build --preset windows-debug --target rotation_demo
# binary lands under bin/ (e.g. bin/Debug/rotation_demo.exe on Windows)
See Installation for the full build setup.
Source
examples/rotation_demo/Game.hpp
#pragma once
#include <ASGE/ASGE.hpp>
/**
* @brief Showcases Transform::m_Rotation actually being applied by
* RenderSystem: a plain rotating sprite (the whole-texture
* DrawTextureAffine path) alongside a rotating *animated* sprite (the
* source-rect DrawTextureAffine overload — rotated and cropped to
* its current animation frame at once).
*
* Both entities just accumulate their own angular speed into their own
* Transform::m_Rotation each Update() — there's no engine-side "spin"
* component, this is ordinary per-entity gameplay state living in the demo.
* LEFT/RIGHT adjust the plain sprite's spin speed (and can reverse its
* direction), SPACE pauses/resumes both, R resets everything.
*/
class RotationDemoState final : public asge::game::state::IGameState<int>
{
asge::ecs::Registry& m_Registry;
asge::game::asset::AssetManager& m_Assets;
asge::ecs::Entity m_Spinner{ asge::ecs::Entity::Null() }; // whole-texture rotation
asge::ecs::Entity m_AnimatedSpinner{ asge::ecs::Entity::Null() }; // rotation + cropped animation frame
float m_SpinnerSpeed{ 1.5f }; // radians/second; LEFT/RIGHT-adjustable, can go negative
bool m_Paused{ false };
void SpawnEntities();
void UpdateRotation(float inDeltaTime);
void AdjustSpinnerSpeed(float inDelta);
void Reset();
void RenderHud(asge::video::IRenderer& inRenderer) const;
public:
RotationDemoState(asge::ecs::Registry& inRegistry, asge::game::asset::AssetManager& inAssets);
[[nodiscard]] std::optional<asge::game::state::Transition<int>>
Update(float inDeltaTime, asge::input::InputState const& inInput) override;
void Render(asge::video::IRenderer& inRenderer) override;
[[nodiscard]] asge::graphics::RGBA_Color ClearColor() const noexcept override { return { 15, 18, 22, 255 }; }
void OnSystemEvent(asge::event::SystemEvent const& inSysEvent) override;
};
class RotationDemoGame final : public asge::game::Game<int>
{
public:
explicit RotationDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev);
protected:
[[nodiscard]] std::unique_ptr<StateType> CreateState(int inId) override;
};
examples/rotation_demo/Game.cpp
#include "Game.hpp"
#include <algorithm>
#include <cmath>
namespace
{
using asge::input::Keycode;
using asge::graphics::RGBA_Color;
using asge::game::components::Animation;
using asge::game::components::Sprite;
using asge::game::components::Transform;
constexpr float kPi = 3.14159265358979323846f;
constexpr float kTwoPi = kPi * 2.0f;
constexpr char const* kArrowPath = "textures/arrow.bmp";
constexpr char const* kSheetPath = "textures/spritesheet.bmp";
constexpr char const* kClipPath = "textures/walk.toml"; // asset::FrameTable meta-file -- see assets/
constexpr float kArrowScale = 4.0f; // draws the 32x32 arrow at 128x128
constexpr float kSheetScale = 3.0f; // draws each 32x32 cell at 96x96
constexpr float kSpinnerX = 260.0f;
constexpr float kAnimatedSpinnerX = 540.0f;
constexpr float kSpinY = 300.0f;
constexpr float kAnimatedAngularSpeed = -0.9f; // radians/second, fixed -- just for visual contrast with the player-adjustable spinner
constexpr float kFrameDuration = 0.12f;
constexpr float kSpeedStep = 0.5f; // radians/second per LEFT/RIGHT press
/** @brief Wraps inRadians into [0, 2*pi) -- keeps the spinner's rotation from growing unbounded over a long-running demo. */
float WrapAngle( float inRadians ) noexcept
{
float wrapped = std::fmod( inRadians, kTwoPi );
if ( wrapped < 0.0f ) wrapped += kTwoPi;
return wrapped;
}
}
RotationDemoState::RotationDemoState(
asge::ecs::Registry& inRegistry, asge::game::asset::AssetManager& inAssets
)
: m_Registry(inRegistry), m_Assets(inAssets)
{
SpawnEntities();
}
void RotationDemoState::SpawnEntities()
{
auto spinner = m_Registry.CreateEntity();
if ( !spinner ) { spinner.LogError(); return; }
m_Spinner = spinner.Value();
m_Registry.AddComponent<Transform>( m_Spinner, Transform{
.m_LocalCoordinates = {kSpinnerX, kSpinY}, .m_LocalScale = {kArrowScale, kArrowScale},
.m_WorldCoordinates = {kSpinnerX, kSpinY}, .m_WorldScale = {kArrowScale, kArrowScale}
} );
// m_Texture stays null until Render()'s AssetManager::ResolveAssets call.
m_Registry.AddComponent<Sprite>( m_Spinner, Sprite{ .m_VirtualPath = kArrowPath } );
auto animated = m_Registry.CreateEntity();
if ( !animated ) { animated.LogError(); return; }
m_AnimatedSpinner = animated.Value();
m_Registry.AddComponent<Transform>( m_AnimatedSpinner, Transform{
.m_LocalCoordinates = {kAnimatedSpinnerX, kSpinY}, .m_LocalScale = {kSheetScale, kSheetScale},
.m_WorldCoordinates = {kAnimatedSpinnerX, kSpinY}, .m_WorldScale = {kSheetScale, kSheetScale}
} );
m_Registry.AddComponent<Sprite>( m_AnimatedSpinner, Sprite{ .m_VirtualPath = kSheetPath } );
m_Registry.AddComponent<Animation>( m_AnimatedSpinner, Animation{
.m_ClipPath = kClipPath, .m_FrameDuration = kFrameDuration
} );
}
void RotationDemoState::UpdateRotation(float inDeltaTime)
{
if ( m_Paused ) return;
// Written straight to both Local and World -- this demo never runs
// TransformPropagationSystem (RenderPipeline doesn't call it), so World
// is what RenderSystem actually draws from.
if ( auto result = m_Registry.GetComponent<Transform>( m_Spinner ) )
{
Transform& t = result.Value().get();
t.m_LocalRotation = WrapAngle( t.m_LocalRotation + m_SpinnerSpeed * inDeltaTime );
t.m_WorldRotation = t.m_LocalRotation;
}
if ( auto result = m_Registry.GetComponent<Transform>( m_AnimatedSpinner ) )
{
Transform& t = result.Value().get();
t.m_LocalRotation = WrapAngle( t.m_LocalRotation + kAnimatedAngularSpeed * inDeltaTime );
t.m_WorldRotation = t.m_LocalRotation;
}
}
void RotationDemoState::AdjustSpinnerSpeed(float inDelta)
{
m_SpinnerSpeed += inDelta;
}
void RotationDemoState::Reset()
{
m_Paused = false;
m_SpinnerSpeed = 1.5f;
for ( auto entity : { m_Spinner, m_AnimatedSpinner } )
{
if ( auto result = m_Registry.GetComponent<Transform>( entity ) )
{
result.Value().get().m_LocalRotation = 0.0f;
result.Value().get().m_WorldRotation = 0.0f;
}
}
}
void RotationDemoState::RenderHud(asge::video::IRenderer &inRenderer) const
{
// "PAUSED" indicator (top-left): lit while rotation is frozen.
inRenderer.DrawRect(
asge::math::Rect{ 20.0f, 20.0f, 30.0f, 16.0f },
m_Paused ? RGBA_Color{ 240, 120, 60, 255 } : RGBA_Color{ 55, 55, 60, 255 },
true
);
// Spin-speed bar (below it): fill tracks m_SpinnerSpeed's magnitude,
// centered so a negative (reversed) speed fills from the middle leftward.
constexpr float kMaxSpeed = 6.0f;
asge::math::Rect const track{ 20.0f, 46.0f, 160.0f, 16.0f };
inRenderer.DrawRect(track, RGBA_Color{ 90, 90, 100, 255 }, false);
float const half = track.m_Width * 0.5f;
float const fill = std::clamp( m_SpinnerSpeed / kMaxSpeed, -1.0f, 1.0f ) * half;
asge::math::Rect const bar = fill >= 0.0f
? asge::math::Rect{ track.m_X + half, track.m_Y, fill, track.m_Height }
: asge::math::Rect{ track.m_X + half + fill, track.m_Y, -fill, track.m_Height };
inRenderer.DrawRect(bar, RGBA_Color{ 100, 170, 240, 255 }, true);
}
std::optional<asge::game::state::Transition<int>>
RotationDemoState::Update(float inDeltaTime, [[maybe_unused]] asge::input::InputState const &inInput)
{
UpdateRotation( inDeltaTime );
return std::nullopt;
}
void RotationDemoState::Render(asge::video::IRenderer &inRenderer)
{
RenderHud( inRenderer );
}
void RotationDemoState::OnSystemEvent(asge::event::SystemEvent const &inSysEvent)
{
auto const* keyEvent = inSysEvent.TryGet<asge::event::KeyboardEvent>();
if ( !keyEvent ) return;
if ( keyEvent->s_Type != asge::event::EventType::KEYBOARD_KEY_PRESSED ) return;
if ( keyEvent->s_Repeat ) return; // edge-triggered: one step/toggle per physical press
if ( keyEvent->s_Keycode == Keycode::LEFT ) AdjustSpinnerSpeed( -kSpeedStep );
else if ( keyEvent->s_Keycode == Keycode::RIGHT ) AdjustSpinnerSpeed( kSpeedStep );
else if ( keyEvent->s_Keycode == Keycode::SPACE ) m_Paused = !m_Paused;
else if ( keyEvent->s_Keycode == Keycode::R ) Reset();
}
RotationDemoGame::RotationDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev)
: Game(inRenderer, inAudioDev)
{
// ASGE_ROTATION_DEMO_ASSET_DIR is injected by CMakeLists.txt; mounted
// once so textures are loaded by virtual path instead of a hardcoded OS
// path baked into this demo.
auto mountResult = m_Vfs.Mount("textures", ASGE_ROTATION_DEMO_ASSET_DIR);
if ( !mountResult ) mountResult.LogError();
SetInitialState(0);
}
std::unique_ptr<RotationDemoGame::StateType> RotationDemoGame::CreateState([[maybe_unused]] int inId)
{
return std::make_unique<RotationDemoState>( m_SceneManager.GetRegistry(), m_Assets );
}