diff --git a/game/assets/character/male/Male_spritesheet_idle.png b/game/assets/character/male/Male_spritesheet_idle.png new file mode 100644 index 0000000..673176d Binary files /dev/null and b/game/assets/character/male/Male_spritesheet_idle.png differ diff --git a/game/assets/character/male/Male_spritesheet_punch_1.png b/game/assets/character/male/Male_spritesheet_punch_1.png new file mode 100644 index 0000000..6926117 Binary files /dev/null and b/game/assets/character/male/Male_spritesheet_punch_1.png differ diff --git a/game/assets/character/male/Male_spritesheet_run.png b/game/assets/character/male/Male_spritesheet_run.png new file mode 100644 index 0000000..ff1341a Binary files /dev/null and b/game/assets/character/male/Male_spritesheet_run.png differ diff --git a/game/assets/shaders/colored_quad.frag b/game/assets/shaders/colored_quad.frag new file mode 100644 index 0000000..8b6ef84 --- /dev/null +++ b/game/assets/shaders/colored_quad.frag @@ -0,0 +1,8 @@ +#version 330 core +in vec4 v_color; + +out vec4 FragColor; + +void main() { + FragColor = v_color; +} diff --git a/game/assets/shaders/colored_quad.vert b/game/assets/shaders/colored_quad.vert new file mode 100644 index 0000000..a0763de --- /dev/null +++ b/game/assets/shaders/colored_quad.vert @@ -0,0 +1,14 @@ +#version 330 core +layout (location = 0) in vec2 a_position; +layout (location = 1) in vec2 a_texCoord; +layout (location = 2) in vec4 a_color; + +uniform mat4 u_view; +uniform mat4 u_projection; + +out vec4 v_color; + +void main() { + v_color = a_color; + gl_Position = u_projection * u_view * vec4(a_position, 0.0, 1.0); +} diff --git a/game/assets/shaders/shaders.json b/game/assets/shaders/shaders.json new file mode 100644 index 0000000..1ad3932 --- /dev/null +++ b/game/assets/shaders/shaders.json @@ -0,0 +1,12 @@ +{ + "shaders": { + "sprite": { + "vertex": "sprite.vert", + "fragment": "sprite.frag" + }, + "colored_quad": { + "vertex": "colored_quad.vert", + "fragment": "colored_quad.frag" + } + } +} diff --git a/game/assets/shaders/sprite.frag b/game/assets/shaders/sprite.frag new file mode 100644 index 0000000..0894353 --- /dev/null +++ b/game/assets/shaders/sprite.frag @@ -0,0 +1,11 @@ +#version 330 core +in vec2 v_texCoord; +in vec4 v_color; + +uniform sampler2D u_texture; + +out vec4 FragColor; + +void main() { + FragColor = texture(u_texture, v_texCoord) * v_color; +} diff --git a/game/assets/shaders/sprite.vert b/game/assets/shaders/sprite.vert new file mode 100644 index 0000000..fbc4e8a --- /dev/null +++ b/game/assets/shaders/sprite.vert @@ -0,0 +1,16 @@ +#version 330 core +layout (location = 0) in vec2 a_position; +layout (location = 1) in vec2 a_texCoord; +layout (location = 2) in vec4 a_color; + +uniform mat4 u_view; +uniform mat4 u_projection; + +out vec2 v_texCoord; +out vec4 v_color; + +void main() { + v_texCoord = a_texCoord; + v_color = a_color; + gl_Position = u_projection * u_view * vec4(a_position, 0.0, 1.0); +} diff --git a/game/docs/ARCHITECTURE.md b/game/docs/ARCHITECTURE.md new file mode 100644 index 0000000..575ddd6 --- /dev/null +++ b/game/docs/ARCHITECTURE.md @@ -0,0 +1,112 @@ +# Game Architecture + +This project keeps the game layer separate from the Frostbite2D engine. The +engine remains in `Frostbite2D/`; game-specific code lives under `game/`. + +## Directory Layout + +```text +game/ +├─ assets/ +│ ├─ character/ Runtime character spritesheets. +│ └─ shaders/ Runtime shaders used by the game target. +├─ docs/ +│ └─ ARCHITECTURE.md This document. +└─ src/ + ├─ actor/ Game actors such as player, enemy, projectile. + ├─ combat/ Hitbox, hurtbox, damage and combat resolution types. + ├─ core/ Shared config, input snapshots and asset paths. + ├─ data/ Level and gameplay data definitions. + ├─ movement/ Reusable movement and collision helpers. + ├─ scene/ Game scenes and level orchestration. + └─ main.cpp Game executable entry point. +``` + +## Runtime Flow + +```text +main.cpp + -> Application::init() + -> SceneManager::ReplaceScene(WhiteboxScene) + -> WhiteboxScene::onEnter() + -> CreateWhiteboxLevel() + -> create PlayerActor + -> pass PlatformWorld to PlayerActor + -> every frame: + -> PlayerActor samples InputState + -> PlayerActor moves through PlatformMover + -> PlayerActor updates animation + -> WhiteboxScene updates camera + -> WhiteboxScene draws whitebox level +``` + +## Module Responsibilities + +`core` + +- `game_config.h` stores shared tuning constants such as virtual resolution, + gravity, player speed and attack duration. +- `asset_paths.h` stores stable runtime asset paths. +- `input_state.*` converts raw SDL keyboard state into an engine-independent + `InputState` snapshot. Player logic consumes this snapshot instead of reading + SDL directly. + +`movement` + +- `PlatformWorld` owns level boundaries and platform rectangles. +- `MovementBody` is a reusable position/size/velocity container. +- `PlatformMover` applies gravity, clamps horizontal bounds and resolves basic + top-side platform landing. + +`data` + +- `LevelDefinition` groups collision and whitebox props. +- `CreateWhiteboxLevel()` currently returns the first hard-coded blockout level. + Later this can be replaced by JSON, Tiled, LDtk, or a custom editor export + without changing `PlayerActor`. + +`actor` + +- `PlayerActor` owns player sprites, player state and animation. +- It depends on `InputStateProvider` and `PlatformMover`, so input and movement + can be reused for enemies or changed independently. + +`scene` + +- `WhiteboxScene` orchestrates the current level: it loads level data, creates + actors, draws whitebox geometry and controls the camera. + +`combat` + +- `hitbox.h` defines early `Hitbox` and `Hurtbox` structs plus team filtering. + Full ACT combat should build from this instead of embedding hit detection in + actors. + +## Current Build Target + +`xmake.lua` defines `Frostbite2DGame` for Windows. It compiles all +`game/src/**.cpp` files, links against `Frostbite2D`, and copies `game/assets` +next to the executable after build. + +Build command: + +```powershell +xmake -b Frostbite2DGame +``` + +Output: + +```text +build/windows/x64/release/Frostbite2DGame.exe +``` + +## Next Architecture Steps + +1. Add `actor/enemy_actor.*` and reuse `PlatformMover`. +2. Add a `combat/CombatSystem` that collects active hitboxes and hurtboxes each + frame, resolves hits, and emits damage events. +3. Replace hard-coded animation timing in `PlayerActor` with data definitions. +4. Move `CreateWhiteboxLevel()` to an external level format once the first + whitebox loop is stable. +5. Add a `scene/gameplay_scene` base if multiple levels share spawn, camera, + combat and pause logic. diff --git a/game/src/actor/player_actor.cpp b/game/src/actor/player_actor.cpp new file mode 100644 index 0000000..6b1bd12 --- /dev/null +++ b/game/src/actor/player_actor.cpp @@ -0,0 +1,160 @@ +#include "player_actor.h" + +#include "../core/asset_paths.h" +#include "../core/game_config.h" + +#include +#include + +namespace ns_game { + +namespace { + +constexpr float kSpriteOffsetX = -32.0f; +constexpr float kSpriteOffsetY = -24.0f; + +} // namespace + +PlayerActor::PlayerActor() { + SetSize(64.0f, 104.0f); + SetTopLeftPosition(160.0f, 380.0f); + loadSprites(); +} + +void PlayerActor::SetMovementWorld(const PlatformWorld* world) { + movementWorld_ = world; +} + +void PlayerActor::loadSprites() { + idleSprite_ = frostbite2D::Sprite::createFromFile(assets::kMaleIdle); + runSprite_ = frostbite2D::Sprite::createFromFile(assets::kMaleRun); + punchSprite_ = frostbite2D::Sprite::createFromFile(assets::kMalePunch); + + auto configure = [this](frostbite2D::Ptr sprite) { + if (!sprite) { + return; + } + sprite->SetSize(config::kPlayerFrameSize, config::kPlayerFrameSize); + sprite->SetSourceRect(frostbite2D::Rect(0.0f, 0.0f, + config::kPlayerFrameSize, + config::kPlayerFrameSize)); + sprite->SetTopLeftPosition(kSpriteOffsetX, kSpriteOffsetY); + sprite->SetVisible(false); + AddChild(sprite); + }; + + configure(idleSprite_); + configure(runSprite_); + configure(punchSprite_); + setAnimState(AnimState::Idle); +} + +void PlayerActor::OnUpdate(float deltaTime) { + deltaTime = std::min(deltaTime, 1.0f / 30.0f); + updateInput(); + updatePhysics(deltaTime); + updateAnimation(deltaTime); +} + +void PlayerActor::updateInput() { + const InputState input = inputProvider_.Sample(); + velocity_.x = input.moveX * config::kPlayerMoveSpeed; + if (input.moveX < 0.0f) { + facingLeft_ = true; + } else if (input.moveX > 0.0f) { + facingLeft_ = false; + } + + if (input.jumpPressed && grounded_) { + velocity_.y = config::kPlayerJumpSpeed; + grounded_ = false; + } + + if (input.attackPressed) { + attackTimer_ = config::kAttackDuration; + setAnimState(AnimState::Punch); + } +} + +void PlayerActor::updatePhysics(float deltaTime) { + if (!movementWorld_) { + return; + } + + MovementBody body; + body.position = GetTopLeftPosition(); + body.size = GetSize(); + body.velocity = velocity_; + body.grounded = grounded_; + + body = platformMover_.Step(body, *movementWorld_, config::kGravity, deltaTime); + velocity_ = body.velocity; + grounded_ = body.grounded; + SetTopLeftPosition(body.position); +} + +void PlayerActor::updateAnimation(float deltaTime) { + if (attackTimer_ > 0.0f) { + attackTimer_ = std::max(0.0f, attackTimer_ - deltaTime); + setAnimState(AnimState::Punch); + } else if (std::abs(velocity_.x) > 1.0f) { + setAnimState(AnimState::Run); + } else { + setAnimState(AnimState::Idle); + } + + int frameCount = 10; + float frameDuration = 0.1f; + if (animState_ == AnimState::Idle) { + frameDuration = 0.16f; + } else if (animState_ == AnimState::Punch) { + frameDuration = 0.07f; + } + + animTimer_ += deltaTime; + while (animTimer_ >= frameDuration) { + animTimer_ -= frameDuration; + frameIndex_ = (frameIndex_ + 1) % frameCount; + } + + auto sprite = activeSprite(); + if (sprite) { + sprite->SetSourceRect( + frostbite2D::Rect(frameIndex_ * config::kPlayerFrameSize, 0.0f, + config::kPlayerFrameSize, + config::kPlayerFrameSize)); + sprite->SetFlippedX(facingLeft_); + } +} + +void PlayerActor::setAnimState(AnimState state) { + if (animState_ != state) { + animState_ = state; + animTimer_ = 0.0f; + frameIndex_ = 0; + } + + if (idleSprite_) { + idleSprite_->SetVisible(state == AnimState::Idle); + } + if (runSprite_) { + runSprite_->SetVisible(state == AnimState::Run); + } + if (punchSprite_) { + punchSprite_->SetVisible(state == AnimState::Punch); + } +} + +frostbite2D::Ptr PlayerActor::activeSprite() const { + switch (animState_) { + case AnimState::Run: + return runSprite_; + case AnimState::Punch: + return punchSprite_; + case AnimState::Idle: + default: + return idleSprite_; + } +} + +} // namespace ns_game diff --git a/game/src/actor/player_actor.h b/game/src/actor/player_actor.h new file mode 100644 index 0000000..c297414 --- /dev/null +++ b/game/src/actor/player_actor.h @@ -0,0 +1,49 @@ +#pragma once + +#include "../core/input_state.h" +#include "../movement/platform_world.h" + +#include +#include +#include + +namespace ns_game { + +class PlayerActor : public frostbite2D::Actor { +public: + PlayerActor(); + + void SetMovementWorld(const PlatformWorld* world); + void OnUpdate(float deltaTime) override; + +private: + enum class AnimState { + Idle, + Run, + Punch + }; + + void loadSprites(); + void updateInput(); + void updatePhysics(float deltaTime); + void updateAnimation(float deltaTime); + void setAnimState(AnimState state); + frostbite2D::Ptr activeSprite() const; + + const PlatformWorld* movementWorld_ = nullptr; + InputStateProvider inputProvider_; + PlatformMover platformMover_; + frostbite2D::Ptr idleSprite_; + frostbite2D::Ptr runSprite_; + frostbite2D::Ptr punchSprite_; + + frostbite2D::Vec2 velocity_; + AnimState animState_ = AnimState::Idle; + float animTimer_ = 0.0f; + float attackTimer_ = 0.0f; + int frameIndex_ = 0; + bool facingLeft_ = false; + bool grounded_ = false; +}; + +} // namespace ns_game diff --git a/game/src/combat/hitbox.h b/game/src/combat/hitbox.h new file mode 100644 index 0000000..1800de1 --- /dev/null +++ b/game/src/combat/hitbox.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace ns_game { + +enum class Team { + Player, + Enemy +}; + +struct Hitbox { + frostbite2D::Rect bounds; + Team ownerTeam = Team::Player; + int damage = 1; + float activeSeconds = 0.0f; +}; + +struct Hurtbox { + frostbite2D::Rect bounds; + Team ownerTeam = Team::Enemy; +}; + +inline bool CanHit(const Hitbox& hitbox, const Hurtbox& hurtbox) { + return hitbox.ownerTeam != hurtbox.ownerTeam && + hitbox.bounds.intersects(hurtbox.bounds); +} + +} // namespace ns_game diff --git a/game/src/core/asset_paths.h b/game/src/core/asset_paths.h new file mode 100644 index 0000000..a40d9f9 --- /dev/null +++ b/game/src/core/asset_paths.h @@ -0,0 +1,12 @@ +#pragma once + +namespace ns_game::assets { + +constexpr const char* kMaleIdle = + "assets/character/male/Male_spritesheet_idle.png"; +constexpr const char* kMaleRun = + "assets/character/male/Male_spritesheet_run.png"; +constexpr const char* kMalePunch = + "assets/character/male/Male_spritesheet_punch_1.png"; + +} // namespace ns_game::assets diff --git a/game/src/core/game_config.h b/game/src/core/game_config.h new file mode 100644 index 0000000..60933fb --- /dev/null +++ b/game/src/core/game_config.h @@ -0,0 +1,13 @@ +#pragma once + +namespace ns_game::config { + +constexpr int kVirtualWidth = 1280; +constexpr int kVirtualHeight = 720; +constexpr float kPlayerFrameSize = 128.0f; +constexpr float kPlayerMoveSpeed = 300.0f; +constexpr float kPlayerJumpSpeed = -620.0f; +constexpr float kGravity = 1800.0f; +constexpr float kAttackDuration = 0.28f; + +} // namespace ns_game::config diff --git a/game/src/core/input_state.cpp b/game/src/core/input_state.cpp new file mode 100644 index 0000000..2ef7fe4 --- /dev/null +++ b/game/src/core/input_state.cpp @@ -0,0 +1,40 @@ +#include "input_state.h" + +#include + +namespace ns_game { + +namespace { + +bool isDown(const Uint8* keys, SDL_Scancode key) { + return keys && keys[key] != 0; +} + +} // namespace + +InputState InputStateProvider::Sample() { + const Uint8* keys = SDL_GetKeyboardState(nullptr); + InputState state; + + if (isDown(keys, SDL_SCANCODE_A) || isDown(keys, SDL_SCANCODE_LEFT)) { + state.moveX -= 1.0f; + } + if (isDown(keys, SDL_SCANCODE_D) || isDown(keys, SDL_SCANCODE_RIGHT)) { + state.moveX += 1.0f; + } + + const bool jumpHeld = + isDown(keys, SDL_SCANCODE_SPACE) || isDown(keys, SDL_SCANCODE_W) || + isDown(keys, SDL_SCANCODE_UP); + const bool attackHeld = + isDown(keys, SDL_SCANCODE_J) || isDown(keys, SDL_SCANCODE_K); + + state.jumpPressed = jumpHeld && !jumpHeldLastFrame_; + state.attackPressed = attackHeld && !attackHeldLastFrame_; + + jumpHeldLastFrame_ = jumpHeld; + attackHeldLastFrame_ = attackHeld; + return state; +} + +} // namespace ns_game diff --git a/game/src/core/input_state.h b/game/src/core/input_state.h new file mode 100644 index 0000000..f6b5d0a --- /dev/null +++ b/game/src/core/input_state.h @@ -0,0 +1,20 @@ +#pragma once + +namespace ns_game { + +struct InputState { + float moveX = 0.0f; + bool jumpPressed = false; + bool attackPressed = false; +}; + +class InputStateProvider { +public: + InputState Sample(); + +private: + bool jumpHeldLastFrame_ = false; + bool attackHeldLastFrame_ = false; +}; + +} // namespace ns_game diff --git a/game/src/data/level_definition.h b/game/src/data/level_definition.h new file mode 100644 index 0000000..6df0029 --- /dev/null +++ b/game/src/data/level_definition.h @@ -0,0 +1,22 @@ +#pragma once + +#include "../movement/platform_world.h" +#include +#include +#include + +namespace ns_game { + +struct WhiteboxProp { + frostbite2D::Rect bounds; + frostbite2D::Color color; +}; + +struct LevelDefinition { + PlatformWorld collision; + std::vector props; +}; + +LevelDefinition CreateWhiteboxLevel(); + +} // namespace ns_game diff --git a/game/src/data/whitebox_level.cpp b/game/src/data/whitebox_level.cpp new file mode 100644 index 0000000..4a66e4c --- /dev/null +++ b/game/src/data/whitebox_level.cpp @@ -0,0 +1,27 @@ +#include "level_definition.h" + +namespace ns_game { + +LevelDefinition CreateWhiteboxLevel() { + LevelDefinition level; + level.collision.levelLeft = 0.0f; + level.collision.levelRight = 1600.0f; + level.collision.platforms = { + frostbite2D::Rect(0.0f, 600.0f, 1600.0f, 80.0f), + frostbite2D::Rect(230.0f, 500.0f, 220.0f, 28.0f), + frostbite2D::Rect(560.0f, 430.0f, 260.0f, 28.0f), + frostbite2D::Rect(930.0f, 510.0f, 260.0f, 28.0f), + frostbite2D::Rect(1250.0f, 420.0f, 230.0f, 28.0f), + }; + level.props = { + {frostbite2D::Rect(118.0f, 392.0f, 88.0f, 208.0f), + frostbite2D::Color(0.26f, 0.30f, 0.34f, 1.0f)}, + {frostbite2D::Rect(780.0f, 250.0f, 72.0f, 180.0f), + frostbite2D::Color(0.26f, 0.30f, 0.34f, 1.0f)}, + {frostbite2D::Rect(1440.0f, 300.0f, 80.0f, 300.0f), + frostbite2D::Color(0.26f, 0.30f, 0.34f, 1.0f)}, + }; + return level; +} + +} // namespace ns_game diff --git a/game/src/main.cpp b/game/src/main.cpp new file mode 100644 index 0000000..b864ae6 --- /dev/null +++ b/game/src/main.cpp @@ -0,0 +1,41 @@ +#include "scene/whitebox_scene.h" +#include "core/game_config.h" + +#include + +#define SDL_MAIN_HANDLED +#include +#include + +int main(int argc, char** argv) { + (void)argc; + (void)argv; + + auto config = frostbite2D::AppConfig::createDefault(); + config.appName = "NS Unknown Game"; + config.windowConfig.title = "NS Unknown Game - Whitebox"; + config.windowConfig.width = 1280; + config.windowConfig.height = 720; + config.windowConfig.resizable = true; + config.windowConfig.vsync = true; + config.useVirtualResolution = true; + config.virtualWidth = ns_game::config::kVirtualWidth; + config.virtualHeight = ns_game::config::kVirtualHeight; + config.resolutionMode = frostbite2D::ResolutionScaleMode::Fit; + config.renderStyleProfile = frostbite2D::RenderStyleProfileId::PixelArt2D; + config.maxFps = 60; + + auto& app = frostbite2D::Application::get(); + if (!app.init(config)) { + std::puts("NS Unknown Game init failed."); + return 1; + } + + app.run([]() { + frostbite2D::SceneManager::get().ReplaceScene( + frostbite2D::MakePtr()); + }); + + app.shutdown(); + return 0; +} diff --git a/game/src/movement/platform_world.cpp b/game/src/movement/platform_world.cpp new file mode 100644 index 0000000..f2a08bd --- /dev/null +++ b/game/src/movement/platform_world.cpp @@ -0,0 +1,39 @@ +#include "platform_world.h" + +#include + +namespace ns_game { + +MovementBody PlatformMover::Step(const MovementBody& body, + const PlatformWorld& world, float gravity, + float deltaTime) const { + MovementBody next = body; + const frostbite2D::Vec2 previousPosition = next.position; + + next.velocity.y += gravity * deltaTime; + next.position += next.velocity * deltaTime; + next.grounded = false; + + const float previousBottom = previousPosition.y + next.size.y; + const float newBottom = next.position.y + next.size.y; + + for (const auto& platform : world.platforms) { + const bool horizontallyOverlaps = + next.position.x + next.size.x > platform.left() && + next.position.x < platform.right(); + const bool wasAbove = previousBottom <= platform.top() + 4.0f; + const bool crossedTop = newBottom >= platform.top(); + if (next.velocity.y >= 0.0f && horizontallyOverlaps && wasAbove && + crossedTop) { + next.position.y = platform.top() - next.size.y; + next.velocity.y = 0.0f; + next.grounded = true; + } + } + + next.position.x = + std::clamp(next.position.x, world.levelLeft, world.levelRight - next.size.x); + return next; +} + +} // namespace ns_game diff --git a/game/src/movement/platform_world.h b/game/src/movement/platform_world.h new file mode 100644 index 0000000..774cdb6 --- /dev/null +++ b/game/src/movement/platform_world.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +namespace ns_game { + +struct PlatformWorld { + float levelLeft = 0.0f; + float levelRight = 1280.0f; + std::vector platforms; +}; + +struct MovementBody { + frostbite2D::Vec2 position; + frostbite2D::Vec2 size; + frostbite2D::Vec2 velocity; + bool grounded = false; +}; + +class PlatformMover { +public: + MovementBody Step(const MovementBody& body, const PlatformWorld& world, + float gravity, float deltaTime) const; +}; + +} // namespace ns_game diff --git a/game/src/scene/whitebox_scene.cpp b/game/src/scene/whitebox_scene.cpp new file mode 100644 index 0000000..38d255f --- /dev/null +++ b/game/src/scene/whitebox_scene.cpp @@ -0,0 +1,84 @@ +#include "whitebox_scene.h" + +#include "../core/game_config.h" + +#include +#include +#include +#include + +namespace ns_game { + +namespace { + +void drawRect(const frostbite2D::Rect& rect, const frostbite2D::Color& color) { + frostbite2D::Renderer::get().drawQuad(rect, color); +} + +} // namespace + +void WhiteboxScene::onEnter() { + frostbite2D::Scene::onEnter(); + level_ = CreateWhiteboxLevel(); + + player_ = frostbite2D::MakePtr(); + player_->SetMovementWorld(&level_.collision); + AddChild(player_); +} + +void WhiteboxScene::OnUpdate(float deltaTime) { + (void)deltaTime; + updateCamera(); +} + +void WhiteboxScene::Render() { + drawWhitebox(); + frostbite2D::Scene::Render(); +} + +void WhiteboxScene::drawWhitebox() const { + const float worldWidth = level_.collision.levelRight - level_.collision.levelLeft; + drawRect(frostbite2D::Rect(level_.collision.levelLeft, 0.0f, worldWidth, + config::kVirtualHeight), + frostbite2D::Color(0.10f, 0.12f, 0.14f, 1.0f)); + + for (float x = level_.collision.levelLeft; x <= level_.collision.levelRight; + x += 80.0f) { + drawRect(frostbite2D::Rect(x, 0.0f, 1.0f, config::kVirtualHeight), + frostbite2D::Color(0.20f, 0.22f, 0.25f, 1.0f)); + } + for (float y = 0.0f; y <= config::kVirtualHeight; y += 80.0f) { + drawRect(frostbite2D::Rect(level_.collision.levelLeft, y, worldWidth, 1.0f), + frostbite2D::Color(0.20f, 0.22f, 0.25f, 1.0f)); + } + + for (const auto& prop : level_.props) { + drawRect(prop.bounds, prop.color); + } + + for (const auto& platform : level_.collision.platforms) { + drawRect(platform, frostbite2D::Color(0.62f, 0.68f, 0.73f, 1.0f)); + drawRect(frostbite2D::Rect(platform.left(), platform.top(), platform.width(), 4.0f), + frostbite2D::Color(0.91f, 0.95f, 0.98f, 1.0f)); + } +} + +void WhiteboxScene::updateCamera() { + if (!player_) { + return; + } + + auto* renderer = frostbite2D::Application::get().getRenderer(); + if (!renderer || !renderer->getCamera()) { + return; + } + + const auto playerPos = player_->GetTopLeftPosition(); + const float targetX = std::clamp(playerPos.x - 420.0f, + level_.collision.levelLeft, + level_.collision.levelRight - + static_cast(config::kVirtualWidth)); + renderer->getCamera()->setPosition(frostbite2D::Vec2(targetX, 0.0f)); +} + +} // namespace ns_game diff --git a/game/src/scene/whitebox_scene.h b/game/src/scene/whitebox_scene.h new file mode 100644 index 0000000..e9ee2cb --- /dev/null +++ b/game/src/scene/whitebox_scene.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../actor/player_actor.h" +#include "../data/level_definition.h" + +#include +#include + +namespace ns_game { + +class WhiteboxScene : public frostbite2D::Scene { +public: + void onEnter() override; + void OnUpdate(float deltaTime) override; + void Render() override; + +private: + void drawWhitebox() const; + void updateCamera(); + + LevelDefinition level_; + frostbite2D::Ptr player_; +}; + +} // namespace ns_game diff --git a/xmake.lua b/xmake.lua index 93d4877..424cc3b 100644 --- a/xmake.lua +++ b/xmake.lua @@ -31,4 +31,21 @@ if is_plat("windows") then add_packages("zlib") add_syslinks("imm32") target_end() + + target("Frostbite2DGame") + set_kind("binary") + add_files("game/src/**.cpp") + add_deps("Frostbite2D") + add_packages("libsdl2") + add_packages("libsdl2_image") + add_packages("libsdl2_mixer") + add_packages("libsdl2_ttf") + add_packages("glm") + add_packages("zlib") + add_syslinks("imm32") + after_build(function (target) + os.cp(path.join(os.projectdir(), "game/assets"), + path.join(target:targetdir(), "assets")) + end) + target_end() end