Merge branch 'master' of mx.cogentleman.com:cogentleman/guitarHeroButBetter

This commit is contained in:
Joseph DiMaria 2026-01-31 00:28:06 -08:00
commit 741d2c9392
19 changed files with 413 additions and 1705 deletions

View File

@ -46,6 +46,7 @@ class FightingCharacter(GameObject):
self.jump_sound: SoundComponent = None # type: ignore[assignment] self.jump_sound: SoundComponent = None # type: ignore[assignment]
self.hit_sound: SoundComponent = None # type: ignore[assignment] self.hit_sound: SoundComponent = None # type: ignore[assignment]
self.die_sound: SoundComponent = None # type: ignore[assignment] self.die_sound: SoundComponent = None # type: ignore[assignment]
self.guitar_hero_components: None
self.fall_through = False self.fall_through = False
self.fall_through_timer = 0.0 self.fall_through_timer = 0.0
self.fall_through_duration = 0.2 self.fall_through_duration = 0.2
@ -95,6 +96,17 @@ class FightingCharacter(GameObject):
self.hit_sound = self.sounds.add_component("hit", SoundComponent, "assets/sounds/hit.wav") self.hit_sound = self.sounds.add_component("hit", SoundComponent, "assets/sounds/hit.wav")
self.die_sound = self.sounds.add_component("die", SoundComponent, "assets/sounds/die.wav") self.die_sound = self.sounds.add_component("die", SoundComponent, "assets/sounds/die.wav")
# TODO, james add more buttons
guitar_hero_key_bindings = {
rl.GAMEPAD_BUTTON_RIGHT_FACE_UP: "assets/sounds/nes_harp/nes_harp_A4.wav",
rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN: "assets/sounds/nes_harp/nes_harp_C4.wav",
rl.GAMEPAD_BUTTON_RIGHT_FACE_LEFT: "assets/sounds/nes_harp/nes_harp_D4.wav",
rl.GAMEPAD_BUTTON_RIGHT_FACE_RIGHT: "assets/sounds/nes_harp/nes_harp_G4.wav",
}
self.guitar_hero_components = {}
for key_press, wav_file in guitar_hero_key_bindings.items():
self.guitar_hero_components[key_press] = self.sounds.add_component(key_press, SoundComponent, wav_file)
self.animation = self.add_component(AnimationController(self.body)) self.animation = self.add_component(AnimationController(self.body))
if self.player_number == 1: if self.player_number == 1:
self.animation.add_animation_from_files("run", self.animation.add_animation_from_files("run",
@ -188,6 +200,20 @@ class FightingCharacter(GameObject):
Returns: Returns:
None None
""" """
# Add more buttons here
gamepad_buttons = [
rl.GAMEPAD_BUTTON_RIGHT_FACE_UP,
rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN,
rl.GAMEPAD_BUTTON_RIGHT_FACE_LEFT,
rl.GAMEPAD_BUTTON_RIGHT_FACE_RIGHT,
]
for button in gamepad_buttons:
if rl.is_gamepad_button_pressed(self.gamepad, button):
self.guitar_hero_components[button].play()
return
deadzone = 0.1 deadzone = 0.1
jump_pressed = rl.is_key_pressed(rl.KEY_W) or rl.is_gamepad_button_pressed(self.gamepad, rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN) jump_pressed = rl.is_key_pressed(rl.KEY_W) or rl.is_gamepad_button_pressed(self.gamepad, rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN)
jump_held = rl.is_key_down(rl.KEY_W) or rl.is_gamepad_button_down(self.gamepad, rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN) jump_held = rl.is_key_down(rl.KEY_W) or rl.is_gamepad_button_down(self.gamepad, rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN)
@ -276,6 +302,7 @@ class FightingCharacter(GameObject):
Returns: Returns:
True to enable contact, False to disable it. True to enable contact, False to disable it.
""" """
return True
normal = contact.worldManifold.normal normal = contact.worldManifold.normal
other = None other = None
sign = 0.0 sign = 0.0
@ -375,7 +402,7 @@ class FightingScene(Scene):
self.physics.world.contactListener = FightingContactListener(self) self.physics.world.contactListener = FightingContactListener(self)
player_entities = self.level.get_entities_by_name("Start") player_entities = self.level.get_entities_by_name("Start")
for i, player_entity in enumerate(player_entities[:4]): for i, player_entity in enumerate(player_entities[:1]):
params = CharacterParams() params = CharacterParams()
params.position = self.level.convert_to_pixels(player_entity.getPosition()) params.position = self.level.convert_to_pixels(player_entity.getPosition())
params.width = 16 params.width = 16

View File

@ -1,7 +1,5 @@
#include "samples/collecting_game.h" #include "samples/ghhb_game.h"
#include "samples/fighting_game.h"
#include "samples/title_screen.h" #include "samples/title_screen.h"
#include "samples/zombie_game.h"
// Emscripten is used for web builds. // Emscripten is used for web builds.
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
@ -19,8 +17,7 @@ void update()
// TODO: Make this a GUI app for each platform. // TODO: Make this a GUI app for each platform.
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
// Initialize the window game.add_manager<WindowManager>(1280, 720, "Guitar Hero But Better");
game.add_manager<WindowManager>(1280, 720, "Game Jam Kit");
auto font_manager = game.add_manager<FontManager>(); auto font_manager = game.add_manager<FontManager>();
game.init(); game.init();
@ -30,11 +27,8 @@ int main(int argc, char** argv)
font_manager->set_texture_filter("Roboto", TEXTURE_FILTER_BILINEAR); font_manager->set_texture_filter("Roboto", TEXTURE_FILTER_BILINEAR);
game.add_scene<TitleScreen>("title"); game.add_scene<TitleScreen>("title");
game.add_scene<FightingScene>("fighting"); game.add_scene<GHHBScene>("ghhb");
game.add_scene<CollectingScene>("collecting");
game.add_scene<ZombieScene>("zombie");
// Main game loop
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
emscripten_set_main_loop(update, 0, true); emscripten_set_main_loop(update, 0, true);
#else #else

View File

@ -1,577 +0,0 @@
/**
* Demonstration of split screen camera system and sensors for collecting items.
*/
#pragma once
#include "engine/prefabs/includes.h"
/**
* A basic collecting character.
* See also engine/prefabs/game_objects.h for PlatformerCharacter.
*/
class CollectingCharacter : public GameObject
{
public:
CharacterParams p;
PhysicsService* physics;
LevelService* level;
BodyComponent* body;
PlatformerMovementComponent* movement;
AnimationController* animation;
MultiComponent<SoundComponent>* sounds;
SoundComponent* jump_sound;
SoundComponent* die_sound;
int score = 0;
bool grounded = false;
bool on_wall_left = false;
bool on_wall_right = false;
float coyote_timer = 0.0f;
float jump_buffer_timer = 0.0f;
int gamepad = 0;
int player_number = 1;
float width = 24.0f;
float height = 24.0f;
CollectingCharacter(CharacterParams p, int player_number = 1) :
p(p),
player_number(player_number),
gamepad(player_number - 1),
width(p.width),
height(p.height)
{
}
void init() override
{
// Grab the physics service.
// All get_service calls should be done in init(). get_service is not quick and this also allows us to test
// that all services exist during init time.
physics = scene->get_service<PhysicsService>();
// Setup the character's physics body using the BodyComponent initialization callback lambda.
body = add_component<BodyComponent>(
[=](BodyComponent& b)
{
b2BodyDef body_def = b2DefaultBodyDef();
body_def.type = b2_dynamicBody;
body_def.fixedRotation = true;
body_def.isBullet = true;
// All units in box2d are in meters.
body_def.position = physics->convert_to_meters(p.position);
// Assign this GameObject as the user data so we can find it in collision callbacks.
body_def.userData = this;
b.id = b2CreateBody(physics->world, &body_def);
b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial();
body_material.friction = p.friction;
body_material.restitution = p.restitution;
b2ShapeDef box_shape_def = b2DefaultShapeDef();
box_shape_def.density = p.density;
box_shape_def.material = body_material;
// Needed to get sensor events.
box_shape_def.enableSensorEvents = true;
// We use a rounded box which helps with getting stuck on edges.
b2Polygon body_polygon = b2MakeRoundedBox(physics->convert_to_meters(p.width / 2.0f),
physics->convert_to_meters(p.height / 2.0f),
physics->convert_to_meters(0.25));
b2CreatePolygonShape(b.id, &box_shape_def, &body_polygon);
});
PlatformerMovementParams mp;
mp.width = p.width;
mp.height = p.height;
movement = add_component<PlatformerMovementComponent>(mp);
level = scene->get_service<LevelService>();
// TODO: Is only allowing one component per type really as cool an idea as I thought?
sounds = add_component<MultiComponent<SoundComponent>>();
jump_sound = sounds->add_component("jump", "assets/sounds/jump.wav");
die_sound = sounds->add_component("die", "assets/sounds/die.wav");
// Setup animations.
animation = add_component<AnimationController>(body);
if (player_number == 1)
{
animation->add_animation("run",
std::vector<std::string>{"assets/pixel_platformer/characters/green_1.png",
"assets/pixel_platformer/characters/green_2.png"},
10.0f);
}
else if (player_number == 2)
{
animation->add_animation("run",
std::vector<std::string>{"assets/pixel_platformer/characters/blue_1.png",
"assets/pixel_platformer/characters/blue_2.png"},
10.0f);
}
else if (player_number == 3)
{
animation->add_animation("run",
std::vector<std::string>{"assets/pixel_platformer/characters/pink_1.png",
"assets/pixel_platformer/characters/pink_2.png"},
10.0f);
}
else if (player_number == 4)
{
animation->add_animation("run",
std::vector<std::string>{"assets/pixel_platformer/characters/yellow_1.png",
"assets/pixel_platformer/characters/yellow_2.png"},
10.0f);
}
}
void update(float delta_time) override
{
// Get input and route to movement component.
float deadzone = 0.1f;
const bool jump_pressed =
IsKeyPressed(KEY_W) || IsGamepadButtonPressed(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN);
const bool jump_held = IsKeyDown(KEY_W) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN);
float move_x = 0.0f;
move_x = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_X);
if (fabsf(move_x) < deadzone)
{
move_x = 0.0f;
}
if (IsKeyDown(KEY_D) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_RIGHT))
{
move_x = 1.0f;
}
else if (IsKeyDown(KEY_A) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_LEFT))
{
move_x = -1.0f;
}
movement->set_input(move_x, jump_pressed, jump_held);
if (movement->grounded && jump_pressed)
{
jump_sound->play();
}
if (fabsf(movement->move_x) > 0.1f)
{
animation->play("run");
animation->flip_x = movement->move_x > 0.0f;
}
else
{
animation->pause();
}
}
void die()
{
// Re-spawn at start position.
body->set_position(p.position);
body->set_velocity(Vector2{0.0f, 0.0f});
die_sound->play();
}
};
enum EnemyType
{
Bat,
DrillHead,
BlockHead
};
/**
* An enemy.
*/
class Enemy : public GameObject
{
public:
Vector2 start;
Vector2 end;
PhysicsService* physics;
BodyComponent* body;
AnimationController* animation;
EnemyType type;
float radius = 12.0f;
Enemy(EnemyType type, Vector2 start, Vector2 end) : type(type), start(start), end(end) {}
void init_object() override
{
physics = scene->get_service<PhysicsService>();
body = add_component<BodyComponent>(
[=](BodyComponent& b)
{
b2BodyDef body_def = b2DefaultBodyDef();
body_def.type = b2_kinematicBody;
body_def.position = physics->convert_to_meters(start);
body_def.userData = this;
b.id = b2CreateBody(physics->world, &body_def);
b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial();
b2ShapeDef circle_shape_def = b2DefaultShapeDef();
circle_shape_def.density = 1.0f;
circle_shape_def.material = body_material;
circle_shape_def.isSensor = true;
circle_shape_def.enableSensorEvents = true;
b2Circle circle_shape = {b2Vec2_zero, physics->convert_to_meters(radius)};
b2CreateCircleShape(b.id, &circle_shape_def, &circle_shape);
});
animation = add_component<AnimationController>(body);
if (type == Bat)
{
animation->add_animation("move",
std::vector<std::string>{"assets/pixel_platformer/enemies/bat_1.png",
"assets/pixel_platformer/enemies/bat_2.png",
"assets/pixel_platformer/enemies/bat_3.png"},
5.0f);
}
else if (type == DrillHead)
{
animation->add_animation("move",
std::vector<std::string>{"assets/pixel_platformer/enemies/drill_head_1.png",
"assets/pixel_platformer/enemies/drill_head_2.png"},
5.0f);
}
else if (type == BlockHead)
{
animation->add_animation("move",
std::vector<std::string>{"assets/pixel_platformer/enemies/block_head_1.png",
"assets/pixel_platformer/enemies/block_head_2.png"},
5.0f);
}
animation->play("move");
GameObject::init_object();
// Start moving towards end position.
Vector2 to_end = end - body->get_position_pixels();
to_end = Vector2Normalize(to_end);
body->set_velocity(to_end * 50.0f);
}
void update(float delta_time) override
{
b2Circle circle_shape = {body->get_position_meters(), physics->convert_to_meters(radius * 2.0f)};
if (b2PointInCircle(physics->convert_to_meters(end), &circle_shape))
{
// Move towards start position.
Vector2 to_start = start - body->get_position_pixels();
to_start = Vector2Normalize(to_start);
body->set_velocity(to_start * 50.0f);
}
else if (b2PointInCircle(physics->convert_to_meters(start), &circle_shape))
{
// Move towards end position.
Vector2 to_end = end - body->get_position_pixels();
to_end = Vector2Normalize(to_end);
body->set_velocity(to_end * 50.0f);
}
// Check for collisions.
auto sensor_contacts = body->get_sensor_overlaps();
for (auto contact_body_id : sensor_contacts)
{
auto user_data = static_cast<GameObject*>(b2Body_GetUserData(contact_body_id));
if (user_data && user_data->has_tag("character"))
{
// Hit player.
CollectingCharacter* character = static_cast<CollectingCharacter*>(user_data);
character->die();
}
}
// Flip based on velocity.
Vector2 velocity = body->get_velocity_pixels();
animation->flip_x = velocity.x > 0.0f;
}
};
/**
* A collectible coin.
*/
class Coin : public GameObject
{
public:
Vector2 position;
PhysicsService* physics;
BodyComponent* body;
AnimationController* animation;
SoundComponent* collect_sound;
Coin(Vector2 position) : position(position) {}
void init() override
{
physics = scene->get_service<PhysicsService>();
body = add_component<BodyComponent>(
[=](BodyComponent& b)
{
b2BodyDef body_def = b2DefaultBodyDef();
body_def.type = b2_staticBody;
body_def.position = physics->convert_to_meters(position);
body_def.userData = this;
b.id = b2CreateBody(physics->world, &body_def);
b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial();
b2ShapeDef circle_shape_def = b2DefaultShapeDef();
circle_shape_def.density = 1.0f;
circle_shape_def.material = body_material;
circle_shape_def.isSensor = true;
circle_shape_def.enableSensorEvents = true;
b2Circle circle_shape = {b2Vec2_zero, physics->convert_to_meters(8.0f)};
b2CreateCircleShape(b.id, &circle_shape_def, &circle_shape);
});
animation = add_component<AnimationController>(body);
animation->add_animation("spin",
std::vector<std::string>{"assets/pixel_platformer/items/coin_1.png",
"assets/pixel_platformer/items/coin_2.png"},
5.0f);
animation->play("spin");
collect_sound = add_component<SoundComponent>("assets/sounds/coin.wav");
}
void update(float delta_time) override
{
auto sensor_contacts = body->get_sensor_overlaps();
for (auto contact_body_id : sensor_contacts)
{
auto user_data = static_cast<GameObject*>(b2Body_GetUserData(contact_body_id));
if (user_data && user_data->has_tag("character"))
{
// Collected by character.
collect_sound->play();
// Disable the coin.
is_active = false;
body->disable();
// Increase the score on the character.
CollectingCharacter* character = static_cast<CollectingCharacter*>(user_data);
character->score += 1;
break;
}
}
}
};
/**
* A collecting game scene.
*/
class CollectingScene : public Scene
{
public:
WindowManager* window_manager;
FontManager* font_manager;
std::vector<std::shared_ptr<CollectingCharacter>> characters;
LevelService* level;
PhysicsService* physics;
std::vector<std::shared_ptr<SplitCamera>> cameras;
Vector2 screen_size;
float scale = 2.5f;
void init_services() override
{
// TextureService and SoundService are needed by other components and game objects.
add_service<TextureService>();
add_service<SoundService>();
// PhysicsService is used by LevelService and must be added first.
physics = add_service<PhysicsService>();
// Setup LDtk level. Checkout the file in LDtk editor to see how it's built.
std::vector<std::string> collision_names = {"walls", "clouds", "trees"};
level = add_service<LevelService>("assets/levels/collecting.ldtk", "Level", collision_names);
}
void init() override
{
window_manager = game->get_manager<WindowManager>();
font_manager = game->get_manager<FontManager>();
const auto& entities_layer = level->get_layer_by_name("Entities");
// Create player characters at the "Start" entities.
auto player_entities = level->get_entities_by_name("Start");
for (int i = 0; i < player_entities.size() && i < 4; i++)
{
auto& player_entity = player_entities[i];
CharacterParams params;
params.position = level->convert_to_pixels(player_entity->getPosition());
params.width = 16;
params.height = 24;
auto character = add_game_object<CollectingCharacter>(params, i + 1);
character->add_tag("character");
characters.push_back(character);
}
// Create enemies at the each enemy entity.
auto bat_entities = level->get_entities_by_name("Bat");
for (auto& bat_entity : bat_entities)
{
auto start_point = bat_entity->getPosition();
Vector2 start_position = level->convert_to_pixels(start_point);
ldtk::IntPoint end_point = bat_entity->getField<ldtk::IntPoint>("end").value();
// Annoyingly, Point fields in LDtk are in cell coordinates rather than pixel coordinates, and the cell
// size is dependent on the layer.
Vector2 end_position = level->convert_cells_to_pixels(end_point, entities_layer);
auto enemy = add_game_object<Enemy>(EnemyType::Bat, start_position, end_position);
enemy->add_tag("enemy");
}
auto drill_entities = level->get_entities_by_name("DrillHead");
for (auto& drill_entity : drill_entities)
{
auto start_point = drill_entity->getPosition();
Vector2 start_position = level->convert_to_pixels(start_point);
ldtk::IntPoint end_point = drill_entity->getField<ldtk::IntPoint>("end").value();
Vector2 end_position = level->convert_cells_to_pixels(end_point, entities_layer);
auto enemy = add_game_object<Enemy>(EnemyType::DrillHead, start_position, end_position);
enemy->add_tag("enemy");
}
auto block_entities = level->get_entities_by_name("BlockHead");
for (auto& block_entity : block_entities)
{
auto start_point = block_entity->getPosition();
Vector2 start_position = level->convert_to_pixels(start_point);
ldtk::IntPoint end_point = block_entity->getField<ldtk::IntPoint>("end").value();
Vector2 end_position = level->convert_cells_to_pixels(end_point, entities_layer);
auto enemy = add_game_object<Enemy>(EnemyType::BlockHead, start_position, end_position);
enemy->add_tag("enemy");
}
// Create coins at the "Coin" entities.
auto coin_entities = level->get_entities_by_name("Coin");
for (auto& coin_entity : coin_entities)
{
Vector2 coin_position = level->convert_to_pixels(coin_entity->getPosition());
auto coin = add_game_object<Coin>(coin_position);
coin->add_tag("coin");
}
// Setup cameras.
screen_size =
Vector2{static_cast<float>(window_manager->get_width()), static_cast<float>(window_manager->get_height())};
for (int i = 0; i < characters.size(); i++)
{
auto cam = add_game_object<SplitCamera>(screen_size / scale, level->get_size());
cameras.push_back(cam);
}
}
void update(float delta_time) override
{
// Set the camera target to follow each character.
for (int i = 0; i < cameras.size(); i++)
{
cameras[i]->target = characters[i]->body->get_position_pixels();
}
auto new_screen_size = Vector2{static_cast<float>(GetScreenWidth()), static_cast<float>(GetScreenHeight())};
if (new_screen_size != screen_size)
{
// Window resized, update cameras.
screen_size = new_screen_size;
// Scale the cameras so they are zoomed in when drawn.
float screen_scale = window_manager->get_width() / screen_size.x;
for (auto camera : cameras)
{
camera->size = screen_size / scale * screen_scale;
camera->camera.offset = {camera->size.x / 2.0f, camera->size.y / 2.0f};
UnloadRenderTexture(camera->renderer);
camera->renderer = LoadRenderTexture((int)camera->size.x, (int)camera->size.y);
}
}
// Trigger scene change on Enter key or gamepad start button.
if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT))
{
game->go_to_scene_next();
}
}
/**
* We override draw_scene instead of draw to control when Scene::draw_scene is called.
* This allows us to draw the scene inside the camera's Begin/End block and render the camera to a texture.
*/
void draw_scene() override
{
// The scene needs to be rendered once per camera.
for (auto camera : cameras)
{
camera->draw_begin();
Scene::draw_scene();
// physics->draw_debug();
// camera->draw_debug();
camera->draw_end();
}
// Draw the cameras.
ClearBackground(MAGENTA);
// Draw each camera's texture side by side.
for (int i = 0; i < cameras.size(); i++)
{
if (i == 0)
{
cameras[i]->draw_texture_pro(0, 0, screen_size.x / 2.0f, screen_size.y / 2.0f);
DrawTextEx(font_manager->get_font("Tiny5"),
TextFormat("Score: %d", characters[0]->score),
Vector2{20.0f, 20.0f},
40.0f,
2.0f,
BLACK);
}
else if (i == 1)
{
cameras[i]->draw_texture_pro(screen_size.x / 2.0f, 0, screen_size.x / 2.0f, screen_size.y / 2.0f);
DrawTextEx(font_manager->get_font("Tiny5"),
TextFormat("Score: %d", characters[1]->score),
Vector2{screen_size.x / 2.0f + 20.0f, 20.0f},
40.0f,
2.0f,
BLACK);
}
else if (i == 2)
{
cameras[i]->draw_texture_pro(0, screen_size.y / 2.0f, screen_size.x / 2.0f, screen_size.y / 2.0f);
DrawTextEx(font_manager->get_font("Tiny5"),
TextFormat("Score: %d", characters[2]->score),
Vector2{20.0f, screen_size.y / 2.0f + 20.0f},
40.0f,
2.0f,
BLACK);
}
else if (i == 3)
{
cameras[i]->draw_texture_pro(screen_size.x / 2.0f,
screen_size.y / 2.0f,
screen_size.x / 2.0f,
screen_size.y / 2.0f);
DrawTextEx(font_manager->get_font("Tiny5"),
TextFormat("Score: %d", characters[3]->score),
Vector2{screen_size.x / 2.0f + 20.0f, screen_size.y / 2.0f + 20.0f},
40.0f,
2.0f,
BLACK);
}
}
// Draw split lines.
DrawLineEx(Vector2{screen_size.x / 2.0f, 0}, Vector2{screen_size.x / 2.0f, screen_size.y}, 4.0f, GRAY);
DrawLineEx(Vector2{0, screen_size.y / 2.0f}, Vector2{screen_size.x, screen_size.y / 2.0f}, 4.0f, GRAY);
}
};

