A 2D action platformer built in Python with Pygame, featuring a Tiled-authored level, animated sprites with a state machine, melee combat and a scrolling camera.
The most complex of my Python projects. Where Pong and Breakout were single-screen games with primitive shapes, this one loads a hand-designed level from an external map file, drives multi-state character animations, and implements combat between the player and enemies.
The player moves through a tile-based world, fights slimes with a katana, and reaches a portal at the end of the level.
- Tiled map integration — the level is authored in the Tiled editor and loaded at runtime via
pytmx, with separate layers for ground tiles, decorations, collision boxes, character spawns, enemies and the portal - Animation state machine — the player switches between idle, run, attack and hurt animation sets, with frame index reset on every state change
- Scrolling camera that follows the player through a custom sprite group
- Melee combat with an attack hitbox spawned in the facing direction, a 2-second cooldown, and damage applied to any enemy inside it
- Enemy AI — slimes patrol within a bounded rectangle defined in the map, flipping direction and mirroring their sprite frames at the edges
- Hit feedback — damaged enemies flash white for 100ms using a pygame mask converted to a surface
- Damage immunity window so the player cannot be hit repeatedly within one second
- Sound effects for attacks and deaths
- Separate hitbox from sprite rectangle, so collision uses a clean 32×32 box independent of the animation frame's dimensions
Requires Python 3.10+, pygame-ce and pytmx.
git clone https://github.com/AugustSud/Python-Game-Platformer-Lost-in-Time.git
cd "Python-Game-Platformer-Lost-in-Time/Lost In Time LVL 1"
pip install pygame-ce pytmx
python Code/main.pyRun from the Lost In Time LVL 1 directory, not from inside Code/. All assets are loaded through relative paths (Background/, Data/, Sounds/), so the working directory has to be the level folder.
| Input | Action |
|---|---|
| ← → | Move |
| Space | Jump |
| Left mouse button | Attack |
Lost In Time LVL 1/
├── Code/
│ ├── main.py # Game class: asset loading, map parsing, game loop
│ ├── sprites.py # Sprite, AnimatedSprite, Player, Slime, Portal, Decoration
│ ├── settings.py # window, tile size, imports
│ ├── groups.py # AllSprites — camera-offset sprite group
│ └── support.py # asset import helpers
├── Data/
│ ├── Character/ # idle, running, attack and hurt animation frames
│ ├── Monsters/ # slime idle and death frames
│ ├── Map/ # World_1.tmx, authored in Tiled
│ └── Portal/
├── Background/
└── Sounds/
Class hierarchy. Sprite holds a surface and a rectangle. AnimatedSprite extends it with a frame list and an animate() method that advances a float index by animation speed × delta time, then wraps with a modulo — so animation speed is independent of frame rate. Player, Slime and Portal all build on AnimatedSprite and only add their own behaviour.
Animation states. Rather than one flat frame list, animated characters hold a dictionary of frame lists keyed by state. Switching state swaps the active list and resets the frame index:
def change_state(self, new_state):
if self.state != new_state:
self.state = new_state
self.frames = self.all_frames[self.state]
self.frames_index = 0The guard matters — without it, calling change_state every frame with the same value would freeze the animation on its first frame.
Frame ordering. Animation files are named inconsistently across folders (idle_1.png, 1.png), and a plain alphabetical sort puts frame 10 before frame 2. import_folder() sorts by the trailing integer in the filename instead, so animations play in the right order regardless of naming convention.
Hitbox separate from sprite. The player's collision uses a dedicated 32×32 FRect rather than the sprite rectangle, because animation frames vary in size — the attack frames are wider than the idle ones, and using them for collision would make the player's physical size change mid-swing.
Axis-separated collision with gravity. Horizontal movement is applied and resolved first, then gravity is added to vertical velocity and vertical movement is resolved. Resolving a vertical collision downward also zeroes the vertical velocity, which is what stops the player accumulating gravity while standing on the ground.
- Player health is set to 1000, effectively making the player immortal — a debugging value that was never reverted.
die()callspygame.quit()andexit()rather than showing a death screen, so the process terminates on death.- One level only, and the portal has no transition behaviour attached yet.
- No pause menu, no UI, and no health bar.
tempCodeRunnerFile.pyis a VS Code artifact that should not be in the repository.
This was the project where separating concerns stopped being theory. Loading a level from Tiled instead of hardcoding tile positions meant level design and game logic became genuinely independent — I could redesign the whole map without touching a line of Python.
The animation state machine took the longest to get right. My first version changed the frame list without resetting the index, which crashed whenever the new state had fewer frames than the old one. Debugging that taught me more about index bounds than any exercise had.
