Overview
This is a 2D game built entirely in Java, complete with custom made character sprites and tile map, both made by me using the pixel-art tool 'Pyxel Edit'. It started as an experiment in how two moving hitboxes should push each other apart, and grew from there: a player-controlled character can walk, swim, or fly across different terrain depending on the chosen character, and enemies such as bats and slimes have their own unique animations and register hits when colliding with the player. The animation cycles switches in automatically based on the character's current movement and direction, and the map itself is organized into separate background, obstacle, and decoration layers.
Architecture & Implementation
The game runs on a fixed 60-frames-per-second loop that each tick reads keyboard input, advances every entity's position, resolves collisions, and repaints the screen. There's no external game engine underneath — windowing and drawing are plain Java Swing/AWT, with two small libraries carrying specialized work: EJML for the collision math and Jackson for parsing the exported tile-map JSON.
flowchart LR
subgraph Loop["Game Loop"]
GP["Frame loop<br/>(60 FPS)"]
GM["Game state"]
end
subgraph Collision["Collision"]
EC["Hitbox projection"]
GX["Hitbox matrices (EJML)"]
end
subgraph World["World Data"]
TM[("Tile-map JSON")]
BP["Map renderer"]
end
subgraph Anim["Animation"]
AD["Animation data"]
ER["Sprite renderer"]
end
GP --> GM --> EC --> GX
TM --> BP --> GM
GM --> ER --> ADHitboxes are represented as 2x2 matrices, built with EJML, holding an axis-aligned box's left, right, top, and bottom edges. Before an entity actually moves, its hitbox is projected forward by the pending velocity and checked against every obstacle's hitbox, so a collision is caught before it happens rather than corrected after the entity has already overlapped a wall.

Knowing that two boxes overlap isn't enough to know which way to push the character back, so the game compares how far the box penetrated on the X axis against how far it moved on X that frame, and does the same for Y. Whichever ratio is smaller tells which axis the collision actually came from. That comparison breaks down once a character hits more than one obstacle in the same frame. Adjusting for the nearer obstacle's axis first can end up correcting the wrong direction entirely.

The fix sorts every obstacle the entity is currently overlapping by how large the overlap area is, then resolves the largest one first before moving on to the rest. This is capped at 20 passes per frame as a safety valve. This is still not bulletproof for very thin obstacles, or when the velocity vector reaches too far and might in those cases still behave irregularly.

World data and art are handled separately from collision, and both were built by me rather than borrowed. The tileset sheets, background layers, and entity sprites are pixel art assets that I've worked on myself. The tile map itself was laid out in Pyxel Edit and exported as JSON. That JSON is baked into one full-map image spanning the background, obstacle, and decoration layers, with any tile flagged as blocked becoming an obstacle. Some tiles, like water, only block certain movement types, so the character can swim or fly over water, but not walk on it. A small animation system slices custom made sprite sheets into timed frame sequences and picks the right movement loop based on the character's current direction and movement type, with a priority value so a one-off animation, such as the dash effect, can interrupt a looping one.
The visible map itself is built from several stacked image layers rather than one hand-painted scene: a base ground layer, an obstacle layer, and a decoration layer are combined once into a single background image, and a separate overlay layer is drawn afterward so a handful of rendered elements can appear to sit above the player and entities instead of behind them. Underneath all of that, the map is really just a grid of tile indices, not hand-drawn geometry, so what reads as one continuous world is actually a small set of tile images repeated wherever their index shows up on the grid.
That tile-grid approach was a deliberate choice rather than the easiest one. Painting the whole map as a single image would have been simpler, but representing the world as an index into a small, reusable set of tiles kept the door open to generating a map procedurally instead of placing every piece of it by hand, in keeping with the same exploratory spirit as the original collision problem. It also had an immediate, practical benefit: since every occurrence of a tile on the map points back to the same source image, changing what a single tile looks like updates it everywhere that tile appears across the whole map, without touching the map layout at all. It also means the collision box for a tile doesn't have to match its full square: an obstacle tile can be given a hitbox that's smaller or offset from its artwork, so a tile can look like a whole rock or fence post while only the part a character would actually bump into blocks movement.