View File

@ -1,564 +0,0 @@
/**
* Demonstration of a shared camera, multiple characters, and basic fighting mechanics.
* Shows how to setup a level with the LevelService and physics bodies with the PhysicsService.
* Also shows how to use animations and sounds.
*/
#pragma once
#include "engine/prefabs/includes.h"
/**
* A basic fighting character.
* See also engine/prefabs/game_objects.h for PlatformerCharacter.
*/
class FightingCharacter : public GameObject
{
public:
CharacterParams p;
PhysicsService* physics;
LevelService* level;
BodyComponent* body;
PlatformerMovementComponent* movement;
AnimationController* animation;
MultiComponent<SoundComponent>* sounds;
SoundComponent* jump_sound;
SoundComponent* hit_sound;
SoundComponent* die_sound;
bool grounded = false;
bool on_wall_left = false;
bool on_wall_right = false;
float coyote_timer = 0.0f;
float jump_buffer_timer = 0.0f;
int gamepad = 0;
int player_number = 1;
float width = 24.0f;
float height = 40.0f;
bool fall_through = false;
float fall_through_timer = 0.0f;
float fall_through_duration = 0.2f;
float attack_display_timer = 0.0f;
float attack_display_duration = 0.1f;
bool attack = false;
FightingCharacter(CharacterParams p, int player_number = 1) :
p(p),
player_number(player_number),
gamepad(player_number - 1),
width(p.width),
height(p.height)
{
}
void init() override
{
// Grab the physics service.
// All get_service calls should be done in init(). get_service is not quick and this also allows us to test
// that all services exist during init time.
physics = scene->get_service<PhysicsService>();
// Setup the character's physics body using the BodyComponent initialization callback lambda.
body = add_component<BodyComponent>(
[=](BodyComponent& b)
{
b2BodyDef body_def = b2DefaultBodyDef();
body_def.type = b2_dynamicBody;
body_def.fixedRotation = true;
body_def.isBullet = true;
// All units in box2d are in meters.
body_def.position = physics->convert_to_meters(p.position);
b.id = b2CreateBody(physics->world, &body_def);
b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial();
body_material.friction = p.friction;
body_material.restitution = p.restitution;
b2ShapeDef box_shape_def = b2DefaultShapeDef();
box_shape_def.density = p.density;
box_shape_def.material = body_material;
// Needed to presolve one-way behavior.
box_shape_def.enablePreSolveEvents = true;
// We use a rounded box which helps with getting stuck on edges.
b2Polygon body_polygon = b2MakeRoundedBox(physics->convert_to_meters(p.width / 2.0f),
physics->convert_to_meters(p.height / 2.0f),
physics->convert_to_meters(0.25));
b2CreatePolygonShape(b.id, &box_shape_def, &body_polygon);
});
PlatformerMovementParams mp;
mp.width = p.width;
mp.height = p.height;
movement = add_component<PlatformerMovementComponent>(mp);
level = scene->get_service<LevelService>();
// TODO: Is only allowing one component per type really as cool an idea as I thought?
sounds = add_component<MultiComponent<SoundComponent>>();
jump_sound = sounds->add_component("jump", "assets/sounds/jump.wav");
hit_sound = sounds->add_component("hit", "assets/sounds/hit.wav");
die_sound = sounds->add_component("die", "assets/sounds/die.wav");
// Setup animations.
animation = add_component<AnimationController>(body);
if (player_number == 1)
{
animation->add_animation("run",
std::vector<std::string>{"assets/sunnyland/fox/run-1.png",
"assets/sunnyland/fox/run-2.png",
"assets/sunnyland/fox/run-3.png",
"assets/sunnyland/fox/run-4.png",
"assets/sunnyland/fox/run-5.png",
"assets/sunnyland/fox/run-6.png"},
10.0f);
animation->add_animation("idle",
std::vector<std::string>{"assets/sunnyland/fox/idle-1.png",
"assets/sunnyland/fox/idle-2.png",
"assets/sunnyland/fox/idle-3.png",
"assets/sunnyland/fox/idle-4.png"},
5.0f);
animation->add_animation("jump", std::vector<std::string>{"assets/sunnyland/fox/jump-1.png"}, 0.0f);
animation->add_animation("fall", std::vector<std::string>{"assets/sunnyland/fox/jump-2.png"}, 0.0f);
animation->origin.y += 4;
}
else if (player_number == 2)
{
animation->add_animation("run",
std::vector<std::string>{"assets/sunnyland/bunny/run-1.png",
"assets/sunnyland/bunny/run-2.png",
"assets/sunnyland/bunny/run-3.png",
"assets/sunnyland/bunny/run-4.png",
"assets/sunnyland/bunny/run-5.png",
"assets/sunnyland/bunny/run-6.png"},
10.0f);
animation->add_animation("idle",
std::vector<std::string>{"assets/sunnyland/bunny/idle-1.png",
"assets/sunnyland/bunny/idle-2.png",
"assets/sunnyland/bunny/idle-3.png",
"assets/sunnyland/bunny/idle-4.png"},
10.0f);
animation->add_animation("jump", std::vector<std::string>{"assets/sunnyland/bunny/jump-1.png"}, 0.0f);
animation->add_animation("fall", std::vector<std::string>{"assets/sunnyland/bunny/jump-2.png"}, 0.0f);
animation->origin.y += 8;
}
else if (player_number == 3)
{
animation->add_animation("run",
std::vector<std::string>{"assets/sunnyland/squirrel/run-1.png",
"assets/sunnyland/squirrel/run-2.png",
"assets/sunnyland/squirrel/run-3.png",
"assets/sunnyland/squirrel/run-4.png",
"assets/sunnyland/squirrel/run-5.png",
"assets/sunnyland/squirrel/run-6.png"},
10.0f);
animation->add_animation("idle",
std::vector<std::string>{"assets/sunnyland/squirrel/idle-1.png",
"assets/sunnyland/squirrel/idle-2.png",
"assets/sunnyland/squirrel/idle-3.png",
"assets/sunnyland/squirrel/idle-4.png",
"assets/sunnyland/squirrel/idle-5.png",
"assets/sunnyland/squirrel/idle-6.png",
"assets/sunnyland/squirrel/idle-7.png",
"assets/sunnyland/squirrel/idle-8.png"},
8.0f);
animation->add_animation("jump",
std::vector<std::string>{"assets/sunnyland/squirrel/jump-1.png",
"assets/sunnyland/squirrel/jump-2.png",
"assets/sunnyland/squirrel/jump-3.png",
"assets/sunnyland/squirrel/jump-4.png"},
15.0f);
animation->origin.y += 7;
}
else if (player_number == 4)
{
animation->add_animation("run",
std::vector<std::string>{"assets/sunnyland/imp/run-1.png",
"assets/sunnyland/imp/run-2.png",
"assets/sunnyland/imp/run-3.png",
"assets/sunnyland/imp/run-4.png",
"assets/sunnyland/imp/run-5.png",
"assets/sunnyland/imp/run-6.png",
"assets/sunnyland/imp/run-7.png",
"assets/sunnyland/imp/run-8.png"},
10.0f);
animation->add_animation("idle",
std::vector<std::string>{"assets/sunnyland/imp/idle-1.png",
"assets/sunnyland/imp/idle-2.png",
"assets/sunnyland/imp/idle-3.png",
"assets/sunnyland/imp/idle-4.png"},
10.0f);
animation->add_animation("jump", std::vector<std::string>{"assets/sunnyland/imp/jump-1.png"}, 0.0f);
animation->add_animation("fall", std::vector<std::string>{"assets/sunnyland/imp/jump-4.png"}, 0.0f);
animation->origin.y += 10;
}
}
void update(float delta_time) override
{
// Get input and route to movement component.
float deadzone = 0.1f;
const bool jump_pressed =
IsKeyPressed(KEY_W) || IsGamepadButtonPressed(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN);
const bool jump_held = IsKeyDown(KEY_W) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN);
float move_x = 0.0f;
move_x = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_X);
if (fabsf(move_x) < deadzone)
{
move_x = 0.0f;
}
if (IsKeyDown(KEY_D) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_RIGHT))
{
move_x = 1.0f;
}
else if (IsKeyDown(KEY_A) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_LEFT))
{
move_x = -1.0f;
}
movement->set_input(move_x, jump_pressed, jump_held);
if (movement->grounded && jump_pressed)
{
jump_sound->play();
}
if (fabsf(movement->move_x) > 0.1f)
{
animation->play("run");
animation->flip_x = movement->move_x < 0.0f;
}
else
{
animation->play("idle");
}
if (!movement->grounded)
{
// Squirrel doesn't have a fall animation so we do this monstrosity.
if (player_number != 3)
{
if (body->get_velocity_meters().y < 0.0f)
{
animation->play("jump");
}
else
{
animation->play("fall");
}
}
else
{
animation->play("jump");
}
}
// Custom one-way platform fall-through logic.
float move_y = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_Y);
if (IsKeyPressed(KEY_S) || IsGamepadButtonPressed(gamepad, GAMEPAD_BUTTON_LEFT_FACE_DOWN) || move_y > 0.5f)
{
fall_through = true;
fall_through_timer = fall_through_duration;
}
if (fall_through_timer > 0.0f)
{
fall_through_timer = std::max(0.0f, fall_through_timer - delta_time);
if (fall_through_timer == 0.0f)
{
fall_through = false;
}
}
// Attack logic.
if (IsKeyPressed(KEY_SPACE) || IsGamepadButtonPressed(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT))
{
attack = true;
attack_display_timer = attack_display_duration;
Vector2 position = body->get_position_pixels();
position.x += (width / 2.0f + 8.0f) * (animation->flip_x ? -1.0f : 1.0f);
auto bodies = physics->circle_overlap(position, 8.0f, body->id);
for (auto& other_body : bodies)
{
// Apply impulse to other character.
b2Vec2 impulse = b2Vec2{animation->flip_x ? -10.0f : 10.0f, -10.0f};
b2Body_ApplyLinearImpulse(other_body, impulse, b2Body_GetPosition(other_body), true);
hit_sound->play();
}
}
if (attack_display_timer > 0.0f)
{
attack_display_timer = std::max(0.0f, attack_display_timer - delta_time);
if (attack_display_timer == 0.0f)
{
attack = false;
}
}
// Death and respawn logic.
if (body->get_position_pixels().y > level->get_size().y + 200.0f)
{
// Re-spawn at start position.
body->set_position(p.position);
body->set_velocity(Vector2{0.0f, 0.0f});
die_sound->play();
}
}
void draw() override
{
// Draw attack indicator.
if (attack)
{
Vector2 position = body->get_position_pixels();
position.x += (width / 2.0f + 8.0f) * (animation->flip_x ? -1.0f : 1.0f);
DrawCircleV(position, 8.0f, Fade(RED, 0.5f));
}
// Animations are drawn by the AnimationController component.
}
/**
* Pre-solve callback for one-way platforms.
*
* @param body_a The first body in the collision.
* @param body_b The second body in the collision.
* @param manifold The contact manifold.
* @param platforms The list of one-way platform StaticBox objects.
* @return true to enable the contact, false to disable it.
*/
bool PreSolve(b2BodyId body_a,
b2BodyId body_b,
b2Manifold* manifold,
std::vector<std::shared_ptr<StaticBox>> platforms) const
{
float sign = 0.0f;
b2BodyId other = b2_nullBodyId;
// Check which body is the character.
if (body_a == body->id)
{
sign = 1.0f;
other = body_b;
}
else if (body_b == body->id)
{
sign = -1.0f;
other = body_a;
}
if (sign * manifold->normal.y < 0.5f)
{
// Normal points down, disable contact.
return false;
}
if (fall_through)
{
for (auto& platform : platforms)
{
if (other == platform->body)
{
// Character is in fall-through state, disable contact.
return false;
}
}
}
// Otherwise, enable contact.
return true;
}
};
/**
* A fighting game scene.
*/
class FightingScene : public Scene
{
public:
RenderTexture2D renderer;
Rectangle render_rect;
std::vector<std::shared_ptr<StaticBox>> platforms;
std::vector<std::shared_ptr<FightingCharacter>> characters;
LevelService* level;
PhysicsService* physics;
std::shared_ptr<CameraObject> camera;
void init_services() override
{
// TextureService and SoundService are needed by other components and game objects.
add_service<TextureService>();
add_service<SoundService>();
// PhysicsService is used by LevelService and must be added first.
physics = add_service<PhysicsService>();
// Setup LDtk level. Checkout the file in LDtk editor to see how it's built.
std::vector<std::string> collision_names = {"walls"};
level = add_service<LevelService>("assets/levels/fighting.ldtk", "Stage", collision_names);
}
void init() override
{
auto window_manager = game->get_manager<WindowManager>();
// Find one-way platform entities in the level and create StaticBox game objects for them.
auto platform_entities = level->get_entities_by_name("One_way_platform");
for (auto& platform_entity : platform_entities)
{
Vector2 position = level->convert_to_pixels(platform_entity->getPosition());
Vector2 size = level->convert_to_pixels(platform_entity->getSize());
auto platform = add_game_object<StaticBox>(position + size / 2.0f, size);
platform->is_visible = false;
platform->add_tag("platform");
platforms.push_back(platform);
}
// Pre-solve callback to handle one-way platforms.
b2World_SetPreSolveCallback(physics->world, PreSolveStatic, this);
// Create player characters at the "Start" entities.
auto player_entities = level->get_entities_by_name("Start");
for (int i = 0; i < player_entities.size() && i < 4; i++)
{
auto& player_entity = player_entities[i];
CharacterParams params;
params.position = level->convert_to_pixels(player_entity->getPosition());
params.width = 16;
params.height = 24;
auto character = add_game_object<FightingCharacter>(params, i + 1);
character->add_tag("character");
characters.push_back(character);
}
// Setup shared camera.
camera = add_game_object<CameraObject>(level->get_size(),
Vector2{0.0f, 0.0f},
Vector2{300.0f, 300.0f},
0.0f,
0.0f,
0.0f,
0.0f);
camera->target = level->get_size() / 2.0f;
// Disable the background layer from drawing. We'll draw it manually in draw_scene().
level->set_layer_visibility("Background", false);
renderer = LoadRenderTexture((int)level->get_size().x, (int)level->get_size().y);
}
void update(float delta_time) override
{
// Set the camera target to the center of all players.
// Also zoom to fit all players.
Vector2 avg_position = {0.0f, 0.0f};
Vector2 min_point = {FLT_MAX, FLT_MAX};
Vector2 max_point = {-FLT_MAX, -FLT_MAX};
for (auto& character : characters)
{
Vector2 char_pos = character->body->get_position_pixels();
avg_position = avg_position + char_pos;
min_point.x = std::min(min_point.x, char_pos.x);
min_point.y = std::min(min_point.y, char_pos.y);
max_point.x = std::max(max_point.x, char_pos.x);
max_point.y = std::max(max_point.y, char_pos.y);
}
avg_position = avg_position / static_cast<float>(characters.size());
camera->target = avg_position;
// Force camera to align with pixel grid to avoid sub-pixel jitter.
camera->target.x = floorf(camera->target.x);
camera->target.y = floorf(camera->target.y);
// Calculate zoom to fit all characters.
float distance = sqrtf((max_point.x - min_point.x) * (max_point.x - min_point.x) +
(max_point.y - min_point.y) * (max_point.y - min_point.y));
float level_diagonal =
sqrtf(level->get_size().x * level->get_size().x + level->get_size().y * level->get_size().y);
float zoom = level_diagonal / (distance + 400);
zoom = std::clamp(zoom, 0.5f, 2.0f);
// Lerp zoom for smoothness.
// Note that this style of camera zoom is incompatible with pixel perfect rendering. Remove this line to see
// pixel perfect scaling.
camera->camera.zoom += (zoom - camera->camera.zoom) * std::min(1.0f, delta_time * 5.0f);
// Draw the level centered in the window.
float aspect_ratio = level->get_size().x / level->get_size().y;
float render_scale = GetScreenHeight() / level->get_size().y;
Vector2 render_size = {level->get_size().y * render_scale * aspect_ratio, level->get_size().y * render_scale};
auto pos = (Vector2{(float)GetScreenWidth(), (float)GetScreenHeight()} - render_size) / 2.0f;
render_rect = Rectangle{pos.x, pos.y, render_size.x, render_size.y};
// Trigger scene change on Enter key or gamepad start button.
if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT))
{
game->go_to_scene_next();
}
}
/**
* We override draw_scene instead of draw to control when Scene::draw_scene is called.
* This allows us to draw the scene inside the camera's Begin/End block and render the camera to a texture.
*/
void draw_scene() override
{
// Draw to render texture.
BeginTextureMode(renderer);
ClearBackground(MAGENTA);
// Draw the background layer outside of the camera.
level->draw_layer("Background");
// Start the camera and render the scene inside.
camera->draw_begin();
Scene::draw_scene();
// physics->draw_debug();
// camera->draw_debug();
camera->draw_end();
EndTextureMode();
// Draw centered.
DrawTexturePro(
renderer.texture,
Rectangle{0, 0, static_cast<float>(renderer.texture.width), -static_cast<float>(renderer.texture.height)},
render_rect,
Vector2{0.0f, 0.0f},
0.0f,
WHITE);
}
/**
* Static pre-solve callback to route to the appropriate character.
*
* @param shape_a The first shape in the collision.
* @param shape_b The second shape in the collision.
* @param manifold The contact manifold.
* @param context The FightingScene instance.
* @return true to enable the contact, false to disable it.
*/
static bool PreSolveStatic(b2ShapeId shape_a, b2ShapeId shape_b, b2Manifold* manifold, void* context)
{
FightingScene* self = static_cast<FightingScene*>(context);
b2BodyId body_a = b2Shape_GetBody(shape_a);
b2BodyId body_b = b2Shape_GetBody(shape_b);
// Find which character is involved and route to its PreSolve method.
for (auto& character : self->characters)
{
if (body_a == character->body->id || body_b == character->body->id)
{
return character->PreSolve(body_a, body_b, manifold, self->platforms);
}
}
return true;
}
};

