Initial release. Removed sample games.
This commit is contained in:
parent
366b2b2a9a
commit
0fcabbf1c6
12
src/main.cpp
12
src/main.cpp
@ -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
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -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
379
src/samples/ghhb_game.h
Normal 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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue
Block a user