add new skill game-engine

This commit is contained in:
jhauga
2026-02-22 23:04:42 -05:00
parent dc8b0cc546
commit 52d3754eaa
16 changed files with 11114 additions and 0 deletions

View File

@@ -0,0 +1,528 @@
# 2D Maze Game Template
A mobile-optimized 2D maze game where players guide a ball through a labyrinth of obstacles to reach a target hole. The game uses the **Device Orientation API** for tilt-based motion controls on mobile devices and keyboard arrow keys on desktop. Built with the **Phaser** framework (v2.x with Arcade Physics), it features multi-level progression, collision detection, audio feedback, vibration haptics, and a timer system.
**Source reference:** [MDN - HTML5 Gamedev Phaser Device Orientation](https://developer.mozilla.org/en-US/docs/Games/Tutorials/HTML5_Gamedev_Phaser_Device_Orientation)
**Live demo:** [Cyber Orb](https://orb.enclavegames.com/)
**Source code:** [GitHub - EnclaveGames/Cyber-Orb](https://github.com/EnclaveGames/Cyber-Orb)
---
## Game Concept
The player controls a ball (the "orb") by tilting their mobile device or pressing arrow keys. The ball rolls through a maze of horizontal and vertical wall segments. The objective on each level is to navigate the ball to a hole at the top of the screen while avoiding walls. Collisions with walls trigger a bounce, a sound effect, and optional vibration. A timer tracks how long the player takes per level and across the entire game.
---
## Project Structure
```
project/
index.html
src/
phaser-arcade-physics.2.2.2.min.js
Boot.js
Preloader.js
MainMenu.js
Howto.js
Game.js
img/
ball.png
hole.png
element-horizontal.png
element-vertical.png
button-start.png
loading-bg.png
loading-bar.png
audio/
bounce.ogg
bounce.mp3
bounce.m4a
```
---
## Phaser Setup and Initialization
### HTML Entry Point
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Cyber Orb</title>
<style>
body { margin: 0; background: #333; }
</style>
<script src="src/phaser-arcade-physics.2.2.2.min.js"></script>
<script src="src/Boot.js"></script>
<script src="src/Preloader.js"></script>
<script src="src/MainMenu.js"></script>
<script src="src/Howto.js"></script>
<script src="src/Game.js"></script>
</head>
<body>
<script>
(() => {
const game = new Phaser.Game(320, 480, Phaser.CANVAS, "game");
game.state.add("Boot", Ball.Boot);
game.state.add("Preloader", Ball.Preloader);
game.state.add("MainMenu", Ball.MainMenu);
game.state.add("Howto", Ball.Howto);
game.state.add("Game", Ball.Game);
game.state.start("Boot");
})();
</script>
</body>
</html>
```
- Canvas size: `320 x 480`
- Renderer: `Phaser.CANVAS` (alternatives: `Phaser.WEBGL`, `Phaser.AUTO`)
---
## Game State Architecture
The game follows a linear state flow:
```
Boot --> Preloader --> MainMenu --> Howto --> Game
```
### Boot State
Loads minimal assets for the loading screen and configures scaling.
```javascript
const Ball = {
_WIDTH: 320,
_HEIGHT: 480,
};
Ball.Boot = function (game) {};
Ball.Boot.prototype = {
preload() {
this.load.image("preloaderBg", "img/loading-bg.png");
this.load.image("preloaderBar", "img/loading-bar.png");
},
create() {
this.game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this.game.scale.pageAlignHorizontally = true;
this.game.scale.pageAlignVertically = true;
this.game.state.start("Preloader");
},
};
```
### Preloader State
Displays a visual loading bar while loading all game assets. Audio is loaded in multiple formats for cross-browser compatibility.
```javascript
Ball.Preloader = function (game) {};
Ball.Preloader.prototype = {
preload() {
this.preloadBg = this.add.sprite(
(Ball._WIDTH - 297) * 0.5,
(Ball._HEIGHT - 145) * 0.5,
"preloaderBg"
);
this.preloadBar = this.add.sprite(
(Ball._WIDTH - 158) * 0.5,
(Ball._HEIGHT - 50) * 0.5,
"preloaderBar"
);
this.load.setPreloadSprite(this.preloadBar);
this.load.image("ball", "img/ball.png");
this.load.image("hole", "img/hole.png");
this.load.image("element-w", "img/element-horizontal.png");
this.load.image("element-h", "img/element-vertical.png");
this.load.spritesheet("button-start", "img/button-start.png", 146, 51);
this.load.audio("audio-bounce", [
"audio/bounce.ogg",
"audio/bounce.mp3",
"audio/bounce.m4a",
]);
},
create() {
this.game.state.start("MainMenu");
},
};
```
### MainMenu State
Displays the title screen with a start button.
```javascript
Ball.MainMenu = function (game) {};
Ball.MainMenu.prototype = {
create() {
this.add.sprite(0, 0, "screen-mainmenu");
this.gameTitle = this.add.sprite(Ball._WIDTH * 0.5, 40, "title");
this.gameTitle.anchor.set(0.5, 0);
this.startButton = this.add.button(
Ball._WIDTH * 0.5, 200, "button-start",
this.startGame, this,
2, 0, 1 // hover, out, down frames
);
this.startButton.anchor.set(0.5, 0);
this.startButton.input.useHandCursor = true;
},
startGame() {
this.game.state.start("Howto");
},
};
```
### Howto State
A single-click instruction screen before gameplay begins.
```javascript
Ball.Howto = function (game) {};
Ball.Howto.prototype = {
create() {
this.buttonContinue = this.add.button(
0, 0, "screen-howtoplay",
this.startGame, this
);
},
startGame() {
this.game.state.start("Game");
},
};
```
---
## Device Orientation API Usage
The Device Orientation API provides real-time data about the physical tilt of a device. Two axes are used:
| Property | Axis | Range | Effect |
|----------|------|-------|--------|
| `event.gamma` | Left/right tilt | -90 to 90 degrees | Horizontal ball velocity |
| `event.beta` | Front/back tilt | -180 to 180 degrees | Vertical ball velocity |
### Registering the Listener
```javascript
// In the Game state's create() method
window.addEventListener("deviceorientation", this.handleOrientation);
```
### Handling Orientation Events
```javascript
handleOrientation(e) {
const x = e.gamma; // left-right tilt
const y = e.beta; // front-back tilt
Ball._player.body.velocity.x += x;
Ball._player.body.velocity.y += y;
}
```
### Tilt Behavior
- Tilt device left: negative gamma, ball rolls left
- Tilt device right: positive gamma, ball rolls right
- Tilt device forward: positive beta, ball rolls down
- Tilt device backward: negative beta, ball rolls up
The tilt angle directly maps to velocity increments -- the steeper the tilt, the greater the force applied to the ball each frame.
---
## Core Game Mechanics
### Game State Structure
```javascript
Ball.Game = function (game) {};
Ball.Game.prototype = {
create() {},
initLevels() {},
showLevel(level) {},
updateCounter() {},
managePause() {},
manageAudio() {},
update() {},
wallCollision() {},
handleOrientation(e) {},
finishLevel() {},
};
```
### Ball Creation and Physics
```javascript
// In create()
this.ball = this.add.sprite(this.ballStartPos.x, this.ballStartPos.y, "ball");
this.ball.anchor.set(0.5);
this.physics.enable(this.ball, Phaser.Physics.ARCADE);
this.ball.body.setSize(18, 18);
this.ball.body.bounce.set(0.3, 0.3);
```
- Anchor at center `(0.5, 0.5)` for rotation around midpoint
- Physics body: 18x18 pixels
- Bounce coefficient: 0.3 (retains 30% velocity after wall collision)
### Keyboard Controls (Desktop Fallback)
```javascript
// In create()
this.keys = this.game.input.keyboard.createCursorKeys();
// In update()
if (this.keys.left.isDown) {
this.ball.body.velocity.x -= this.movementForce;
} else if (this.keys.right.isDown) {
this.ball.body.velocity.x += this.movementForce;
}
if (this.keys.up.isDown) {
this.ball.body.velocity.y -= this.movementForce;
} else if (this.keys.down.isDown) {
this.ball.body.velocity.y += this.movementForce;
}
```
### Hole (Goal) Setup
```javascript
this.hole = this.add.sprite(Ball._WIDTH * 0.5, 90, "hole");
this.physics.enable(this.hole, Phaser.Physics.ARCADE);
this.hole.anchor.set(0.5);
this.hole.body.setSize(2, 2);
```
The hole has a tiny 2x2 collision body for precise overlap detection.
---
## Level System
### Level Data Format
Each level is an array of wall segment objects with position and type:
```javascript
this.levelData = [
[{ x: 96, y: 224, t: "w" }], // Level 1
[
{ x: 72, y: 320, t: "w" },
{ x: 200, y: 320, t: "h" },
{ x: 72, y: 150, t: "w" },
], // Level 2
// ... more levels
];
```
- `x, y`: Position in pixels
- `t`: Type -- `"w"` for horizontal wall, `"h"` for vertical wall
### Building Levels
```javascript
initLevels() {
for (let i = 0; i < this.maxLevels; i++) {
const newLevel = this.add.group();
newLevel.enableBody = true;
newLevel.physicsBodyType = Phaser.Physics.ARCADE;
for (const item of this.levelData[i]) {
newLevel.create(item.x, item.y, `element-${item.t}`);
}
newLevel.setAll("body.immovable", true);
newLevel.visible = false;
this.levels.push(newLevel);
}
}
```
### Showing a Level
```javascript
showLevel(level) {
const lvl = level || this.level;
if (this.levels[lvl - 2]) {
this.levels[lvl - 2].visible = false;
}
this.levels[lvl - 1].visible = true;
}
```
---
## Collision Detection
### Wall Collisions (Bounce)
```javascript
// In update()
this.physics.arcade.collide(
this.ball, this.borderGroup,
this.wallCollision, null, this
);
this.physics.arcade.collide(
this.ball, this.levels[this.level - 1],
this.wallCollision, null, this
);
```
`collide` causes the ball to bounce off walls and triggers the callback.
### Hole Overlap (Pass-Through Detection)
```javascript
this.physics.arcade.overlap(
this.ball, this.hole,
this.finishLevel, null, this
);
```
`overlap` detects intersection without physical collision response.
### Wall Collision Callback
```javascript
wallCollision() {
if (this.audioStatus) {
this.bounceSound.play();
}
if ("vibrate" in window.navigator) {
window.navigator.vibrate(100);
}
}
```
---
## Audio System
```javascript
// In create()
this.bounceSound = this.game.add.audio("audio-bounce");
// Toggle
manageAudio() {
this.audioStatus = !this.audioStatus;
}
```
---
## Vibration API
```javascript
if ("vibrate" in window.navigator) {
window.navigator.vibrate(100); // 100ms vibration pulse
}
```
Feature-detect before calling. Provides tactile feedback on supported mobile devices.
---
## Timer System
```javascript
// In create()
this.timer = 0;
this.totalTimer = 0;
this.timerText = this.game.add.text(15, 15, "Time: 0", this.fontBig);
this.totalTimeText = this.game.add.text(120, 30, "Total time: 0", this.fontSmall);
this.time.events.loop(Phaser.Timer.SECOND, this.updateCounter, this);
// Counter callback
updateCounter() {
this.timer++;
this.timerText.setText(`Time: ${this.timer}`);
this.totalTimeText.setText(`Total time: ${this.totalTimer + this.timer}`);
}
```
---
## Level Completion
```javascript
finishLevel() {
if (this.level >= this.maxLevels) {
this.totalTimer += this.timer;
alert(`Congratulations, game completed!\nTotal time: ${this.totalTimer}s`);
this.game.state.start("MainMenu");
} else {
alert(`Level ${this.level} completed!`);
this.totalTimer += this.timer;
this.timer = 0;
this.level++;
this.timerText.setText(`Time: ${this.timer}`);
this.totalTimeText.setText(`Total time: ${this.totalTimer}`);
this.levelText.setText(`Level: ${this.level} / ${this.maxLevels}`);
this.ball.body.x = this.ballStartPos.x;
this.ball.body.y = this.ballStartPos.y;
this.ball.body.velocity.x = 0;
this.ball.body.velocity.y = 0;
this.showLevel();
}
}
```
---
## Complete Update Loop
```javascript
update() {
// Keyboard input
if (this.keys.left.isDown) {
this.ball.body.velocity.x -= this.movementForce;
} else if (this.keys.right.isDown) {
this.ball.body.velocity.x += this.movementForce;
}
if (this.keys.up.isDown) {
this.ball.body.velocity.y -= this.movementForce;
} else if (this.keys.down.isDown) {
this.ball.body.velocity.y += this.movementForce;
}
// Wall collisions
this.physics.arcade.collide(
this.ball, this.borderGroup, this.wallCollision, null, this
);
this.physics.arcade.collide(
this.ball, this.levels[this.level - 1], this.wallCollision, null, this
);
// Hole overlap
this.physics.arcade.overlap(
this.ball, this.hole, this.finishLevel, null, this
);
}
```
---
## Phaser API Quick Reference
| Function | Purpose |
|----------|---------|
| `this.add.sprite(x, y, key)` | Create a game object |
| `this.add.group()` | Create a container for objects |
| `this.add.button(x, y, key, cb, ctx, over, out, down)` | Create interactive button |
| `this.add.text(x, y, text, style)` | Create text display |
| `this.physics.enable(obj, system)` | Enable physics on object |
| `this.physics.arcade.collide(a, b, cb)` | Detect collision with bounce |
| `this.physics.arcade.overlap(a, b, cb)` | Detect overlap without bounce |
| `this.load.image(key, path)` | Load image asset |
| `this.load.spritesheet(key, path, w, h)` | Load sprite animation sheet |
| `this.load.audio(key, paths[])` | Load audio with format fallbacks |
| `this.game.add.audio(key)` | Instantiate audio object |
| `this.time.events.loop(interval, cb, ctx)` | Create repeating timer |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,310 @@
# GameBase Template Repository
A feature-rich, opinionated starter template for 2D game projects built with **Haxe** and the **Heaps** game engine. Created and maintained by **Sebastien Benard** (deepnight), the lead developer behind *Dead Cells*. GameBase provides a production-tested foundation with entity management, level integration via LDtk, rendering pipeline, and a game loop architecture -- all designed to let developers skip boilerplate and jump straight into game-specific logic.
**Repository:** [github.com/deepnight/gameBase](https://github.com/deepnight/gameBase)
**Author:** [Sebastien Benard / deepnight](https://deepnight.net)
**Technology:** Haxe + Heaps (HashLink or JS targets)
**Level editor integration:** [LDtk](https://ldtk.io)
---
## Purpose
GameBase exists to solve the "blank project" problem. Instead of setting up rendering, entity systems, camera controls, debug overlays, and level loading from scratch, developers clone this repository and begin implementing game-specific mechanics immediately. It reflects patterns refined through commercial game development, particularly from the development of *Dead Cells*.
Key benefits:
- Pre-built entity system with grid-based positioning and sub-pixel precision
- LDtk level editor integration for visual level design
- Built-in debug tools and overlays
- Frame-rate independent game loop with fixed-step updates
- Camera system with follow, shake, zoom, and clamp
- Configurable Controller/input management
- Scalable rendering pipeline with Heaps
---
## Repository Structure
```
gameBase/
src/
game/
App.hx -- Application entry point and initialization
Game.hx -- Main game process, holds level and entities
Entity.hx -- Base entity class with grid coords, velocity, animation
Level.hx -- Level loading and collision map from LDtk
Camera.hx -- Camera follow, shake, zoom, clamping
Fx.hx -- Visual effects (particles, flashes, etc.)
Types.hx -- Enums, typedefs, and constants
en/
Hero.hx -- Player entity (example implementation)
Mob.hx -- Enemy entity (example implementation)
import.hx -- Global imports (available everywhere)
res/
atlas/ -- Sprite sheets and texture atlases
levels/ -- LDtk level project files
fonts/ -- Bitmap fonts
.ldtk -- LDtk project file (root)
build.hxml -- Haxe compiler configuration
Makefile -- Build/run shortcuts
README.md
```
---
## Key Files and Their Roles
### `src/game/App.hx` -- Application Entry Point
The main application class that extends `dn.Process`. Handles:
- Window/display initialization
- Scene management (root scene graph)
- Global input controller setup
- Debug toggle and console
```haxe
class App extends dn.Process {
public static var ME : App;
override function init() {
ME = this;
// Initialize rendering, controller, assets
new Game();
}
}
```
### `src/game/Game.hx` -- Game Process
Manages the active game session:
- Holds reference to the current `Level`
- Manages all active `Entity` instances (via a global linked list)
- Handles pause, game-over, and restart logic
- Coordinates camera and effects
```haxe
class Game extends dn.Process {
public var level : Level;
public var hero : en.Hero;
public var fx : Fx;
public var camera : Camera;
public function new() {
super(App.ME);
level = new Level();
fx = new Fx();
camera = new Camera();
hero = new en.Hero();
}
}
```
### `src/game/Entity.hx` -- Base Entity
The core entity class featuring:
- **Grid-based positioning:** `cx`, `cy` (integer cell coordinates) plus `xr`, `yr` (sub-cell ratio 0.0 to 1.0) for smooth sub-pixel movement
- **Velocity and friction:** `dx`, `dy` (velocity) with configurable `frictX`, `frictY`
- **Gravity:** Optional per-entity gravity
- **Sprite management:** Animated sprite via Heaps `h2d.Anim` or `dn.heaps.HSprite`
- **Lifecycle:** `update()`, `fixedUpdate()`, `postUpdate()`, `dispose()`
- **Collision helpers:** `hasCollision(cx, cy)` check against the level collision map
```haxe
class Entity {
// Grid position
public var cx : Int = 0; // Cell X
public var cy : Int = 0; // Cell Y
public var xr : Float = 0.5; // X ratio within cell (0..1)
public var yr : Float = 1.0; // Y ratio within cell (0..1)
// Velocity
public var dx : Float = 0;
public var dy : Float = 0;
// Pixel position (computed)
public var attachX(get,never) : Float;
inline function get_attachX() return (cx + xr) * Const.GRID;
public var attachY(get,never) : Float;
inline function get_attachY() return (cy + yr) * Const.GRID;
// Physics step
public function fixedUpdate() {
xr += dx;
dx *= frictX;
// X collision
if (xr > 1) { cx++; xr--; }
if (xr < 0) { cx--; xr++; }
yr += dy;
dy *= frictY;
// Y collision
if (yr > 1) { cy++; yr--; }
if (yr < 0) { cy--; yr++; }
}
}
```
### `src/game/Level.hx` -- Level Management
Loads and manages level data from LDtk project files:
- Parses tile layers, entity layers, and int grid layers
- Builds a collision grid (`hasCollision(cx, cy)`)
- Provides helper methods to query the level structure
```haxe
class Level {
var data : ldtk.Level;
var collisions : Map<Int, Bool>;
public function new(ldtkLevel) {
data = ldtkLevel;
// Parse IntGrid layer for collision marks
for (cy in 0...data.l_Collisions.cHei)
for (cx in 0...data.l_Collisions.cWid)
if (data.l_Collisions.getInt(cx, cy) == 1)
collisions.set(coordId(cx, cy), true);
}
public inline function hasCollision(cx:Int, cy:Int) : Bool {
return collisions.exists(coordId(cx, cy));
}
}
```
### `src/game/Camera.hx` -- Camera System
Provides:
- **Target tracking:** Follow an entity smoothly with configurable dead zones
- **Shake:** Screen shake with decay
- **Zoom:** Dynamic zoom in/out
- **Clamping:** Keep the camera within level bounds
### `src/game/Fx.hx` -- Effects System
Particle and visual effect management:
- Particle pools
- Screen flash
- Slow-motion helpers
- Color overlay effects
---
## Technology Stack
### Haxe
A cross-platform, high-level programming language that compiles to multiple targets:
- **HashLink (HL):** Native bytecode VM for desktop (primary dev target)
- **JavaScript (JS):** Browser/web target
- **C/C++:** Via HXCPP for native builds
### Heaps (Heaps.io)
A high-performance, cross-platform 2D/3D game engine:
- GPU-accelerated rendering via OpenGL/DirectX/WebGL
- Scene graph architecture with `h2d.Object` hierarchy
- Sprite batching and texture atlases
- Bitmap font rendering
- Input abstraction
### LDtk
A modern, open-source 2D level editor created by Sebastien Benard:
- Visual, tile-based level design
- IntGrid layers for collision and metadata
- Entity layers for game object placement
- Auto-tiling rules
- Haxe API auto-generated from the project file
---
## Setup Instructions
### Prerequisites
1. **Install Haxe** (4.0+): [haxe.org](https://haxe.org/download/)
2. **Install HashLink** (for desktop target): [hashlink.haxe.org](https://hashlink.haxe.org/)
3. **Install LDtk** (for level editing): [ldtk.io](https://ldtk.io/)
### Getting Started
```bash
# Clone the repository
git clone https://github.com/deepnight/gameBase.git my-game
cd my-game
# Install Haxe dependencies
haxelib install heaps
haxelib install deepnightLibs
haxelib install ldtk-haxe-api
# Build and run (HashLink target)
haxe build.hxml
hl bin/client.hl
# Or use the Makefile (if available)
make run
```
### Using as a Starting Point
1. **Clone or use the template** -- Do not fork; clone into a new directory with your game's name.
2. **Rename the package** -- Update `src/game/` package declarations and project references to match your game.
3. **Edit `build.hxml`** -- Adjust the main class, output path, and target as needed.
4. **Design levels in LDtk** -- Open the `.ldtk` file, define your layers and entities, and export.
5. **Implement entities** -- Create new entity classes in `src/game/en/` extending `Entity`.
6. **Iterate** -- Use the debug console (toggle in-game) for live inspection and tuning.
---
## Build Targets
| Target | Command | Output | Use Case |
|--------|---------|--------|----------|
| HashLink | `haxe build.hxml` | `bin/client.hl` | Development, desktop release |
| JavaScript | `haxe build.js.hxml` | `bin/client.js` | Web/browser builds |
| DirectX/OpenGL | Via HL native | Native executable | Production desktop release |
---
## Debug Features
GameBase includes built-in debug tooling:
- **Debug overlay:** Toggle with a key to show entity bounds, grid, velocities, collision map
- **Console:** In-game command console for toggling flags, teleporting, spawning entities
- **FPS counter:** Visible frame-rate and update-rate monitor
- **Process inspector:** View active processes and their hierarchy
---
## Game Loop Architecture
GameBase uses a fixed-timestep game loop pattern:
```
Each frame:
1. preUpdate() -- Input polling, pre-frame logic
2. fixedUpdate() -- Physics, movement, collisions (fixed timestep)
- May run 0-N times per frame to catch up
3. update() -- General per-frame logic
4. postUpdate() -- Sprite position sync, camera update, rendering prep
```
This ensures physics behavior is consistent regardless of frame rate, while rendering and visual updates remain smooth.
---
## Entity Lifecycle
```
Constructor --> init() --> [game loop: fixedUpdate/update/postUpdate] --> dispose()
```
- **Constructor:** Set initial position, create sprite, register in global entity list
- **fixedUpdate():** Physics step (velocity, friction, gravity, collision)
- **update():** AI, state machine, animation triggers
- **postUpdate():** Sync sprite position to grid coordinates, apply visual effects
- **dispose():** Remove from entity list, destroy sprite, clean up references

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,507 @@
# Simple 2D Platformer Engine Template
A grid-based 2D platformer engine tutorial by **Sebastien Benard** (deepnight), the lead developer behind *Dead Cells*. This template covers the fundamental architecture for a performant platformer: a dual-coordinate positioning system that blends integer grid cells with sub-pixel precision, velocity and friction mechanics, gravity, and a robust collision detection and response system. The approach is language-agnostic but examples use Haxe.
**Source references:**
- [Part 1 - Basics](https://deepnight.net/tutorial/a-simple-platformer-engine-part-1-basics/)
- [Part 2 - Collisions](https://deepnight.net/tutorial/a-simple-platformer-engine-part-2-collisions/)
**Author:** [Sebastien Benard / deepnight](https://deepnight.net)
---
## Engine Architecture Overview
The engine is built around a grid-based world where each cell has a fixed pixel size (e.g., 16x16). Entities exist within this grid using a **dual-coordinate system**: integer cell coordinates for coarse position and floating-point ratios for sub-pixel precision within each cell. This design enables pixel-perfect collision detection against the grid while maintaining smooth, fluid movement.
### Core Principles
1. **Grid is truth:** The world is a 2D grid of cells. Collision data lives in the grid.
2. **Entities straddle cells:** An entity's position is defined by which cell it occupies (`cx`, `cy`) plus how far into that cell it is (`xr`, `yr`).
3. **Velocity is in grid-ratio units:** Movement deltas (`dx`, `dy`) represent fractions of a cell per step, not raw pixels.
4. **Collisions are grid lookups:** Instead of testing sprite bounds against geometry, the engine checks the grid cells an entity is about to enter.
---
## Part 1: Basics
### The Grid
The level is a 2D array where each cell is either empty or solid. A constant defines the cell size in pixels:
```haxe
static inline var GRID = 16;
```
Collision data is stored as a simple 2D boolean or integer map:
```haxe
// Check if a grid cell is solid
function hasCollision(cx:Int, cy:Int):Bool {
// Look up cell value in the level data
return level.getCollision(cx, cy) != 0;
}
```
### Entity Positioning: Dual Coordinates
Every entity tracks its position using four values:
| Variable | Type | Description |
|----------|------|-------------|
| `cx` | Int | Cell X coordinate (which column the entity is in) |
| `cy` | Int | Cell Y coordinate (which row the entity is in) |
| `xr` | Float | X ratio within the cell, range 0.0 to 1.0 |
| `yr` | Float | Y ratio within the cell, range 0.0 to 1.0 |
An entity at `cx=5, cy=3, xr=0.5, yr=1.0` is horizontally centered in cell (5,3) and sitting on the bottom edge.
### Converting to Pixel Coordinates
To render the entity, convert grid coordinates to pixel positions:
```haxe
// Pixel position for rendering
var pixelX : Float = (cx + xr) * GRID;
var pixelY : Float = (cy + yr) * GRID;
```
This produces smooth, sub-pixel-precise positions for rendering even though the collision system operates on discrete grid cells.
### Velocity and Movement
Velocity is expressed in **cell-ratio units per fixed-step** (not pixels per frame):
```haxe
var dx : Float = 0; // Horizontal velocity (cells per step)
var dy : Float = 0; // Vertical velocity (cells per step)
```
Each fixed-step update, velocity is added to the ratio:
```haxe
// Apply horizontal movement
xr += dx;
// Apply vertical movement
yr += dy;
```
### Cell Overflow
When the ratio exceeds the 0..1 range, the entity has moved into an adjacent cell:
```haxe
// X overflow
while (xr > 1) { xr--; cx++; }
while (xr < 0) { xr++; cx--; }
// Y overflow
while (yr > 1) { yr--; cy++; }
while (yr < 0) { yr++; cy--; }
```
### Friction
Friction is applied as a multiplier each step, decaying velocity toward zero:
```haxe
var frictX : Float = 0.82; // Horizontal friction (0 = instant stop, 1 = no friction)
var frictY : Float = 0.82; // Vertical friction
// Applied each step after movement
dx *= frictX;
dy *= frictY;
// Clamp very small values to zero
if (Math.abs(dx) < 0.0005) dx = 0;
if (Math.abs(dy) < 0.0005) dy = 0;
```
Typical friction values:
- `0.82` -- Standard ground friction (responsive, quick stop)
- `0.94` -- Ice or slippery surface (slow deceleration)
- `0.96` -- Air friction (very slow horizontal deceleration)
### Gravity
Gravity is a constant added to `dy` each step:
```haxe
static inline var GRAVITY = 0.05; // In cell-ratio units per step^2
// In fixedUpdate:
dy += GRAVITY;
```
Since `dy` accumulates and friction is applied, the entity reaches a natural terminal velocity.
### Rendering / Sprite Sync
After the physics step, the sprite is placed at the computed pixel position:
```haxe
// In postUpdate, after physics is done:
sprite.x = (cx + xr) * GRID;
sprite.y = (cy + yr) * GRID;
```
For a platformer character, the anchor point is typically at the bottom-center of the sprite. With `yr = 1.0` representing the bottom of the current cell, the sprite's feet align with the floor.
### Basic Entity Template
```haxe
class Entity {
// Grid coordinates
var cx : Int = 0;
var cy : Int = 0;
var xr : Float = 0.5;
var yr : Float = 1.0;
// Velocity
var dx : Float = 0;
var dy : Float = 0;
// Friction
var frictX : Float = 0.82;
var frictY : Float = 0.82;
// Gravity
static inline var GRAVITY = 0.05;
// Grid size
static inline var GRID = 16;
// Pixel position (computed)
public var attachX(get, never) : Float;
inline function get_attachX() return (cx + xr) * GRID;
public var attachY(get, never) : Float;
inline function get_attachY() return (cy + yr) * GRID;
public function fixedUpdate() {
// Gravity
dy += GRAVITY;
// Apply velocity
xr += dx;
yr += dy;
// Apply friction
dx *= frictX;
dy *= frictY;
// Clamp small values
if (Math.abs(dx) < 0.0005) dx = 0;
if (Math.abs(dy) < 0.0005) dy = 0;
// Cell overflow
while (xr > 1) { xr--; cx++; }
while (xr < 0) { xr++; cx--; }
while (yr > 1) { yr--; cy++; }
while (yr < 0) { yr++; cy--; }
}
public function postUpdate() {
sprite.x = attachX;
sprite.y = attachY;
}
}
```
---
## Part 2: Collisions
### Collision Philosophy
Instead of using bounding-box-to-bounding-box collision detection (which becomes complex with slopes, one-way platforms, and edge cases), this engine checks grid cells directly. Since the entity's position is already expressed in grid terms, collision detection becomes a series of simple integer lookups.
### The Core Idea
Before allowing the entity to move into a neighboring cell, check if that cell is solid. If it is, clamp the entity's ratio and zero out its velocity on that axis.
### Axis Separation
Collisions are handled **per axis** -- first X, then Y (or vice versa). This simplifies the logic and avoids corner-case tunneling issues.
### X-Axis Collision
After applying `dx` to `xr`, before doing the cell-overflow step, check for collisions:
```haxe
// Apply X movement
xr += dx;
// Check collision to the RIGHT
if (dx > 0 && hasCollision(cx + 1, cy) && xr >= 0.7) {
xr = 0.7; // Clamp: stop before entering the solid cell
dx = 0; // Kill horizontal velocity
}
// Check collision to the LEFT
if (dx < 0 && hasCollision(cx - 1, cy) && xr <= 0.3) {
xr = 0.3; // Clamp: stop before entering the solid cell
dx = 0; // Kill horizontal velocity
}
// Cell overflow (after collision check)
while (xr > 1) { xr--; cx++; }
while (xr < 0) { xr++; cx--; }
```
**Why 0.7 and 0.3?** These thresholds represent the entity's collision radius within a cell. An entity centered at `xr = 0.5` with a half-width of 0.3 cells would collide at `xr = 0.7` on the right side and `xr = 0.3` on the left side. Adjust these values based on entity width.
### Y-Axis Collision
Similarly, after applying `dy` to `yr`:
```haxe
// Apply Y movement
yr += dy;
// Check collision BELOW (floor)
if (dy > 0 && hasCollision(cx, cy + 1) && yr >= 1.0) {
yr = 1.0; // Clamp: land on top of the solid cell
dy = 0; // Kill vertical velocity
}
// Check collision ABOVE (ceiling)
if (dy < 0 && hasCollision(cx, cy - 1) && yr <= 0.3) {
yr = 0.3; // Clamp: stop before entering ceiling cell
dy = 0; // Kill vertical velocity
}
// Cell overflow
while (yr > 1) { yr--; cy++; }
while (yr < 0) { yr++; cy--; }
```
For floor collisions, `yr = 1.0` means the entity sits exactly on the bottom edge of its current cell, which is the top edge of the cell below it. This is the natural "standing on ground" position.
### On-Ground Detection
To determine if the entity is standing on solid ground (for jump logic, animations, etc.):
```haxe
function isOnGround() : Bool {
return hasCollision(cx, cy + 1) && yr >= 0.98;
}
```
The threshold `0.98` instead of `1.0` allows for minor floating-point imprecision.
### Complete Entity with Collisions
```haxe
class Entity {
var cx : Int = 0;
var cy : Int = 0;
var xr : Float = 0.5;
var yr : Float = 1.0;
var dx : Float = 0;
var dy : Float = 0;
var frictX : Float = 0.82;
var frictY : Float = 0.82;
static inline var GRID = 16;
static inline var GRAVITY = 0.05;
// Collision radius (half-width in cell-ratio units)
var collRadius : Float = 0.3;
function hasCollision(testCx:Int, testCy:Int):Bool {
return level.isCollision(testCx, testCy);
}
function isOnGround():Bool {
return hasCollision(cx, cy + 1) && yr >= 0.98;
}
public function fixedUpdate() {
// --- Gravity ---
dy += GRAVITY;
// --- X Axis ---
xr += dx;
// Right collision
if (dx > 0 && hasCollision(cx + 1, cy) && xr >= 1.0 - collRadius) {
xr = 1.0 - collRadius;
dx = 0;
}
// Left collision
if (dx < 0 && hasCollision(cx - 1, cy) && xr <= collRadius) {
xr = collRadius;
dx = 0;
}
// X cell overflow
while (xr > 1) { xr--; cx++; }
while (xr < 0) { xr++; cx--; }
// --- Y Axis ---
yr += dy;
// Floor collision
if (dy > 0 && hasCollision(cx, cy + 1) && yr >= 1.0) {
yr = 1.0;
dy = 0;
}
// Ceiling collision
if (dy < 0 && hasCollision(cx, cy - 1) && yr <= collRadius) {
yr = collRadius;
dy = 0;
}
// Y cell overflow
while (yr > 1) { yr--; cy++; }
while (yr < 0) { yr++; cy--; }
// --- Friction ---
dx *= frictX;
dy *= frictY;
if (Math.abs(dx) < 0.0005) dx = 0;
if (Math.abs(dy) < 0.0005) dy = 0;
}
public function postUpdate() {
sprite.x = (cx + xr) * GRID;
sprite.y = (cy + yr) * GRID;
}
}
```
---
## Collision Edge Cases and Solutions
### Diagonal Movement / Corner Clipping
Because collisions are checked per-axis in sequence, an entity moving diagonally into a corner naturally resolves against one axis first. This prevents the entity from getting stuck in corners and eliminates the need for complex diagonal collision logic.
### High-Speed Tunneling
If `dx` or `dy` is large enough to skip an entire cell in one step, the entity could "tunnel" through walls. Solutions:
1. **Cap velocity:** Clamp `dx` and `dy` to a maximum of 0.5 (half a cell per step)
2. **Subdivide steps:** If velocity exceeds the threshold, run the collision check in smaller increments
3. **Ray-march the grid:** Check every cell along the movement path
```haxe
// Simple velocity cap
if (dx > 0.5) dx = 0.5;
if (dx < -0.5) dx = -0.5;
if (dy > 0.5) dy = 0.5;
if (dy < -0.5) dy = -0.5;
```
### One-Way Platforms
Platforms the entity can jump up through but land on from above:
```haxe
// In Y collision, check for one-way platform
if (dy > 0 && isOneWayPlatform(cx, cy + 1) && yr >= 1.0 && prevYr < 1.0) {
yr = 1.0;
dy = 0;
}
```
Key: Only collide when the entity is moving downward (`dy > 0`) and was previously above the platform (`prevYr < 1.0`).
### Slopes
For basic slope support, instead of a binary collision check, query the slope height at the entity's x-position within the cell:
```haxe
// Pseudocode for slope collision
var slopeHeight = getSlopeHeight(cx, cy + 1, xr);
if (yr >= slopeHeight) {
yr = slopeHeight;
dy = 0;
}
```
---
## Jumping
Jumping is simply a negative `dy` impulse:
```haxe
function jump() {
if (isOnGround()) {
dy = -0.5; // Jump impulse (in cell-ratio units)
}
}
```
Gravity naturally decelerates the upward motion, creating a parabolic arc. To allow variable-height jumps (holding the button longer = higher jump):
```haxe
// On jump button release, reduce upward velocity
function onJumpRelease() {
if (dy < 0) {
dy *= 0.5; // Cut remaining upward velocity
}
}
```
---
## Coordinate System Diagram
```
Cell (cx, cy) Next Cell (cx+1, cy)
+-------------------+ +-------------------+
| | | |
| xr=0.0 xr=1.0 --> | xr=0.0 |
| | | |
| * | | |
| (xr=0.5, | | |
| yr=0.5) | | |
| | | |
+-------------------+ +-------------------+
yr=0.0 yr=1.0 = top of cell below
Pixel position = (cx + xr) * GRID, (cy + yr) * GRID
```
---
## Update Order Summary
```
fixedUpdate():
1. Apply gravity dy += GRAVITY
2. Apply X velocity xr += dx
3. Check X collisions Clamp xr, zero dx if colliding
4. Handle X cell overflow cx/xr normalization
5. Apply Y velocity yr += dy
6. Check Y collisions Clamp yr, zero dy if colliding
7. Handle Y cell overflow cy/yr normalization
8. Apply friction dx *= frictX, dy *= frictY
9. Zero out tiny values Threshold check
postUpdate():
1. Sync sprite position sprite.x/y = pixel coords
2. Update animation Based on state/velocity
3. Camera follow Track entity
```
---
## Design Advantages
| Feature | Benefit |
|---------|---------|
| Grid-based collision | O(1) lookup per check, no broad-phase needed |
| Dual coordinates | Sub-pixel smooth rendering with integer collision |
| Per-axis collision | Simple logic, naturally handles corners |
| Ratio-based velocity | Resolution-independent movement |
| Friction multiplier | Tunable feel per surface type |
| Cell overflow while-loops | Handles multi-cell movement safely |