guitarHeroButBetter/py/engine/prefabs/services.h
James Whiteman 3f7b409302 Python code
2026-01-30 21:16:42 -08:00

1160 lines
34 KiB
C++

#pragma once
#include <LDtkLoader/Project.hpp>
#include "engine/framework.h"
#include "engine/physics_debug.h"
#include "engine/raycasts.h"
/**
* For when you want multiple of the same service.
*/
template <typename T>
class MultiService : public Service
{
public:
std::unordered_map<std::string, std::unique_ptr<T>> services;
MultiService() = default;
/**
* Initialize all services.
*/
void init_service() override
{
for (auto& service : services)
{
service.second->init();
}
Service::init_service();
}
/**
* Update all services.
*/
void update(float delta_time) override
{
for (auto& service : services)
{
service.second->update();
}
Service::update(delta_time);
}
/**
* Draw all services.
*/
void draw() override
{
for (auto& service : services)
{
service.second->draw();
}
}
/**
* Add a service to the MultiService.
*
* @param name The name to give the service.
* @param service The service to add.
*/
void add_service(std::string name, std::unique_ptr<T> service)
{
static_assert(std::is_base_of<Service, T>::value, "T must derive from Service");
services[name] = std::move(service);
}
/**
* Create a service and add it to the MultiService.
*
* @param name The name to give the service.
* @param args The arguments to forward to the service constructor.
* @return A pointer to the added service.
*/
template <typename... TArgs>
T* add_service(std::string name, TArgs&&... args)
{
static_assert(std::is_base_of<Service, T>::value, "T must derive from Service");
auto new_service = std::make_unique<T>(std::forward<TArgs>(args)...);
T* service_ptr = new_service.get();
add_service(name, std::move(new_service));
return service_ptr;
}
/**
* Get a service by name.
*
* @param name The name of the service.
* @return A pointer to the service.
*/
T* get_service(std::string name)
{
return services[name].get();
}
};
/**
* Service for managing textures.
* Useful when you don't want to load the same texture multiple times.
*/
class TextureService : public Service
{
public:
std::unordered_map<std::string, Texture2D> textures;
TextureService() = default;
~TextureService()
{
for (auto& pair : textures)
{
UnloadTexture(pair.second);
}
}
/**
* Get a texture by filename.
* Loads the texture if it is not already loaded.
*
* @param filename The filename of the texture.
* @return A reference to the texture.
*/
Texture2D& get_texture(const std::string& filename)
{
if (textures.find(filename) == textures.end())
{
Texture2D texture = LoadTexture(filename.c_str());
textures[filename] = texture;
}
return textures[filename];
}
};
/**
* Service for managing sounds.
* Useful when you don't want to load the same sound multiple times and want to play overlapping sounds.
*/
class SoundService : public Service
{
public:
std::unordered_map<std::string, std::vector<Sound>> sounds;
SoundService() = default;
~SoundService()
{
for (auto& pair : sounds)
{
// The first sound is a real sound.
UnloadSound(pair.second[0]);
for (int i = 1; i < pair.second.size(); i++)
{
UnloadSoundAlias(pair.second[i]);
}
}
}
/**
* Get a sound by filename.
* Loads the sound if it is not already loaded.
* Creates a new alias if the sound is already loaded to allow overlapping sounds.
*
* @param filename The filename of the sound.
* @return A reference to the sound.
*/
Sound& get_sound(const std::string& filename)
{
if (sounds.find(filename) == sounds.end())
{
Sound sound = LoadSound(filename.c_str());
sounds[filename] = {sound};
}
else
{
// Create a new alias to allow overlapping sounds.
Sound sound = LoadSoundAlias(sounds[filename][0]);
sounds[filename].push_back(sound);
}
return sounds[filename].back();
}
};
/**
* Service for managing the physics world.
*/
class PhysicsService : public Service
{
public:
b2WorldId world = b2_nullWorldId;
b2Vec2 gravity = {0.0f, 10.0f};
float time_step = 1.0f / 60.0f;
int sub_steps = 6;
float meters_to_pixels = 30.0f;
float pixels_to_meters = 1.0f / meters_to_pixels;
PhysicsDebugRenderer debug_draw;
/**
* Constructor for PhysicsService.
*
* @param gravity The gravity vector for the physics world.
* @param time_step The time step for the physics simulation.
* @param sub_steps The number of sub-steps for the physics simulation.
* @param meters_to_pixels The scale factor from meters to pixels.
*/
PhysicsService(b2Vec2 gravity = b2Vec2{0.0f, 10.0f},
float time_step = 1.0f / 60.0f,
int sub_steps = 6,
float meters_to_pixels = 30.0f) :
gravity(gravity),
time_step(time_step),
sub_steps(sub_steps),
meters_to_pixels(meters_to_pixels),
pixels_to_meters(1.0f / meters_to_pixels)
{
}
~PhysicsService()
{
if (b2World_IsValid(world))
{
b2DestroyWorld(world);
}
}
/**
* Initialize the physics world.
*/
void init() override
{
b2WorldDef world_def = b2DefaultWorldDef();
world_def.gravity = gravity;
world_def.contactHertz = 120;
world = b2CreateWorld(&world_def);
debug_draw.init(meters_to_pixels);
}
/**
* Update the physics world.
*
* @param delta_time The time elapsed since the last frame.
*/
void update(float delta_time) override
{
if (!b2World_IsValid(world))
{
return;
}
b2World_Step(world, time_step, sub_steps);
}
/**
* Draw the physics debug information.
*/
void draw_debug()
{
debug_draw.draw_debug(world);
}
/**
* Convert between pixels and meters.
*
* @param meters The value in meters.
* @return The value in pixels.
*/
Vector2 convert_to_pixels(b2Vec2 meters) const
{
const auto converted = meters * meters_to_pixels;
return {converted.x, converted.y};
}
/**
* Convert between pixels and meters.
*
* @param pixels The value in pixels.
* @return The value in meters.
*/
b2Vec2 convert_to_meters(Vector2 pixels) const
{
const auto converted = pixels * pixels_to_meters;
return {converted.x, converted.y};
}
/**
* Convert a length from meters to pixels.
*
* @param meters The length in meters.
* @return The length in pixels.
*/
float convert_to_pixels(float meters) const
{
return meters * meters_to_pixels;
}
/**
* Convert a length from pixels to meters.
*
* @param pixels The length in pixels.
* @return The length in meters.
*/
float convert_to_meters(float pixels) const
{
return pixels * pixels_to_meters;
}
/**
* Raycast in pixels.
*
* @param ignore Box2d body to ignore.
* @param from The start point of the ray.
* @param to The end point of the ray.
* @return A RayHit struct describing the hit.
*/
RayHit raycast(b2BodyId ignore, Vector2 from, Vector2 to)
{
auto start = convert_to_meters(from);
auto translation = convert_to_meters(to - from);
return raycast_closest(world, ignore, start, translation);
}
/**
* Check for circle shape overlaps in pixels.
*
* @param center The center of the circle.
* @param radius The radius of the circle.
* @param ignore_body The body to ignore.
* @return A vector of body IDs that overlap with the circle.
*/
std::vector<b2BodyId> circle_overlap(Vector2 center, float radius, b2BodyId ignore_body = b2_nullBodyId)
{
auto center_m = convert_to_meters(center);
auto radius_m = convert_to_meters(radius);
return circle_hit(world, ignore_body, center_m, radius_m);
}
/**
* Check for rectangle shape overlaps in pixels.
*
* @param rectangle The rectangle to check.
* @param rotation The rotation of the rectangle in degrees.
* @param ignore_body The body to ignore.
* @return A vector of body IDs that overlap with the rectangle.
*/
std::vector<b2BodyId> rectangle_overlap(Rectangle rectangle,
float rotation = 0.0f,
b2BodyId ignore_body = b2_nullBodyId)
{
Vector2 size = {rectangle.width, rectangle.height};
Vector2 center = {rectangle.x + size.x / 2.0f, rectangle.y + size.y / 2.0f};
auto size_m = convert_to_meters(size);
auto center_m = convert_to_meters(center);
return rectangle_hit(world, ignore_body, center_m, size_m, rotation);
}
};
/**
* Hash for ldtk::IntPoint to be used in unordered_map/set.
*/
struct IntPointHash
{
size_t operator()(const ldtk::IntPoint& p) const noexcept
{
std::size_t h1 = std::hash<int>{}(p.x);
std::size_t h2 = std::hash<int>{}(p.y);
return h1 ^ (h2 << 1);
}
};
/**
* Undirected edge between two IntPoints, stored canonically (a < b).
*/
struct Edge
{
ldtk::IntPoint a, b;
};
/**
* Equality operator for Edge.
*
* @param e1 The first edge.
* @param e2 The second edge.
* @return True if the edges are equal, false otherwise.
*/
static inline bool operator==(const Edge& e1, const Edge& e2)
{
return e1.a == e2.a && e1.b == e2.b;
}
/**
* Hash for Edge to be used in unordered_map/set.
*/
struct EdgeHash
{
size_t operator()(const Edge& e) const noexcept
{
IntPointHash h;
std::size_t h1 = h(e.a);
std::size_t h2 = h(e.b);
return h1 ^ (h2 << 1);
}
};
struct LayerRenderer
{
RenderTexture2D renderer;
ldtk::IID layer_iid;
bool visible = true;
};
/**
* Service for managing LDtk levels.
* Depends on TextureService and PhysicsService.
*/
class LevelService : public Service
{
public:
ldtk::Project project;
std::string project_file;
std::string level_name;
std::vector<std::string> collision_names;
std::vector<LayerRenderer> renderers;
std::vector<b2BodyId> layer_bodies;
float scale = 1.0f;
PhysicsService* physics;
/**
* Constructor for LevelService.
*
* @param project_file The path to the LDtk project file.
* @param level_name The name of the level to load.
* @param collision_names The names of the layers to create collision bodies for.
* @param scale The scale factor for the level.
*/
LevelService(std::string project_file,
std::string level_name,
std::vector<std::string> collision_names,
float scale = 1.0f) :
project_file(project_file),
level_name(level_name),
collision_names(collision_names),
scale(scale)
{
}
virtual ~LevelService()
{
for (auto& renderer : renderers)
{
UnloadRenderTexture(renderer.renderer);
}
for (auto& body : layer_bodies)
{
if (b2Body_IsValid(body))
{
b2DestroyBody(body);
}
}
}
/**
* Initialize the level service.
* Loads the LDtk project and level, creates textures and collision bodies.
*/
void init() override
{
if (!FileExists(project_file.c_str()))
{
TraceLog(LOG_FATAL, "LDtk file not found: %s", project_file.c_str());
}
project.loadFromFile(project_file);
const auto& world = project.getWorld();
const auto& levels = world.allLevels();
bool found = false;
for (const auto& level : levels)
{
if (level.name == level_name)
{
found = true;
break;
}
}
if (!found)
{
TraceLog(LOG_FATAL, "LDtk level not found: %s", level_name.c_str());
}
physics = scene->get_service<PhysicsService>();
const auto& level = world.getLevel(level_name);
const auto& layers = level.allLayers();
// Loop through all layers and create textures and collisions bodies.
for (auto& layer : layers)
{
if (!layer.hasTileset())
{
continue;
}
// Load the texture and the renderer.
auto directory = std::string(GetDirectoryPath(project_file.c_str()));
auto tileset_file = directory + "/" + layer.getTileset().path;
if (!FileExists(tileset_file.c_str()))
{
TraceLog(LOG_FATAL, "Tileset file not found: %s", tileset_file.c_str());
}
auto texture_service = scene->get_service<TextureService>();
Texture2D texture = texture_service->get_texture(tileset_file);
RenderTexture2D renderer = LoadRenderTexture(level.size.x, level.size.y);
// Draw all the tiles.
const auto& tiles_vector = layer.allTiles();
BeginTextureMode(renderer);
// Clear with transparency so we can render layers on top of each other.
ClearBackground({0, 0, 0, 0});
for (const auto& tile : tiles_vector)
{
const auto& position = tile.getPosition();
const auto& texture_rect = tile.getTextureRect();
Vector2 dest = {
static_cast<float>(position.x),
static_cast<float>(position.y),
};
Rectangle src = {static_cast<float>(texture_rect.x),
static_cast<float>(texture_rect.y),
static_cast<float>(texture_rect.width) * (tile.flipX ? -1.0f : 1.0f),
static_cast<float>(texture_rect.height) * (tile.flipY ? -1.0f : 1.0f)};
DrawTextureRec(texture, src, dest, WHITE);
}
EndTextureMode();
LayerRenderer layer_renderer;
layer_renderer.renderer = renderer;
layer_renderer.layer_iid = layer.iid;
renderers.push_back(layer_renderer);
// Create bodies.
const auto& size = layer.getGridSize();
auto make_edge = [&](ldtk::IntPoint p0, ldtk::IntPoint p1) -> Edge
{
if (p1.x < p0.x || (p1.x == p0.x && p1.y < p0.y))
std::swap(p0, p1);
return {p0, p1};
};
std::unordered_set<Edge, EdgeHash> edges;
for (int y = 0; y < size.y; y++)
{
for (int x = 0; x < size.x; x++)
{
if (!is_solid(layer, x, y, size))
continue;
// neighbor empty => boundary edge
if (!is_solid(layer, x, y - 1, size))
edges.insert(make_edge({x, y}, {x + 1, y}));
if (!is_solid(layer, x, y + 1, size))
edges.insert(make_edge({x, y + 1}, {x + 1, y + 1}));
if (!is_solid(layer, x - 1, y, size))
edges.insert(make_edge({x, y}, {x, y + 1}));
if (!is_solid(layer, x + 1, y, size))
edges.insert(make_edge({x + 1, y}, {x + 1, y + 1}));
}
}
std::unordered_map<ldtk::IntPoint, std::vector<ldtk::IntPoint>, IntPointHash> adj;
adj.reserve(edges.size() * 2);
for (auto& e : edges)
{
adj[e.a].push_back(e.b);
adj[e.b].push_back(e.a);
}
// Helper to remove an undirected edge from the set as we consume it
auto erase_edge = [&](ldtk::IntPoint p0, ldtk::IntPoint p1) { edges.erase(make_edge(p0, p1)); };
// Walk loops
std::vector<std::vector<ldtk::IntPoint>> loops;
while (!edges.empty())
{
// pick an arbitrary remaining edge
Edge startE = *edges.begin();
ldtk::IntPoint start = startE.a;
ldtk::IntPoint cur = startE.b;
ldtk::IntPoint prev = start;
std::vector<ldtk::IntPoint> poly;
poly.push_back(start);
poly.push_back(cur);
erase_edge(start, cur);
while (!(cur == start))
{
// choose next neighbor that is not prev and still has an edge remaining
const auto& nbs = adj[cur];
ldtk::IntPoint next = prev; // fallback
bool found = false;
for (const ldtk::IntPoint& cand : nbs)
{
if (cand == prev)
continue;
if (edges.find(make_edge(cur, cand)) != edges.end())
{
next = cand;
found = true;
break;
}
}
if (!found)
{
// Open chain (should be rare for tile boundaries unless the boundary touches the map edge)
break;
}
prev = cur;
cur = next;
poly.push_back(cur);
erase_edge(prev, cur);
// safety guard to avoid infinite loops on bad topology
if (poly.size() > 100000)
break;
}
// If closed, last vertex == start; Box2D chains usually want NOT duplicated end vertex.
if (!poly.empty() && poly.back() == poly.front())
{
poly.pop_back();
}
// Only keep valid chains
if (poly.size() >= 3)
{
// If we're not solid on the right, then we wrapped the wrong way.
if (!loop_has_solid_on_right(poly, layer))
{
std::reverse(poly.begin(), poly.end());
}
// Not really necessary but here we reduce the number of points on a line to just the ends.
// std::vector<ldtk::IntPoint> reduced;
// reduced.push_back(poly[0]);
// b2Vec2 original_normal = {0, 0};
// for (int i = 1; i < poly.size(); i++)
// {
// auto first = poly[i - 1];
// auto second = poly[i];
// float length = sqrt((second.x - first.x) * (second.x - first.x) +
// (second.y - first.y) * (second.y - first.y));
// b2Vec2 normal = {(second.x - first.x) / length, (second.y - first.y) / length};
// if (length == 0)
// {
// normal = {0, 0};
// }
// if (i == 1)
// {
// original_normal = normal;
// }
// if (normal != original_normal)
// {
// reduced.push_back(first);
// original_normal = normal;
// }
// }
// reduced.push_back(poly.back());
// loops.push_back(std::move(reduced));
loops.push_back(std::move(poly));
}
}
b2BodyDef bd = b2DefaultBodyDef();
bd.type = b2_staticBody;
bd.position = {0, 0};
assert(b2World_IsValid(physics->world));
b2BodyId layer_body = b2CreateBody(physics->world, &bd);
for (auto& loop : loops)
{
std::vector<b2Vec2> verts;
verts.reserve(loop.size());
for (auto& p : loop)
{
float xpx = p.x * layer.getCellSize() * scale;
float ypx = p.y * layer.getCellSize() * scale;
verts.push_back(physics->convert_to_meters({xpx, ypx}));
}
std::vector<b2SurfaceMaterial> mats;
for (int i = 0; i < verts.size(); i++)
{
b2SurfaceMaterial mat = b2DefaultSurfaceMaterial();
mat.friction = 0.1f;
mat.restitution = 0.1f;
mats.push_back(mat);
}
b2ChainDef cd = b2DefaultChainDef();
cd.points = verts.data();
cd.count = (int)verts.size();
cd.materials = mats.data();
cd.materialCount = (int)mats.size();
cd.isLoop = true;
b2CreateChain(layer_body, &cd);
layer_bodies.push_back(layer_body);
}
}
}
/**
* Draw the level.
* Draws all the layer renderers.
*/
void draw() override
{
// Draw renderers in reverse.
for (int i = (int)renderers.size() - 1; i >= 0; i--)
{
const auto& layer_renderer = renderers[i];
if (!layer_renderer.visible)
{
continue;
}
const auto& renderer = layer_renderer.renderer;
Rectangle src = {0,
0,
static_cast<float>(renderer.texture.width),
-static_cast<float>(renderer.texture.height)};
Rectangle dest = {0,
0,
static_cast<float>(renderer.texture.width) * scale,
static_cast<float>(renderer.texture.height) * scale};
DrawTexturePro(renderer.texture, src, dest, {0}, .0f, WHITE);
}
}
/**
* Draw a specific layer by its IID.
*
* @param layer_id The IID of the layer.
*/
void draw_layer(ldtk::IID layer_id)
{
for (const auto& layer_renderer : renderers)
{
if (layer_renderer.layer_iid == layer_id)
{
const auto& renderer = layer_renderer.renderer;
Rectangle src = {0,
0,
static_cast<float>(renderer.texture.width),
-static_cast<float>(renderer.texture.height)};
Rectangle dest = {0,
0,
static_cast<float>(renderer.texture.width) * scale,
static_cast<float>(renderer.texture.height) * scale};
DrawTexturePro(renderer.texture, src, dest, {0}, .0f, WHITE);
return;
}
}
}
/**
* Draw a specific layer by its name.
*
* @param layer_name The name of the layer.
*/
void draw_layer(std::string layer_name)
{
const auto& level = get_level();
const auto& layer = level.getLayer(layer_name);
draw_layer(layer.iid);
}
/**
* Check if a cell in the layer is solid.
* Used for collision generation.
*
* @param layer The LDtk layer.
* @param x The x coordinate of the cell.
* @param y The y coordinate of the cell.
* @param size The size of the layer in cells.
*/
bool is_solid(const ldtk::Layer& layer, int x, int y, const ldtk::IntPoint& size)
{
if (x < 0 || y < 0 || x >= size.x || y >= size.y)
{
return false;
}
std::string name = layer.getIntGridVal(x, y).name;
if (std::find(collision_names.begin(), collision_names.end(), name) != collision_names.end())
{
return true;
}
return false;
};
/**
* Check if there is solid on the right side of a loop of corners.
* Used to determine loop winding.
*
* @param loop_corners The corners of the loop.
* @param layer The LDtk layer.
* @return True if there is solid on the right side of the loop, false otherwise.
*/
bool loop_has_solid_on_right(const std::vector<ldtk::IntPoint>& loop_corners, const ldtk::Layer& layer)
{
const int cell_size = layer.getCellSize();
// Pick an edge with non-zero length.
int n = (int)loop_corners.size();
for (int i = 0; i < n; ++i)
{
ldtk::IntPoint a = loop_corners[i];
ldtk::IntPoint b = loop_corners[(i + 1) % n];
int dx = b.x - a.x;
int dy = b.y - a.y;
if (dx == 0 && dy == 0)
continue;
// Convert corner coords to scaled pixel coords.
float ax = a.x * cell_size * scale;
float ay = a.y * cell_size * scale;
float bx = b.x * cell_size * scale;
float by = b.y * cell_size * scale;
// Edge direction.
float ex = bx - ax;
float ey = by - ay;
float len = std::sqrt(ex * ex + ey * ey);
if (len < 1e-4f)
{
continue;
}
ex /= len;
ey /= len;
// Right normal = (-ey, ex)
float rx = -ey;
float ry = ex;
// Midpoint of the edge.
float mx = 0.5f * (ax + bx);
float my = 0.5f * (ay + by);
// Sample a point slightly to the right, a quarter cell away.
float eps = 0.25f * cell_size * scale;
float sx = mx + rx * eps;
float sy = my + ry * eps;
// Map sample pixel to grid cell.
int gx = (int)std::floor(sx / (cell_size * scale));
int gy = (int)std::floor(sy / (cell_size * scale));
return is_solid(layer, gx, gy, layer.getGridSize());
}
// Fallback: if degenerate, say false
return false;
}
/**
* Set the visibility of a layer by its IID.
*
* @param layer_id The IID of the layer.
* @param visible True to make the layer visible, false to hide it.
*/
void set_layer_visibility(ldtk::IID layer_id, bool visible)
{
for (auto& layer_renderer : renderers)
{
if (layer_renderer.layer_iid == layer_id)
{
layer_renderer.visible = visible;
return;
}
}
}
/**
* Set the visibility of a layer by its name.
*
* @param layer_name The name of the layer.
* @param visible True to make the layer visible, false to hide it.
*/
void set_layer_visibility(std::string layer_name, bool visible)
{
const auto& level = get_level();
const auto& layer = level.getLayer(layer_name);
for (auto& layer_renderer : renderers)
{
if (layer_renderer.layer_iid == layer.iid)
{
layer_renderer.visible = visible;
return;
}
}
}
/**
* Get the LDtk world.
*
* @return A reference to the LDtk world.
*/
const ldtk::World& get_world()
{
return project.getWorld();
}
/**
* Get the LDtk level.
*
* @return A reference to the LDtk level.
*/
const ldtk::Level& get_level()
{
const auto& world = project.getWorld();
return world.getLevel(level_name);
}
/**
* Get the level size in pixels.
*
* @return A Vector2 containing the size of the level.
*/
Vector2 get_size()
{
const auto& level = get_level();
return {level.size.x * scale, level.size.y * scale};
}
/**
* Get a layer by its name.
*
* @param name The name of the layer.
* @return A reference to the LDtk layer.
*/
const ldtk::Layer& get_layer_by_name(const std::string& name)
{
const auto& level = get_level();
return level.getLayer(name);
}
/**
* Get all entities across all layers in the level.
*
* @return A vector of LDtk entities.
*/
std::vector<const ldtk::Entity*> get_entities()
{
if (!is_init)
{
TraceLog(LOG_ERROR, "LDtk project not loaded.");
return {};
}
const auto& level = get_level();
const auto& layers = level.allLayers();
std::vector<const ldtk::Entity*> entities;
for (const auto& layer : layers)
{
const auto& layer_entities = layer.allEntities();
entities.reserve(entities.size() + layer_entities.size());
for (const auto& entity : layer_entities)
{
entities.push_back(&entity);
}
}
return entities;
}
/**
* Get all entities across all layers in the level with the given name.
*
* @param name The name of the entities to get.
* @return A vector of LDtk entities.
*/
std::vector<const ldtk::Entity*> get_entities_by_name(const std::string& name)
{
if (!is_init)
{
TraceLog(LOG_ERROR, "LDtk project not loaded.");
return {};
}
const auto& level = get_level();
const auto& layers = level.allLayers();
std::vector<const ldtk::Entity*> entities;
for (const auto& layer : layers)
{
const auto& layer_entities = layer.getEntitiesByName(name);
entities.reserve(entities.size() + layer_entities.size());
for (const auto& entity : layer_entities)
{
entities.push_back(&entity.get());
}
}
return entities;
}
/**
* Get all entities across all layers in the level with the given tag.
*
* @param tag The tag of the entities to get.
* @return A vector of LDtk entities.
*/
std::vector<const ldtk::Entity*> get_entities_by_tag(const std::string& tag)
{
if (!is_init)
{
TraceLog(LOG_ERROR, "LDtk project not loaded.");
return {};
}
const auto& level = get_level();
const auto& layers = level.allLayers();
std::vector<const ldtk::Entity*> entities;
for (const auto& layer : layers)
{
const auto& layer_entities = layer.getEntitiesByTag(tag);
entities.reserve(entities.size() + layer_entities.size());
for (const auto& entity : layer_entities)
{
entities.push_back(&entity.get());
}
}
return entities;
}
/**
* Get the first entity across all layers in the level with the given name.
*
* @param name The name of the entity to get.
* @return A pointer to the LDtk entity, or nullptr if not found.
*/
const ldtk::Entity* get_entity_by_name(const std::string& name)
{
auto entities = get_entities_by_name(name);
if (entities.empty())
{
return nullptr;
}
return entities[0];
}
/**
* Get the first entity across all layers in the level with the given tag.
*
* @param tag The tag of the entity to get.
* @return A pointer to the LDtk entity, or nullptr if not found.
*/
const ldtk::Entity* get_entity_by_tag(const std::string& tag)
{
auto entities = get_entities_by_tag(tag);
if (entities.empty())
{
return nullptr;
}
return entities[0];
}
/**
* Convert a grid point to pixels.
*
* @param point The grid point to convert.
* @return A Vector2 containing the point in pixels.
*/
Vector2 convert_to_pixels(const ldtk::IntPoint& point) const
{
return {point.x * scale, point.y * scale};
}
/**
* Convert a cell point to pixels.
*
* @param cell_point The cell point to convert.
* @param layer The LDtk layer the cell point is in.
*/
Vector2 convert_cells_to_pixels(const ldtk::IntPoint& cell_point, const ldtk::Layer& layer) const
{
float cell_size = static_cast<float>(layer.getCellSize());
return {cell_point.x * cell_size * scale, cell_point.y * cell_size * scale};
}
/**
* Convert a grid point to meters.
*
* @param point The grid point to convert.
* @return A b2Vec2 containing the point in meters.
*/
b2Vec2 convert_to_meters(const ldtk::IntPoint& point) const
{
return physics->convert_to_meters(convert_to_pixels(point));
}
/**
* Convert pixels to a grid point.
*
* @param pixels The pixel position to convert.
* @return An IntPoint containing the point in grid coordinates.
*/
ldtk::IntPoint convert_to_grid(const Vector2& pixels) const
{
return {static_cast<int>(pixels.x / scale), static_cast<int>(pixels.y / scale)};
}
/**
* Convert meters to a grid point.
*
* @param meters The meter position to convert.
* @return An IntPoint containing the point in grid coordinates.
*/
ldtk::IntPoint convert_to_grid(const b2Vec2& meters) const
{
auto pixels = physics->convert_to_pixels(meters);
return {static_cast<int>(pixels.x / scale), static_cast<int>(pixels.y / scale)};
}
/**
* Get the position of an entity in pixels.
*
* @param entity The entity to get the position of.
* @return A Vector2 containing the position of the entity in pixels.
*/
Vector2 get_entity_position(ldtk::Entity* entity)
{
return convert_to_pixels(entity->getPosition());
}
/**
* Get the size of an entity in pixels.
*
* @param entity The entity to get the size of.
* @return A Vector2 containing the size of the entity in pixels.
*/
Vector2 get_entity_size(ldtk::Entity* entity)
{
return convert_to_pixels(entity->getSize());
}
};