379
src/samples/ghhb_game.h Normal file
View File

@ -0,0 +1,379 @@
/**
* DDR / Guitar Hero style rhythm game: 12 lanes, pre-coded notes, controller support.
* Controller: D-pad (lanes 0-3), face X/Y/A/B (4-7), LB/LT/RB/RT (8-11). 8bitdo compatible.
*/
#pragma once
#include "engine/prefabs/includes.h"
#include <algorithm>
#include <unordered_set>
#include <vector>
namespace
{
constexpr int LANE_COUNT = 12;
constexpr float HIT_WINDOW_PX = 80.0f;
constexpr float RECEPTOR_HEIGHT = 100.0f;
constexpr float SCROLL_PX_PER_SEC = 350.0f;
const int GAMEPAD_BUTTONS[LANE_COUNT] = {
GAMEPAD_BUTTON_LEFT_FACE_LEFT,
GAMEPAD_BUTTON_LEFT_FACE_UP,
GAMEPAD_BUTTON_LEFT_FACE_RIGHT,
GAMEPAD_BUTTON_LEFT_FACE_DOWN,
GAMEPAD_BUTTON_RIGHT_FACE_LEFT,
GAMEPAD_BUTTON_RIGHT_FACE_UP,
GAMEPAD_BUTTON_RIGHT_FACE_RIGHT,
GAMEPAD_BUTTON_RIGHT_FACE_DOWN,
GAMEPAD_BUTTON_LEFT_TRIGGER_2,
GAMEPAD_BUTTON_LEFT_TRIGGER_1,
GAMEPAD_BUTTON_RIGHT_TRIGGER_1,
GAMEPAD_BUTTON_RIGHT_TRIGGER_2,
};
const int KEY_KEYS[LANE_COUNT] = {
KEY_Q,
KEY_W,
KEY_E,
KEY_R,
KEY_A,
KEY_S,
KEY_D,
KEY_F,
KEY_Z,
KEY_X,
KEY_C,
KEY_V,
};
struct Note
{
float time = 0.0f;
int lane = 0;
float y_position(float song_time, float hit_line_y) const
{
return hit_line_y - (time - song_time) * SCROLL_PX_PER_SEC;
}
};
std::vector<Note> default_chart()
{
std::vector<Note> notes;
float t = 2.0f;
for (int i = 0; i < 4; i++)
{
for (int lane = 0; lane < LANE_COUNT; lane++)
{
notes.push_back(Note{t, lane});
t += 0.4f;
}
t += 0.6f;
}
for (int lane = 0; lane < LANE_COUNT; lane++)
{
notes.push_back(Note{t, lane});
t += 0.2f;
}
t += 0.5f;
for (int i = 0; i < 3; i++)
{
for (int lane = 0; lane < LANE_COUNT; lane++)
{
notes.push_back(Note{t + lane * 0.08f, lane});
}
t += 0.8f;
}
return notes;
}
} // namespace
class GHHBScene : public Scene
{
public:
Font font = {0};
std::vector<Note> chart = default_chart();
std::vector<Note*> spawned;
std::unordered_set<Note*> completed_notes;
float song_time = 0.0f;
int score = 0;
int combo = 0;
float hit_line_y = 0.0f;
float lane_width = 0.0f;
float screen_width = 0.0f;
float screen_height = 0.0f;
int gamepad_id = 0;
static constexpr float PRESS_FLASH_DURATION = 0.12f;
static constexpr float MISS_FLASH_DURATION = 0.15f;
float press_flash_timer[LANE_COUNT] = {0};
float hit_flash_timer[LANE_COUNT] = {0};
float miss_flash_timer[LANE_COUNT] = {0};
bool game_ended = false;
static constexpr float RESULTS_DELAY_AFTER_LAST_NOTE = 1.0f;
void on_enter() override
{
song_time = 0.0f;
score = 0;
combo = 0;
game_ended = false;
spawned.clear();
completed_notes.clear();
for (int i = 0; i < LANE_COUNT; i++)
{
press_flash_timer[i] = 0.0f;
hit_flash_timer[i] = 0.0f;
miss_flash_timer[i] = 0.0f;
}
}
void init() override
{
auto font_manager = game->get_manager<FontManager>();
font = font_manager->get_font("Roboto");
screen_width = static_cast<float>(GetScreenWidth());
screen_height = static_cast<float>(GetScreenHeight());
hit_line_y = screen_height - RECEPTOR_HEIGHT / 2.0f;
lane_width = screen_width / LANE_COUNT;
}
float lane_center_x(int lane) const
{
return (lane + 0.5f) * lane_width;
}
bool is_note_hittable(const Note& n) const
{
float y = n.y_position(song_time, hit_line_y);
return fabsf(y - hit_line_y) <= HIT_WINDOW_PX;
}
void consume_note(Note* n)
{
auto it = std::find_if(spawned.begin(), spawned.end(), [n](Note* p) { return p == n; });
if (it != spawned.end())
{
hit_flash_timer[n->lane] = PRESS_FLASH_DURATION;
spawned.erase(it);
completed_notes.insert(n);
}
combo++;
score += 100 + std::min(combo * 10, 50);
}
void update(float delta_time) override
{
if (game_ended)
{
if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT))
{
game->go_to_scene("title");
}
return;
}
song_time += delta_time;
float last_note_time = 0.0f;
for (const auto& n : chart)
{
if (n.time > last_note_time)
{
last_note_time = n.time;
}
}
float note_exit_seconds = HIT_WINDOW_PX / SCROLL_PX_PER_SEC;
if (song_time >= last_note_time + note_exit_seconds + RESULTS_DELAY_AFTER_LAST_NOTE)
{
game_ended = true;
return;
}
float lead_seconds = (hit_line_y - 60.0f) / SCROLL_PX_PER_SEC;
for (auto& n : chart)
{
if (completed_notes.count(&n) != 0)
{
continue;
}
bool already = std::find_if(spawned.begin(), spawned.end(),
[&n](Note* p) { return p == &n; }) != spawned.end();
if (!already && song_time >= n.time - lead_seconds)
{
spawned.push_back(&n);
}
}
for (auto it = spawned.begin(); it != spawned.end();)
{
Note* n = *it;
float y = n->y_position(song_time, hit_line_y);
if (y > hit_line_y)
{
miss_flash_timer[n->lane] = MISS_FLASH_DURATION;
completed_notes.insert(n);
it = spawned.erase(it);
combo = 0;
}
else
{
++it;
}
}
for (int lane = 0; lane < LANE_COUNT; lane++)
{
press_flash_timer[lane] -= delta_time;
if (press_flash_timer[lane] < 0.0f)
{
press_flash_timer[lane] = 0.0f;
}
hit_flash_timer[lane] -= delta_time;
if (hit_flash_timer[lane] < 0.0f)
{
hit_flash_timer[lane] = 0.0f;
}
miss_flash_timer[lane] -= delta_time;
if (miss_flash_timer[lane] < 0.0f)
{
miss_flash_timer[lane] = 0.0f;
}
bool pressed = IsKeyPressed(KEY_KEYS[lane]) ||
IsGamepadButtonPressed(gamepad_id, GAMEPAD_BUTTONS[lane]);
if (pressed)
{
press_flash_timer[lane] = PRESS_FLASH_DURATION;
}
if (!pressed)
{
continue;
}
Note* best = nullptr;
float best_dist = 1e9f;
for (Note* n : spawned)
{
if (n->lane != lane)
{
continue;
}
if (!is_note_hittable(*n))
{
continue;
}
float y = n->y_position(song_time, hit_line_y);
float d = fabsf(y - hit_line_y);
if (d < best_dist)
{
best_dist = d;
best = n;
}
}
if (best != nullptr)
{
consume_note(best);
}
}
if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT))
{
game->go_to_scene("title");
}
}
void draw() override
{
ClearBackground(Color{30, 30, 46, 255});
for (int lane = 0; lane < LANE_COUNT; lane++)
{
float cx = lane_center_x(lane);
DrawRectangle(static_cast<int>(lane * lane_width),
0,
static_cast<int>(lane_width),
static_cast<int>(screen_height),
lane % 2 == 0 ? Color{50, 50, 70, 255} : Color{45, 45, 65, 255});
DrawLineEx(Vector2{cx, 0}, Vector2{cx, screen_height}, 2.0f, Color{70, 70, 90, 255});
}
float receptor_top = screen_height - RECEPTOR_HEIGHT;
DrawRectangle(0,
static_cast<int>(receptor_top),
static_cast<int>(screen_width),
static_cast<int>(RECEPTOR_HEIGHT),
Color{60, 60, 100, 255});
for (int lane = 0; lane < LANE_COUNT; lane++)
{
if (hit_flash_timer[lane] > 0.0f)
{
float alpha = 180.0f * (hit_flash_timer[lane] / PRESS_FLASH_DURATION);
DrawRectangle(static_cast<int>(lane * lane_width),
static_cast<int>(receptor_top),
static_cast<int>(lane_width),
static_cast<int>(RECEPTOR_HEIGHT),
Color{80, 255, 120, static_cast<unsigned char>(alpha)});
}
else if (press_flash_timer[lane] > 0.0f)
{
float alpha = 180.0f * (press_flash_timer[lane] / PRESS_FLASH_DURATION);
DrawRectangle(static_cast<int>(lane * lane_width),
static_cast<int>(receptor_top),
static_cast<int>(lane_width),
static_cast<int>(RECEPTOR_HEIGHT),
Color{255, 255, 255, static_cast<unsigned char>(alpha)});
}
if (miss_flash_timer[lane] > 0.0f)
{
float alpha = 200.0f * (miss_flash_timer[lane] / MISS_FLASH_DURATION);
DrawRectangle(static_cast<int>(lane * lane_width),
static_cast<int>(receptor_top),
static_cast<int>(lane_width),
static_cast<int>(RECEPTOR_HEIGHT),
Color{255, 80, 80, static_cast<unsigned char>(alpha)});
}
}
DrawLineEx(Vector2{0, hit_line_y}, Vector2{screen_width, hit_line_y}, 3.0f, WHITE);
for (Note* n : spawned)
{
float y = n->y_position(song_time, hit_line_y);
if (y < -40.0f || y > screen_height + 40.0f)
{
continue;
}
float cx = lane_center_x(n->lane);
DrawCircle(static_cast<int>(cx), static_cast<int>(y), 22, Color{220, 100, 100, 255});
DrawCircleLines(static_cast<int>(cx), static_cast<int>(y), 22,
Color{255, 150, 150, 255});
}
std::string score_text = "Score: " + std::to_string(score) + " Combo: " + std::to_string(combo);
DrawTextEx(font, score_text.c_str(), {20, 16}, 28, 1, WHITE);
DrawTextEx(font, "Arrows / D-pad / X Y A B / LB LT RB RT: hit when note reaches white line",
{20, receptor_top - 28}, 18, 1, Color{200, 200, 200, 255});
if (game_ended)
{
DrawRectangle(0, 0, static_cast<int>(screen_width), static_cast<int>(screen_height),
Color{0, 0, 0, 200});
const char* title_text = "Song complete!";
const float title_font_size = 42.0f;
float title_w = MeasureTextEx(font, title_text, title_font_size, 1).x;
DrawTextEx(font, title_text,
{screen_width / 2.0f - title_w / 2.0f, screen_height / 2.0f - 80},
title_font_size, 1, WHITE);
std::string final_score_text = "Final score: " + std::to_string(score);
const float score_font_size = 32.0f;
float score_w = MeasureTextEx(font, final_score_text.c_str(), score_font_size, 1).x;
DrawTextEx(font, final_score_text.c_str(),
{screen_width / 2.0f - score_w / 2.0f, screen_height / 2.0f - 20},
score_font_size, 1, Color{220, 220, 255, 255});
const char* prompt = "Press Enter to return to menu";
const float prompt_font_size = 20.0f;
float prompt_w = MeasureTextEx(font, prompt, prompt_font_size, 1).x;
DrawTextEx(font, prompt,
{screen_width / 2.0f - prompt_w / 2.0f, screen_height / 2.0f + 40},
prompt_font_size, 1, Color{180, 180, 200, 255});
}
}
};

