diff --git a/CMakeLists.txt b/CMakeLists.txt index 82504d9..d042943 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,9 +11,23 @@ add_subdirectory(libs/blah) # add our source add_executable(game src/main.cpp + src/world.h src/world.cpp + src/game.h src/game.cpp + src/content.h src/content.cpp + src/assets/sprite.h + src/assets/sprite.cpp + src/components/animator.h + src/components/animator.cpp + src/components/collider.h + src/components/collider.cpp + src/components/player.h + src/components/player.cpp + src/components/mover.h + src/components/mover.cpp + src/masks.h ) # Reference blah diff --git a/content/sprites/circle.ase b/content/sprites/circle.ase new file mode 100644 index 0000000..710d0f6 Binary files /dev/null and b/content/sprites/circle.ase differ diff --git a/content/sprites/player.ase b/content/sprites/player.ase new file mode 100644 index 0000000..0d785f0 Binary files /dev/null and b/content/sprites/player.ase differ diff --git a/src/assets/sprite.cpp b/src/assets/sprite.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/assets/sprite.h b/src/assets/sprite.h new file mode 100644 index 0000000..e9c4e75 --- /dev/null +++ b/src/assets/sprite.h @@ -0,0 +1,26 @@ +#pragma once +#include + +using namespace Blah; + +namespace TL +{ + struct Sprite + { + struct Frame + { + Subtexture image; + float duration; + }; + + struct Animation + { + String name; + Vector frames; + }; + + String name; + Vec2 origin; + Vector animations; + }; +} \ No newline at end of file diff --git a/src/components/animator.cpp b/src/components/animator.cpp new file mode 100644 index 0000000..ef41913 --- /dev/null +++ b/src/components/animator.cpp @@ -0,0 +1,76 @@ +#include "animator.h" +#include "../content.h" + +using namespace TL; + +Animator::Animator(const String& sprite) +{ + m_sprite = Content::find_sprite(sprite); + m_animation_index = 0; +} + +void Animator::play(const String& animation) +{ + BLAH_ASSERT(m_sprite, "No Sprite Assigned!"); + + for (int i = 0; i < m_sprite->animations.size(); i++) + { + if (m_sprite->animations[i].name == animation) + { + m_animation_index = i; + m_frame_index = 0; + break; + } + } +} + +void Animator::update() +{ + // only update if we're in a valid state + if (in_valid_state()) + { + // quick references + auto& anim = m_sprite->animations[m_animation_index]; + auto& frame = anim.frames[m_frame_index]; + + // increment frame counter + m_frame_counter += Time::delta; + + // move to next frame after duration + while (m_frame_counter >= frame.duration) + { + // reset frame counter + m_frame_counter -= frame.duration; + + // increement frame, move back if we're at the end + m_frame_index++; + if (m_frame_index >= anim.frames.size()) + m_frame_index = 0; + } + } +} + +void Animator::render(Batch& batch) +{ + if (in_valid_state()) + { + batch.push_matrix( + Mat3x2::create_transform(entity()->position, m_sprite->origin, Vec2::one, 0)); + + auto& anim = m_sprite->animations[m_animation_index]; + auto& frame = anim.frames[m_frame_index]; + batch.tex(frame.image, Vec2::zero, Color::white); + + batch.pop_matrix(); + } +} + +bool Animator::in_valid_state() const +{ + return + m_sprite && + m_animation_index >= 0 && + m_animation_index < m_sprite->animations.size() && + m_frame_index >= 0 && + m_frame_index < m_sprite->animations[m_animation_index].frames.size(); +} diff --git a/src/components/animator.h b/src/components/animator.h new file mode 100644 index 0000000..3ff92d4 --- /dev/null +++ b/src/components/animator.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include "../assets/sprite.h" +#include "../world.h" + +using namespace Blah; + +namespace TL +{ + class Animator : public Component + { + private: + const Sprite* m_sprite = nullptr; + int m_animation_index = 0; + int m_frame_index = 0; + float m_frame_counter = 0; + + public: + + Animator() = default; + Animator(const String& sprite); + + void play(const String& animation); + + void update() override; + void render(Batch& batch) override; + + private: + bool in_valid_state() const; + }; +} \ No newline at end of file diff --git a/src/components/collider.cpp b/src/components/collider.cpp new file mode 100644 index 0000000..717d6ea --- /dev/null +++ b/src/components/collider.cpp @@ -0,0 +1,147 @@ +#include "collider.h" + +using namespace TL; + +Collider::Collider() +{ + visible = false; + active = false; +} + +Collider Collider::make_rect(const RectI& rect) +{ + Collider collider; + collider.m_shape = Shape::Rect; + collider.m_rect = rect; + return collider; +} + +Collider Collider::make_grid(int tile_size, int columns, int rows) +{ + Collider collider; + collider.m_shape = Shape::Grid; + collider.m_grid.tile_size = tile_size; + collider.m_grid.columns = columns; + collider.m_grid.rows = rows; + collider.m_grid.cells = std::shared_ptr(new bool[columns * rows]); + return collider; +} + +Collider::Shape Collider::shape() const +{ + return m_shape; +} + +RectI Collider::get_rect() const +{ + BLAH_ASSERT(m_shape == Shape::Rect, "Collider is not a Rectangle"); + return m_rect; +} + +void Collider::set_rect(const RectI& value) +{ + BLAH_ASSERT(m_shape == Shape::Rect, "Collider is not a Rectangle"); + m_rect = value; +} + +bool Collider::get_cell(int x, int y) const +{ + BLAH_ASSERT(m_shape == Shape::Grid, "Collider is not a Grid"); + BLAH_ASSERT(x >= 0 && y >= 0 && x < m_grid.columns && y < m_grid.rows, "Cell is out of bounds"); + + return m_grid.cells[x + y * m_grid.columns]; +} + +void Collider::set_cell(int x, int y, bool value) +{ + BLAH_ASSERT(m_shape == Shape::Grid, "Collider is not a Grid"); + BLAH_ASSERT(x >= 0 && y >= 0 && x < m_grid.columns&& y < m_grid.rows, "Cell is out of bounds"); + + m_grid.cells[x + y * m_grid.columns] = value; +} + +bool Collider::check(uint32_t mask, Point offset) const +{ + auto other = world()->first(); + while (other) + { + if (other != this && + (other->mask & mask) == mask && + overlaps(other, offset)) + return true; + + other = (Collider*)other->next(); + } + + return false; +} + +bool Collider::overlaps(const Collider* other, Point offset) const +{ + if (m_shape == Shape::Rect) + { + if (other->m_shape == Shape::Rect) + { + return rect_to_rect(this, other, offset); + } + else if (other->m_shape == Shape::Grid) + { + return rect_to_grid(this, other, offset); + } + } + else if (m_shape == Shape::Grid) + { + if (other->m_shape == Shape::Rect) + { + return rect_to_grid(other, this, -offset); + } + else if (other->m_shape == Shape::Grid) + { + BLAH_ASSERT(false, "Grid->Grid Overlap checks not supported!"); + } + } + + return false; +} + +void Collider::render(Batch& batch) +{ + static const Color color = Color::red; + + batch.push_matrix(Mat3x2::create_translation(entity()->position)); + + if (m_shape == Shape::Rect) + { + batch.rect_line(m_rect, 1, color); + } + else if (m_shape == Shape::Grid) + { + for (int x = 0; x < m_grid.columns; x++) + { + for (int y = 0; y < m_grid.rows; y++) + { + if (!m_grid.cells[x + y * m_grid.columns]) + continue; + + batch.rect_line( + Rect(x * m_grid.tile_size, y * m_grid.tile_size, m_grid.tile_size, m_grid.tile_size), + 1, color); + } + } + } + + batch.pop_matrix(); +} + +bool TL::Collider::rect_to_rect(const Collider* a, const Collider* b, Point offset) +{ + RectI ar = a->m_rect + a->entity()->position + offset; + RectI br = b->m_rect + b->entity()->position; + + return ar.overlaps(br); +} + +bool TL::Collider::rect_to_grid(const Collider* a, const Collider* b, Point offset) +{ + return false; +} diff --git a/src/components/collider.h b/src/components/collider.h new file mode 100644 index 0000000..a976589 --- /dev/null +++ b/src/components/collider.h @@ -0,0 +1,54 @@ +#pragma once +#include +#include +#include "../world.h" + +using namespace Blah; + +namespace TL +{ + class Collider : public Component + { + public: + enum class Shape + { + None, + Rect, + Grid + }; + + uint32_t mask; + + Collider(); + + static Collider make_rect(const RectI& rect); + static Collider make_grid(int tile_size, int columns, int rows); + + Shape shape() const; + RectI get_rect() const; + void set_rect(const RectI& value); + bool get_cell(int x, int y) const; + void set_cell(int x, int y, bool value); + + bool check(uint32_t mask, Point offset = Point::zero) const; + bool overlaps(const Collider* other, Point offset = Point::zero) const; + + void render(Batch& batch) override; + + private: + struct Grid + { + int columns; + int rows; + int tile_size; + std::shared_ptr cells; + }; + + Shape m_shape = Shape::None; + RectI m_rect; + Grid m_grid; + + static bool rect_to_rect(const Collider* a, const Collider* b, Point offset); + static bool rect_to_grid(const Collider* a, const Collider* b, Point offset); + }; +} \ No newline at end of file diff --git a/src/components/mover.cpp b/src/components/mover.cpp new file mode 100644 index 0000000..7768c4b --- /dev/null +++ b/src/components/mover.cpp @@ -0,0 +1,89 @@ +#include "mover.h" +#include "../masks.h" + +using namespace TL; + +bool Mover::move_x(int amount) +{ + if (collider) + { + int sign = Calc::sign(amount); + + while (amount != 0) + { + if (collider->check(Mask::solid, Point(sign, 0))) + { + stop_x(); + return true; + } + + amount -= sign; + entity()->position.x += sign; + } + } + else + { + entity()->position.x += amount; + } +} + +bool Mover::move_y(int amount) +{ + if (collider) + { + int sign = Calc::sign(amount); + + while (amount != 0) + { + if (collider->check(Mask::solid, Point(0, sign))) + { + stop_y(); + return true; + } + + amount -= sign; + entity()->position.y += sign; + } + } + else + { + entity()->position.y += amount; + } +} + +void Mover::stop_x() +{ + speed.x = 0; + m_remainder.x = 0; +} + +void Mover::stop_y() +{ + speed.y = 0; + m_remainder.y = 0; +} + +void Mover::stop() +{ + speed.x = 0; + speed.y = 0; + m_remainder.x = 0; + m_remainder.y = 0; +} + +void Mover::update() +{ + // get the amount we should move, including remainder from the previous frame + Vec2 total = m_remainder + speed * Time::delta; + + // round to integer values since we only move in pixels at a time + Point to_move = Point((int)total.x, (int)total.y); + + // store remainder floating values for next frame + m_remainder.x = total.x - to_move.x; + m_remainder.y = total.y - to_move.y; + + // move by integer values + move_x(to_move.x); + move_y(to_move.y); +} diff --git a/src/components/mover.h b/src/components/mover.h new file mode 100644 index 0000000..9c22964 --- /dev/null +++ b/src/components/mover.h @@ -0,0 +1,28 @@ +#pragma once +#include "../world.h" +#include "collider.h" +#include + +using namespace Blah; + +namespace TL +{ + class Mover : public Component + { + private: + Vec2 m_remainder; + + public: + Collider* collider; + Vec2 speed; + + bool move_x(int amount); + bool move_y(int amount); + + void stop_x(); + void stop_y(); + void stop(); + + void update() override; + }; +} \ No newline at end of file diff --git a/src/components/player.cpp b/src/components/player.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/components/player.h b/src/components/player.h new file mode 100644 index 0000000..a84c172 --- /dev/null +++ b/src/components/player.h @@ -0,0 +1,14 @@ +#pragma once +#include "../world.h" +#include + +using namespace Blah; + +namespace TL +{ + class Player : public Component + { + public: + void update() override; + }; +} \ No newline at end of file diff --git a/src/content.cpp b/src/content.cpp index 0a0119d..632eda5 100644 --- a/src/content.cpp +++ b/src/content.cpp @@ -1,10 +1,21 @@ #include "content.h" +#include "assets/sprite.h" using namespace TL; namespace { FilePath root; + Vector sprites; + Vector subtextures; + TextureRef sprite_atlas; + + struct SpriteInfo + { + String name; + Aseprite aseprite; + uint64_t pack_index; + }; } SpriteFont Content::font; @@ -31,10 +42,94 @@ FilePath Content::path() void Content::load() { + Packer packer; + packer.padding = 0; + + // load the main font font = SpriteFont(path() + "fonts/dogica.ttf", 8, SpriteFont::ASCII); + + // load sprites + Vector sprite_info; + uint64_t pack_index = 0; + { + // get all the sprites + FilePath sprite_path = path() + "sprites/"; + for (auto& it : Directory::enumerate(sprite_path, true)) + { + if (!it.ends_with(".ase")) + continue; + + SpriteInfo* info = sprite_info.expand(); + info->aseprite = Aseprite(it.cstr()); + info->name = String(it.cstr() + sprite_path.length(), it.end() - 4); + } + + // add to the atlas + for (auto& info : sprite_info) + { + info.pack_index = pack_index; + for (auto& frame : info.aseprite.frames) + { + packer.add(pack_index, frame.image); + pack_index++; + } + } + } + + // build the atlas + { + packer.pack(); + sprite_atlas = Texture::create(packer.pages[0]); + + subtextures.expand(packer.entries.size()); + for (auto& entry : packer.entries) + subtextures[entry.id] = Subtexture(sprite_atlas, entry.packed, entry.frame); + } + + // add sprites + for (auto& info : sprite_info) + { + Sprite* sprite = sprites.expand(); + sprite->name = info.name; + sprite->origin = Vec2::zero; + + if (info.aseprite.slices.size() > 0 && info.aseprite.slices[0].has_pivot) + { + sprite->origin = Vec2( + info.aseprite.slices[0].pivot.x, + info.aseprite.slices[0].pivot.y); + } + + for (auto& tag : info.aseprite.tags) + { + Sprite::Animation* anim = sprite->animations.expand(); + anim->name = tag.name; + + for (int i = tag.from; i <= tag.to; i++) + { + Sprite::Frame* frame = anim->frames.expand(); + frame->duration = info.aseprite.frames[i].duration / 1000.0f; + frame->image = subtextures[info.pack_index + i]; + } + } + } } void Content::unload() { font.dispose(); } + +TextureRef Content::atlas() +{ + return sprite_atlas; +} + +const Sprite* Content::find_sprite(const char* name) +{ + for (auto& it : sprites) + if (it.name == name) + return ⁢ + + return nullptr; +} diff --git a/src/content.h b/src/content.h index d19821c..5895516 100644 --- a/src/content.h +++ b/src/content.h @@ -5,6 +5,8 @@ using namespace Blah; namespace TL { + struct Sprite; + class Content { public: @@ -13,5 +15,8 @@ namespace TL static FilePath path(); static void load(); static void unload(); + static TextureRef atlas(); + + static const Sprite* find_sprite(const char* name); }; } \ No newline at end of file diff --git a/src/game.cpp b/src/game.cpp index 6369bb0..d375bb7 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -1,5 +1,9 @@ #include "game.h" #include "content.h" +#include "masks.h" +#include "components/animator.h" +#include "components/collider.h" +#include "components/mover.h" using namespace TL; @@ -13,6 +17,22 @@ void Game::startup() // set batcher to use Nearest Filter batch.default_sampler = TextureSampler(TextureFilter::Nearest); + + // add a test entity + auto en = world.add_entity(Point(100, 60)); + auto an = en->add(Animator("player")); + auto col = en->add(Collider::make_rect(RectI(-4, -8, 8, 8))); + auto mover = en->add(Mover()); + + mover->collider = col; + mover->speed = Vec2(5, 20); + an->play("idle"); + + auto floor = world.add_entity(Point(50, 100)); + auto c2 = floor->add(Collider::make_rect(RectI(0, 0, 100, 16))); + c2->mask = Mask::solid; + + m_draw_colliders = true; } void Game::shutdown() @@ -22,16 +42,30 @@ void Game::shutdown() void Game::update() { + if (Input::pressed(Key::F1)) + m_draw_colliders = !m_draw_colliders; + world.update(); } void Game::render() { // draw gameplay stuff { - buffer->clear(Color::red); + buffer->clear(0x4488aa); + + world.render(batch); + + if (m_draw_colliders) + { + auto collider = world.first(); + while (collider) + { + collider->render(batch); + collider = (Collider*)collider->next(); + } + } - batch.str(Content::font, "Hello World", Vec2(32, 32), Color::white); batch.render(buffer); batch.clear(); } diff --git a/src/game.h b/src/game.h index 53e45e2..326e91e 100644 --- a/src/game.h +++ b/src/game.h @@ -17,5 +17,8 @@ namespace TL void shutdown(); void update(); void render(); + + private: + bool m_draw_colliders; }; } \ No newline at end of file diff --git a/src/masks.h b/src/masks.h new file mode 100644 index 0000000..47d43af --- /dev/null +++ b/src/masks.h @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace TL +{ + struct Mask + { + static constexpr uint32_t solid = 1 << 0; + }; +} \ No newline at end of file diff --git a/src/world.cpp b/src/world.cpp index efca357..e39e487 100644 --- a/src/world.cpp +++ b/src/world.cpp @@ -130,7 +130,7 @@ World::~World() } } -Entity* World::add_entity() +Entity* World::add_entity(Point point) { // create entity instance Entity* instance; @@ -149,6 +149,7 @@ Entity* World::add_entity() m_alive.insert(instance); // assign + instance->position = point; instance->m_world = this; // return new entity! diff --git a/src/world.h b/src/world.h index 1d71993..caa71d3 100644 --- a/src/world.h +++ b/src/world.h @@ -73,7 +73,7 @@ namespace TL public: bool active = true; bool visible = true; - Blah::Vec2 position; + Blah::Point position; World* world(); const World* world() const; @@ -115,7 +115,7 @@ namespace TL World& operator=(World&&) = delete; ~World(); - Entity* add_entity(); + Entity* add_entity(Blah::Point position = Blah::Point(0, 0)); Entity* first_entity(); @@ -249,28 +249,28 @@ namespace TL T* World::first() { uint8_t type = Component::Types::id(); - return m_components_alive[type].first; + return (T*)m_components_alive[type].first; } template const T* World::first() const { uint8_t type = Component::Types::id(); - return m_components_alive[type].first; + return (T*)m_components_alive[type].first; } template T* World::last() { uint8_t type = Component::Types::id(); - return m_components_alive[type].last; + return (T*)m_components_alive[type].last; } template const T* World::last() const { uint8_t type = Component::Types::id(); - return m_components_alive[type].last; + return (T*)m_components_alive[type].last; } template