Add game framework and whitebox level
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
#version 330 core
|
||||||
|
in vec4 v_color;
|
||||||
|
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
FragColor = v_color;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"shaders": {
|
||||||
|
"sprite": {
|
||||||
|
"vertex": "sprite.vert",
|
||||||
|
"fragment": "sprite.frag"
|
||||||
|
},
|
||||||
|
"colored_quad": {
|
||||||
|
"vertex": "colored_quad.vert",
|
||||||
|
"fragment": "colored_quad.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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#include "player_actor.h"
|
||||||
|
|
||||||
|
#include "../core/asset_paths.h"
|
||||||
|
#include "../core/game_config.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
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<frostbite2D::Sprite> 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<frostbite2D::Sprite> PlayerActor::activeSprite() const {
|
||||||
|
switch (animState_) {
|
||||||
|
case AnimState::Run:
|
||||||
|
return runSprite_;
|
||||||
|
case AnimState::Punch:
|
||||||
|
return punchSprite_;
|
||||||
|
case AnimState::Idle:
|
||||||
|
default:
|
||||||
|
return idleSprite_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../core/input_state.h"
|
||||||
|
#include "../movement/platform_world.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/2d/actor.h>
|
||||||
|
#include <frostbite2D/2d/sprite.h>
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
|
||||||
|
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<frostbite2D::Sprite> activeSprite() const;
|
||||||
|
|
||||||
|
const PlatformWorld* movementWorld_ = nullptr;
|
||||||
|
InputStateProvider inputProvider_;
|
||||||
|
PlatformMover platformMover_;
|
||||||
|
frostbite2D::Ptr<frostbite2D::Sprite> idleSprite_;
|
||||||
|
frostbite2D::Ptr<frostbite2D::Sprite> runSprite_;
|
||||||
|
frostbite2D::Ptr<frostbite2D::Sprite> 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
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#include "input_state.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../movement/platform_world.h"
|
||||||
|
#include <frostbite2D/types/type_color.h>
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
struct WhiteboxProp {
|
||||||
|
frostbite2D::Rect bounds;
|
||||||
|
frostbite2D::Color color;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LevelDefinition {
|
||||||
|
PlatformWorld collision;
|
||||||
|
std::vector<WhiteboxProp> props;
|
||||||
|
};
|
||||||
|
|
||||||
|
LevelDefinition CreateWhiteboxLevel();
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#include "scene/whitebox_scene.h"
|
||||||
|
#include "core/game_config.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#define SDL_MAIN_HANDLED
|
||||||
|
#include <frostbite2D/core/application.h>
|
||||||
|
#include <frostbite2D/scene/scene_manager.h>
|
||||||
|
|
||||||
|
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<ns_game::WhiteboxScene>());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.shutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#include "platform_world.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ns_game {
|
||||||
|
|
||||||
|
struct PlatformWorld {
|
||||||
|
float levelLeft = 0.0f;
|
||||||
|
float levelRight = 1280.0f;
|
||||||
|
std::vector<frostbite2D::Rect> 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
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#include "whitebox_scene.h"
|
||||||
|
|
||||||
|
#include "../core/game_config.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/core/application.h>
|
||||||
|
#include <frostbite2D/graphics/camera.h>
|
||||||
|
#include <frostbite2D/graphics/renderer.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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<PlayerActor>();
|
||||||
|
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<float>(config::kVirtualWidth));
|
||||||
|
renderer->getCamera()->setPosition(frostbite2D::Vec2(targetX, 0.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../actor/player_actor.h"
|
||||||
|
#include "../data/level_definition.h"
|
||||||
|
|
||||||
|
#include <frostbite2D/scene/scene.h>
|
||||||
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
|
||||||
|
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<PlayerActor> player_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ns_game
|
||||||
@@ -31,4 +31,21 @@ if is_plat("windows") then
|
|||||||
add_packages("zlib")
|
add_packages("zlib")
|
||||||
add_syslinks("imm32")
|
add_syslinks("imm32")
|
||||||
target_end()
|
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
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user