View File

@ -6,7 +6,7 @@ class TitleScreen : public Scene
{ {
public: public:
Font font; Font font;
std::string title = "Game Jam Kit"; std::string title = "Guitar Hero But Better";
void init() override void init() override
{ {
auto font_manager = game->get_manager<FontManager>(); auto font_manager = game->get_manager<FontManager>();
@ -18,7 +18,7 @@ public:
// Trigger scene change on Enter key or gamepad start button. // Trigger scene change on Enter key or gamepad start button.
if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT)) if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT))
{ {
game->go_to_scene_next(); game->go_to_scene("ghhb");
} }
} }
@ -28,7 +28,7 @@ public:
auto height = GetScreenHeight(); auto height = GetScreenHeight();
auto title_text_size = MeasureTextEx(font, title.c_str(), 64, 0); auto title_text_size = MeasureTextEx(font, title.c_str(), 64, 0);
std::string subtitle = "Press Start or Enter to Switch Scenes"; std::string subtitle = "Press Start or Enter to Play";
auto subtitle_text_size = MeasureTextEx(font, subtitle.c_str(), 32, 0); auto subtitle_text_size = MeasureTextEx(font, subtitle.c_str(), 32, 0);
ClearBackground(SKYBLUE); ClearBackground(SKYBLUE);

