mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-13 12:45:13 +00:00
add new skill game-engine
This commit is contained in:
528
skills/game-engine/assets/2d-maze-game.md
Normal file
528
skills/game-engine/assets/2d-maze-game.md
Normal 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 |
|
||||
1855
skills/game-engine/assets/2d-platform-game.md
Normal file
1855
skills/game-engine/assets/2d-platform-game.md
Normal file
File diff suppressed because it is too large
Load Diff
310
skills/game-engine/assets/gameBase-template-reop.md
Normal file
310
skills/game-engine/assets/gameBase-template-reop.md
Normal 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
|
||||
1528
skills/game-engine/assets/paddle-game-template.md
Normal file
1528
skills/game-engine/assets/paddle-game-template.md
Normal file
File diff suppressed because it is too large
Load Diff
507
skills/game-engine/assets/simple-2d-engine.md
Normal file
507
skills/game-engine/assets/simple-2d-engine.md
Normal 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 |
|
||||
Reference in New Issue
Block a user