View File

@ -1,551 +0,0 @@
/**
* Demonstration of a top down shooter game.
* Shows how to draw lights using custom blend modes.
*/
#pragma once
#include "engine/prefabs/includes.h"
#include "rlgl.h"
// Custom Blend Modes for lights. See https://www.raylib.com/examples/shapes/loader.html?name=shapes_top_down_lights
#define RLGL_SRC_ALPHA 0x0302
#define RLGL_MIN 0x8007
class ZombieScene;
/**
* A bullet fired by a character.
*/
class Bullet : public GameObject
{
public:
PhysicsService* physics;
BodyComponent* body;
SpriteComponent* sprite;
SoundComponent* hit_sound;
float speed = 800.0f; // pixels per second
void init() override
{
physics = scene->get_service<PhysicsService>();
body = add_component<BodyComponent>(
[=](BodyComponent& b)
{
b2BodyDef body_def = b2DefaultBodyDef();
body_def.type = b2_dynamicBody;
body_def.isBullet = true;
// Start off-screen.
body_def.position = physics->convert_to_meters(Vector2{-1000.0f, -1000.0f});
body_def.userData = this;
b.id = b2CreateBody(physics->world, &body_def);
b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial();
body_material.restitution = 0.0f;
body_material.friction = 0.0f;
b2ShapeDef circle_shape_def = b2DefaultShapeDef();
circle_shape_def.density = 0.25f;
circle_shape_def.material = body_material;
b2Circle circle_shape = {b2Vec2_zero, physics->convert_to_meters(8.0f)};
b2CreateCircleShape(b.id, &circle_shape_def, &circle_shape);
});
sprite = add_component<SpriteComponent>("assets/zombie_shooter/bullet.png", body);
hit_sound = add_component<SoundComponent>("assets/sounds/hit.wav");
}
void update(float delta_time) override
{
auto contacts = body->get_contacts();
for (const auto& contact : contacts)
{
// Deactivate the bullet if we hit anything.
is_active = false;
// Move it off-screen.
body->set_position(Vector2{-1000.0f, -1000.0f});
body->set_velocity(Vector2{0.0f, 0.0f});
GameObject* other = static_cast<GameObject*>(b2Body_GetUserData(contact));
if (other)
{
if (other->has_tag("zombie"))
{
hit_sound->play();
// Hit a zombie, deactivate it too.
other->is_active = false;
// Move it off-screen.
auto zombie_body = other->get_component<BodyComponent>();
if (zombie_body)
{
zombie_body->set_position(Vector2{-1000.0f, -1000.0f});
zombie_body->set_velocity(Vector2{0.0f, 0.0f});
zombie_body->disable();
}
auto zombie_sprite = other->get_component<SpriteComponent>();
if (zombie_sprite)
{
zombie_sprite->set_position(Vector2{-1000.0f, -1000.0f});
}
}
break;
}
}
}
};
/**
* A top-down character controlled by the player.
*/
class TopDownCharacter : public GameObject
{
public:
Vector2 position = {0, 0};
BodyComponent* body;
PhysicsService* physics;
SpriteComponent* sprite;
TopDownMovementComponent* movement;
MultiComponent<SoundComponent>* sounds;
SoundComponent* shoot_sound;
std::vector<std::shared_ptr<Bullet>> bullets;
int player_num = 0;
int health = 10;
float contact_timer = 1.0f;
float contact_cooldown = 0.3f;
TopDownCharacter(Vector2 position, std::vector<std::shared_ptr<Bullet>> bullets, int player_num = 0) :
position(position),
bullets(std::move(bullets)),
player_num(player_num)
{
}
void init() override
{
// Grab the physics service.
// All get_service calls should be done in init(). get_service is not quick and this also allows us to test
// that all services exist during init time.
physics = scene->get_service<PhysicsService>();
body = add_component<BodyComponent>(
[=](BodyComponent& b)
{
b2BodyDef body_def = b2DefaultBodyDef();
body_def.type = b2_dynamicBody;
body_def.fixedRotation = true;
body_def.position = physics->convert_to_meters(position);
body_def.userData = this;
b.id = b2CreateBody(physics->world, &body_def);
b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial();
b2ShapeDef circle_shape_def = b2DefaultShapeDef();
circle_shape_def.density = 1.0f;
circle_shape_def.material = body_material;
b2Circle circle_shape = {b2Vec2_zero, physics->convert_to_meters(16.0f)};
b2CreateCircleShape(b.id, &circle_shape_def, &circle_shape);
});
// Setup movement.
TopDownMovementParams mp;
mp.accel = 5000.0f;
mp.friction = 5000.0f;
mp.max_speed = 350.0f;
movement = add_component<TopDownMovementComponent>(mp);
// Setup sounds.
// TODO: Is only allowing one component per type really as cool an idea as I thought?
sounds = add_component<MultiComponent<SoundComponent>>();
shoot_sound = sounds->add_component("shoot", "assets/sounds/shoot.wav");
// Setup sprite.
sprite =
add_component<SpriteComponent>("assets/zombie_shooter/player_" + std::to_string(player_num + 1) + ".png");
}
void update(float delta_time) override
{
Vector2 move = {0.0f, 0.0f};
move = {GetGamepadAxisMovement(player_num, GAMEPAD_AXIS_LEFT_X),
GetGamepadAxisMovement(player_num, GAMEPAD_AXIS_LEFT_Y)};
if (IsKeyDown(KEY_W) || IsGamepadButtonDown(player_num, GAMEPAD_BUTTON_LEFT_FACE_UP))
{
move.y -= 1.0f;
}
if (IsKeyDown(KEY_S) || IsGamepadButtonDown(player_num, GAMEPAD_BUTTON_LEFT_FACE_DOWN))
{
move.y += 1.0f;
}
if (IsKeyDown(KEY_A) || IsGamepadButtonDown(player_num, GAMEPAD_BUTTON_LEFT_FACE_LEFT))
{
move.x -= 1.0f;
}
if (IsKeyDown(KEY_D) || IsGamepadButtonDown(player_num, GAMEPAD_BUTTON_LEFT_FACE_RIGHT))
{
move.x += 1.0f;
}
movement->set_input(move.x, move.y);
// Update sprite position and rotation.
sprite->set_position(body->get_position_pixels());
sprite->set_rotation(movement->facing_dir);
// Shooting
if (IsKeyPressed(KEY_SPACE) || IsGamepadButtonPressed(player_num, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT))
{
// Find an inactive bullet from the pool.
for (auto& bullet : bullets)
{
if (!bullet->is_active)
{
// Play shoot sound.
shoot_sound->play();
// Activate and position the bullet.
Vector2 char_pos = body->get_position_pixels();
Vector2 shoot_dir = {std::cos(movement->facing_dir * DEG2RAD),
std::sin(movement->facing_dir * DEG2RAD)};
Vector2 bullet_start_pos = {char_pos.x + shoot_dir.x * 48.0f, char_pos.y + shoot_dir.y * 48.0f};
bullet->body->set_position(bullet_start_pos);
bullet->body->set_rotation(movement->facing_dir + 90.0f);
// Set bullet velocity.
Vector2 velocity = {shoot_dir.x * bullet->speed, shoot_dir.y * bullet->speed};
bullet->body->set_velocity(velocity);
bullet->is_active = true;
break;
}
}
}
// Damage.
auto contacts = body->get_contacts();
for (const auto& contact : contacts)
{
GameObject* other = static_cast<GameObject*>(b2Body_GetUserData(contact));
if (other)
{
if (other->has_tag("zombie"))
{
// When we are in contact with a zombie long enough, take 1 damage.
if (contact_timer > 0.0f)
{
contact_timer -= delta_time;
}
if (contact_timer <= 0.0f)
{
health -= 1;
contact_timer = contact_cooldown;
if (health <= 0)
{
// Deactivate character.
is_active = false;
// Move off-screen.
body->set_position(Vector2{-1000.0f, -1000.0f});
body->set_velocity(Vector2{0.0f, 0.0f});
}
}
}
}
}
}
};
/**
* A zombie that chases the closest player.
*/
class Zombie : public GameObject
{
public:
BodyComponent* body;
PhysicsService* physics;
SpriteComponent* sprite;
TopDownMovementComponent* movement;
std::vector<std::shared_ptr<TopDownCharacter>> players;
Zombie(std::vector<std::shared_ptr<TopDownCharacter>> players) : players(std::move(players)) {}
void init() override
{
// Grab the physics service.
physics = scene->get_service<PhysicsService>();
body = add_component<BodyComponent>(
[=](BodyComponent& b)
{
b2BodyDef body_def = b2DefaultBodyDef();
body_def.type = b2_dynamicBody;
body_def.fixedRotation = true;
body_def.position = physics->convert_to_meters(Vector2{-1000.0f, -1000.0f});
body_def.userData = this;
b.id = b2CreateBody(physics->world, &body_def);
b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial();
b2ShapeDef circle_shape_def = b2DefaultShapeDef();
circle_shape_def.density = 1.0f;
circle_shape_def.material = body_material;
b2Circle circle_shape = {b2Vec2_zero, physics->convert_to_meters(16.0f)};
b2CreateCircleShape(b.id, &circle_shape_def, &circle_shape);
// Disable by default.
b2Body_Disable(b.id);
});
// Setup movement.
TopDownMovementParams mp;
mp.accel = 5000.0f;
mp.friction = 5000.0f;
mp.max_speed = 100.0f;
movement = add_component<TopDownMovementComponent>(mp);
// Setup sprite.
sprite = add_component<SpriteComponent>("assets/zombie_shooter/zombie.png");
}
void update(float delta_time) override
{
// Find the closest player and move towards them.
Vector2 closest_player_pos = {0, 0};
float closest_dist_sq = FLT_MAX;
for (auto& player : players)
{
Vector2 player_pos = player->body->get_position_pixels();
Vector2 to_player = {player_pos.x - body->get_position_pixels().x,
player_pos.y - body->get_position_pixels().y};
float dist_sq = to_player.x * to_player.x + to_player.y * to_player.y;
if (dist_sq < closest_dist_sq)
{
closest_dist_sq = dist_sq;
closest_player_pos = player_pos;
}
}
Vector2 to_closest = {closest_player_pos.x - body->get_position_pixels().x,
closest_player_pos.y - body->get_position_pixels().y};
float to_closest_len = std::sqrt(to_closest.x * to_closest.x + to_closest.y * to_closest.y);
if (to_closest_len > 0.0f)
{
to_closest.x /= to_closest_len;
to_closest.y /= to_closest_len;
}
movement->set_input(to_closest.x, to_closest.y);
// Update sprite position and rotation.
sprite->set_position(body->get_position_pixels());
sprite->set_rotation(movement->facing_dir);
}
};
/**
* A spawner that spawns zombies at intervals.
*/
class Spawner : public GameObject
{
public:
float spawn_timer = 0.0f;
float spawn_interval = 1.0f; // Spawn a zombie every 1 second
std::vector<std::shared_ptr<Zombie>> zombie_pool;
Vector2 position = {0, 0};
Vector2 size = {0, 0};
Spawner(Vector2 position, Vector2 size, std::vector<std::shared_ptr<Zombie>> zombies) :
position(position - size * 0.5f),
size(size),
zombie_pool(std::move(zombies))
{
}
void update(float delta_time) override
{
spawn_timer -= delta_time;
if (spawn_timer <= 0.0f)
{
spawn_timer = spawn_interval;
// Spawn a zombie at a random position within the spawner area
float x = position.x + static_cast<float>(GetRandomValue(0, static_cast<int>(size.x)));
float y = position.y + static_cast<float>(GetRandomValue(0, static_cast<int>(size.y)));
Vector2 spawn_pos = {x, y};
for (auto& zombie : zombie_pool)
{
if (!zombie->is_active)
{
zombie->body->set_position(spawn_pos);
zombie->is_active = true;
zombie->body->enable();
return;
}
}
}
}
};
/**
* A scene for the zombie shooter game.
*/
class ZombieScene : public Scene
{
public:
FontManager* font_manager;
PhysicsService* physics;
LevelService* level;
RenderTexture2D renderer;
RenderTexture2D light_map;
Texture2D light_texture;
std::vector<std::shared_ptr<Bullet>> bullets;
std::vector<std::shared_ptr<TopDownCharacter>> characters;
std::vector<std::shared_ptr<Zombie>> zombies;
void init_services() override
{
// TextureService and SoundService are needed by other components and game objects.
add_service<TextureService>();
add_service<SoundService>();
// Set gravity to zero for top-down game.
physics = add_service<PhysicsService>(b2Vec2_zero);
std::vector<std::string> collision_names = {"walls", "obstacles"};
level = add_service<LevelService>("assets/levels/top_down.ldtk", "Level", collision_names);
// Grab the font manager.
font_manager = game->get_manager<FontManager>();
}
void init() override
{
const auto& entities_layer = level->get_layer_by_name("Entities");
// Prepare a pool of bullets.
// It is unwise to call init during update loops, so we create all bullets here and deactivate them.
for (int i = 0; i < 100; i++)
{
auto bullet = add_game_object<Bullet>();
// When is_active is false, update and draw are skipped.
bullet->is_active = false;
bullets.push_back(bullet);
}
// Create player characters.
auto player_entities = level->get_entities_by_name("Start");
for (int i = 0; i < player_entities.size() && i < 4; i++)
{
auto& player_entity = player_entities[i];
auto position = level->convert_to_pixels(player_entity->getPosition());
auto character = add_game_object<TopDownCharacter>(position, bullets, i);
character->add_tag("player");
characters.push_back(character);
}
// Prepare a pool of zombies.
// It is unwise to call init during update loops, so we create all zombies here and deactivate them.
for (int i = 0; i < 100; i++)
{
// Start off-screen
auto zombie = add_game_object<Zombie>(characters);
// When is_active is false, update and draw are skipped.
zombie->is_active = false;
zombie->add_tag("zombie");
zombies.push_back(zombie);
}
// Create spawner.
auto spawn_entity = level->get_entities_by_name("Spawn")[0];
auto spawn_position = level->convert_to_pixels(spawn_entity->getPosition());
auto spawn_size = level->convert_to_pixels(spawn_entity->getSize());
auto spawner = add_game_object<Spawner>(spawn_position, spawn_size, zombies);
// We want to control when the foreground layer is drawn.
level->set_layer_visibility("Foreground", false);
// Create render texture to scale the level to the screen.
renderer = LoadRenderTexture((int)level->get_size().x, (int)level->get_size().y);
light_map = LoadRenderTexture((int)level->get_size().x, (int)level->get_size().y);
light_texture = get_service<TextureService>()->get_texture("assets/zombie_shooter/light.png");
}
void update(float delta_time) override
{
// Trigger scene change on Enter key or gamepad start button.
if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT))
{
game->go_to_scene_next();
}
}
void draw_scene() override
{
// Build up the light mask
BeginTextureMode(light_map);
ClearBackground(BLACK);
// Force the blend mode to only set the alpha of the destination
rlSetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MIN);
rlSetBlendMode(BLEND_CUSTOM);
// Merge in all the light masks
for (int i = 0; i < 4; i++)
{
// DrawCircleGradient((int)characters[i]->body->get_position_pixels().x,
// (int)characters[i]->body->get_position_pixels().y,
// 300.0f,
// ColorAlpha(WHITE, 0),
// ColorAlpha(BLACK, 1));
DrawTexture(light_texture,
(int)(characters[i]->body->get_position_pixels().x - light_texture.width / 2),
(int)(characters[i]->body->get_position_pixels().y - light_texture.height / 2),
WHITE);
}
rlDrawRenderBatchActive();
// Go back to normal blend
rlSetBlendMode(BLEND_ALPHA);
EndTextureMode();
// Draw to render texture first.
BeginTextureMode(renderer);
ClearBackground(MAGENTA);
Scene::draw_scene();
level->draw_layer("Foreground");
DrawTexturePro(
light_map.texture,
{0.0f, 0.0f, static_cast<float>(light_map.texture.width), static_cast<float>(-light_map.texture.height)},
{0.0f, 0.0f, static_cast<float>(light_map.texture.width), static_cast<float>(light_map.texture.height)},
{0.0f, 0.0f},
0.0f,
ColorAlpha(WHITE, 0.92f));
DrawRectangle(10, 10, 210, 210, Fade(WHITE, 0.3f));
DrawTextEx(font_manager->get_font("Roboto"),
TextFormat("Health: %d\nHealth: %d\nHealth: %d\nHealth: %d",
characters[0]->health,
characters[1]->health,
characters[2]->health,
characters[3]->health),
Vector2{20.0f, 20.0f},
45.0f,
1.0f,
RED);
EndTextureMode();
// Draw the render texture scaled to the screen.
DrawTexturePro(
renderer.texture,
{0.0f, 0.0f, static_cast<float>(renderer.texture.width), static_cast<float>(-renderer.texture.height)},
{0.0f, 0.0f, static_cast<float>(GetScreenWidth()), static_cast<float>(GetScreenHeight())},
{0.0f, 0.0f},
0.0f,
WHITE);
}
};