Skip to content

Commit d164989

Browse files
committed
fixed animated sprite, adding warning to charactermovement, update some input handling, physics engine delta time cap
1 parent 5069bdd commit d164989

File tree

16 files changed

+419
-221
lines changed

16 files changed

+419
-221
lines changed

Parts/CharacterMovement.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class CharacterMovement extends Part {
1818

1919
act(_delta: number): void {
2020
if (!this.input) {
21+
if (!this.warned.has("MissingInput")) this.top?.warn(`CharacterMovement <${this.name}> (${this.id}) is missing an input property. Please create an input on the scene and pass it.`) ? this.warned.add("MissingInput") : null;
2122
return;
2223
}
2324

Parts/Children/AnimatedSprite.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@ export class AnimatedSprite extends Renderer {
1717
webEngine: boolean = false; // Flag to indicate if this is running in a web engine context
1818
onAnimationComplete?: (animationName: string, sprite: AnimatedSprite) => void; // Callback for when an animation completes
1919
spritesheetImage?: string; // Optional image for the spritesheet
20-
constructor({ spritesheet, spritesheetImage, width, height, startingAnimation, disableAntiAliasing = false, onAnimationComplete, webEngine = false }: { spritesheet: string, spritesheetImage?: string, width: number, height: number, startingAnimation?: string, disableAntiAliasing?: boolean, onAnimationComplete?: (animationName: string, sprite: AnimatedSprite) => void, webEngine?: boolean }) {
20+
startLoop: boolean; // Override spritesheet data and loop immediately
21+
startBouncing: boolean; // override spritesheet data and loop immediately
22+
lastFrameTime: number = performance.now(); // Timestamp of the last frame update
23+
constructor({ spritesheet, spritesheetImage, width, height, startingAnimation, disableAntiAliasing = false, onAnimationComplete, webEngine = false, bounce = false, loop = true }: { spritesheet: string, spritesheetImage?: string, width: number, height: number, startingAnimation?: string, disableAntiAliasing?: boolean, onAnimationComplete?: (animationName: string, sprite: AnimatedSprite) => void, webEngine?: boolean, loop: boolean, bounce: boolean }) {
2124
super({ width, height }); // Call the parent constructor with empty imageSource
2225
this.name = "AnimatedSprite";
2326
this.debugEmoji = "🎞️"; // Default emoji for debugging the animated sprite
2427
this.spritesheet = spritesheet;
2528
this.width = width;
2629
this.height = height;
2730
this.ready = false;
28-
31+
this.startLoop = loop;
32+
this.startBouncing = bounce;
33+
this.spritesheetImage = spritesheetImage; // Optional image for the spritesheet
2934
this.currentAnimation = startingAnimation || "default";
3035
this.disableAntiAliasing = disableAntiAliasing;
3136
this.onAnimationComplete = onAnimationComplete; // Set the callback for animation completion
@@ -73,7 +78,6 @@ export class AnimatedSprite extends Renderer {
7378
if (!spritesheetData.meta.animations || typeof spritesheetData.meta.animations !== "object") {
7479
throw new Error("Invalid spritesheet format: 'meta.animations' is missing or not an object.");
7580
}
76-
7781
const image = new Image();
7882
// If spritesheetImage is provided, use it directly. Otherwise, try to resolve from spritesheet data.
7983
if (this.spritesheetImage) {
@@ -86,8 +90,10 @@ export class AnimatedSprite extends Renderer {
8690
}
8791
}
8892

93+
8994
image.onerror = (err) => {
9095
this.top?.error(`Failed to load spritesheet image <${spritesheetData.meta.image}>:`, err);
96+
console.error('Failed to load spritesheet image', err);
9197
this.ready = false;
9298
};
9399
this.spritesheetData = spritesheetData; // Store the parsed spritesheet data
@@ -129,13 +135,14 @@ export class AnimatedSprite extends Renderer {
129135
}
130136

131137
}
132-
133138
if (this.currentAnimation === "default" && this.spritesheetData.meta.startingAnimation) {
134139
this.currentAnimation = this.spritesheetData.meta.startingAnimation;
140+
this.setAnimation(this.currentAnimation, { loop: this.startLoop, bounce: this.startBouncing });
135141
} else if (this.currentAnimation === "default" && Object.keys(this.spritesheetData.meta.animations).length > 0) {
136142
this.currentAnimation = Object.keys(this.spritesheetData.meta.animations)[0];
137-
}
143+
this.setAnimation(this.currentAnimation, { loop: this.startLoop, bounce: this.startBouncing });
138144

145+
}
139146
this.ready = true;
140147
}
141148
frame(index: number): HTMLImageElement | null {
@@ -195,14 +202,17 @@ export class AnimatedSprite extends Renderer {
195202
this.top?.warn(`Animation '${animationName}' does not exist in spritesheet for animated sprite <${this.name}> attached to ${this.parent?.name}.`);
196203
}
197204
}
198-
act(delta: number) {
199-
super.act(delta);
205+
act(deltaTime: number) {
206+
super.act(deltaTime);
200207
if (!this.ready) {
201208
return;
202209
}
203-
const duration = (this.spritesheetData?.frames[this.currentFrameIndex].duration || 100)
210+
const duration = (this.spritesheetData?.frames[this.currentFrameIndex].duration || 100);
211+
const now = performance.now();
212+
const between = now - this.lastFrameTime;
204213
if (this.ready && this.spritesheetData) {
205-
if (delta > duration) {
214+
if (between > duration) {
215+
this.lastFrameTime = now;
206216
if (this.spritesheetData.meta.animations[this.currentAnimation].bounce) {
207217
let direction = this.bouncing ? -1 : 1; // Determine direction based on bouncing flag
208218
const animFrames = this.spritesheetData.meta.animations[this.currentAnimation].frames.length;
@@ -240,8 +250,7 @@ export class AnimatedSprite extends Renderer {
240250
const transform = this.sibling<Transform>("Transform");
241251
if (!transform) {
242252
if (!this.warned.has("TransformMissing")) {
243-
const seen = this.top?.warn(`AnimatedSprite <${this.name}> attached to ${this.parent?.name} does not have a Transform component. Skipping rendering. This will only show once.`);
244-
if (seen) this.warned.add("TransformMissing");
253+
this.top?.warn(`AnimatedSprite <${this.name}> attached to ${this.parent?.name} does not have a Transform component. Skipping rendering. This will only show once.`) ? this.warned.add("TransformMissing") : null;
245254
}
246255
return;
247256
}
@@ -271,7 +280,7 @@ export class AnimatedSprite extends Renderer {
271280
// Create a neat little vertical progress bar for the current animation using an embedded HTML div
272281
const barHeight = 15; // px
273282
const barWidth = 6; // px
274-
const progress = delta / duration;
283+
const progress = this.lastFrameTime / duration;
275284
this.hoverbug = // use a different loop emoji for loop and bounce
276285
`${this.ready ? "✅" : "❌"} ${this.spritesheetData?.meta.animations[this.currentAnimation].loop ? "🔁" : ""}` +
277286
`<div style="display:inline-block; width:${barWidth}px; height:${barHeight}px; background:linear-gradient(to top, dodgerblue ${progress * 100}%, #ccc ${progress * 100}%); border-radius:3px; border:1px solid #888; vertical-align:middle;border-radius:0px"></div> ` +

Parts/Children/Button.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { Renderer } from "./Renderer";
77
import { Sound } from "../Sound";
88
import { isNamedTupleMember } from "typescript";
99
import type { Camera } from "../Camera";
10+
import type { BoxCollider } from "./BoxCollider";
11+
import { PolygonCollider } from "./PolygonCollider";
1012

1113
export class Button extends Renderer {
1214
styles?: ButtonStyles;
@@ -17,11 +19,32 @@ export class Button extends Renderer {
1719
hoverSound?: Sound;
1820
activeSound?: Sound;
1921

20-
constructor({ label, onClick, styles, clickSound, hoverSound, activeSound }: { label: string; onClick: () => void; styles?: ButtonStyles, clickSound?: Sound, hoverSound?: Sound, activeSound?: Sound }) {
21-
super({ width: styles?.default?.width ?? 100, height: styles?.default?.height ?? 50, disableAntiAliasing: true });
22+
constructor({ label, onClick, styles, clickSound, hoverSound, activeSound, width, height, backgroundColor, color, font, borderRadius, borderWidth, borderColor, hoverBackground, hoverColor, activeBackground, activeColor }: { label: string; onClick: () => void; styles?: ButtonStyles, clickSound?: Sound, hoverSound?: Sound, activeSound?: Sound, width?: number, height?: number, backgroundColor?: string, color?: string, font?: string, borderRadius?: number, borderWidth?: number, borderColor?: string, hoverBackground?: string, hoverColor?: string, activeBackground?: string, activeColor?: string }) {
23+
super({ width: styles?.default?.width || width || 100, height: styles?.default?.height || height || 50, disableAntiAliasing: true });
2224
this.name = label;
2325
this.onClickHandler = onClick;
24-
this.styles = styles;
26+
this.styles = styles || {};
27+
this.styles.default = {
28+
width,
29+
height,
30+
backgroundColor,
31+
color,
32+
font,
33+
borderWidth,
34+
borderRadius,
35+
borderColor,
36+
...this.styles.default
37+
}
38+
this.styles.hover = {
39+
backgroundColor: hoverBackground,
40+
color: hoverColor,
41+
...this.styles.hover
42+
};
43+
this.styles.active = {
44+
backgroundColor: activeBackground,
45+
color: activeColor,
46+
...this.styles.active
47+
};
2548
this.clickSound = clickSound;
2649
this.hoverSound = hoverSound;
2750
this.activeSound = activeSound;
@@ -73,10 +96,8 @@ export class Button extends Renderer {
7396
`Button <${this.name}> (${this.id}) does not have Transform sibling. Please ensure you add a Transform component before adding a Button.`
7497
);
7598
}
76-
// Set superficial dimensions based on default styles
77-
const defaultStyle = this.styles?.default;
78-
this.superficialWidth = defaultStyle?.width ?? 100;
79-
this.superficialHeight = defaultStyle?.height ?? 50;
99+
this.superficialWidth = this.width ?? 100;
100+
this.superficialHeight = this.height ?? 50;
80101
}
81102
setOnClick(onClick: () => void) {
82103
this.onClickHandler = onClick;
@@ -91,6 +112,10 @@ export class Button extends Renderer {
91112
if (!transform) {
92113
throw new Error(`Button <${this.name}> does not have a Transform sibling. Ensure it is mounted to a GameObject with a Transform component.`);
93114
}
115+
const boxCollider = this.sibling<BoxCollider>("BoxCollider") || this.sibling<PolygonCollider>("PolygonCollider");
116+
if (!boxCollider && !this.warned.has("MissingBoxCollider")){
117+
this.top?.warn(`Button <${this.name}> (${this.id}) does not have a Collider sibling. It may not function correctly without a collider for input detection.`) ? this.warned.add("MissingBoxCollider") : undefined;
118+
}
94119
const scene = this.registrations["scene"] as Scene | undefined;
95120
if (scene) {
96121
if (!scene.child<Input>("Input")) {

Parts/Children/PolygonCollider.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export class PolygonCollider extends Collider {
6464
if (ctx) {
6565
ctx.save();
6666
ctx.strokeStyle = this.colliding ? "rgba(255, 0, 100, 0.8)" : "rgba(0, 255, 100, 0.8)";
67-
ctx.fillStyle = this.colliding ? "rgba(255, 0, 100, 0.5)" : "rgba(0, 255, 0, 0.5)";
6867
ctx.lineWidth = 1;
6968

7069
ctx.beginPath();
@@ -73,7 +72,6 @@ export class PolygonCollider extends Collider {
7372
ctx.lineTo(this.worldVertices[i].x, this.worldVertices[i].y);
7473
}
7574
ctx.closePath();
76-
ctx.fill();
7775
ctx.stroke();
7876
ctx.restore();
7977
}

Parts/Game.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export class Game extends Part {
126126
} else if (starterScene instanceof Scene) {
127127
this.currentScene = starterScene;
128128
} else {
129-
this.warn("No valid scene provided to start the game. Using the first scene found.");
129+
this.warn("No valid scene provided to start the game. Using the first scene found. Check console for more details");
130130
this.currentScene = this.childrenArray[0];
131131
if (!this.currentScene) {
132132
throw new Error("No scenes available to start the game.");
@@ -145,7 +145,7 @@ export class Game extends Part {
145145
}
146146
if (!this._isPaused && this.currentScene) {
147147
const now = performance.now();
148-
const delta = Math.min((now - this._lastUpdateTime), 1000 / 60);
148+
const delta = (now - this._lastUpdateTime)
149149
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
150150
if (this.devmode) {
151151
this.currentScene.calculateLayout();
@@ -225,6 +225,16 @@ export class Game extends Part {
225225
}
226226
} else if (scene instanceof Scene) {
227227
this.currentScene = scene;
228+
} else {
229+
console.error('Set unknown scene type- neither string nor Scene instance');
230+
console.log(scene);
231+
let json;
232+
try {
233+
json = JSON.stringify(scene);
234+
} catch (error: any) {
235+
json = `<Error Parsing JSON: ${error?.message || 'Error'}>`;
236+
}
237+
this.debug(`Trying to set scene to unknown type- neither string nor Scene instance. Got ${typeof scene} - ${json}`);
228238
}
229239
}
230240
warn (...args: any[]) {

Parts/Input.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ export class Input extends Part {
187187
}
188188
return false;
189189
});
190-
191190
if (game.hovering && game.hovering !== hovered) {
192191
game.hovering.onunhover();
193192
game.hovering = undefined;

Parts/PhysicsEngine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ export class PhysicsEngine extends Part {
3434

3535
act(delta: number) {
3636
super.act(delta);
37-
Engine.update(this.engine, delta);
37+
Engine.update(this.engine, Math.min(delta, 1000 / 30));
3838
}
3939
}

README.md

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,136 @@
1-
Basic pipline
1+
# Getting Started
22

3+
This guide will walk you through the basics of creating a game with the Forge engine.
34

4-
Game is a part -> act -> defers to currentScene.act()
5-
Scene is a part -> act -> loops through layers -> layer.act();
6-
Layer is a part -> act -> loops through GameObjects -> gameObject.act();
7-
GameObjects are parts -> act -> goes through child parts and calls act on those (maybe renderers, maybe physics engines, movement, etc)
5+
## Project Structure
6+
7+
The Forge engine is organized into the following files and directories:
8+
9+
- `helpers.ts`: A collection of helper functions.
10+
- `types.ts`: Contains the TypeScript interfaces used throughout the engine.
11+
- `README.md`: The main README file for the project.
12+
- `docs/`: Contains the documentation for the engine.
13+
- `engine/`: Contains the engine page for creating games.
14+
- `Math/`: Contains the `Vector.ts` file, which is a simple vector library.
15+
- `Parts/`: Contains the core components of the engine.
16+
- `public/`: Contains the public assets for the game, such as images and scripts.
17+
- `src/`: Contains the source code for the game.
18+
19+
## Overview
20+
21+
The Forge engine is build on the core concept of trees. We consider each node on the tree a "Part" in the game. The tree always begins with a Game part, and then it can be built further from there.
22+
A typical game structure might look like the following:
23+
24+
```
25+
Game
26+
-> Start Scene
27+
----> Text
28+
--------> Transform
29+
----> Start Button
30+
--------> Transform
31+
32+
-> Game Scene
33+
----> Player
34+
--------> Transform
35+
--------> SpriteRender
36+
--------> BoxCollider
37+
----> Enemy
38+
--------> Transform
39+
--------> SpriteRender
40+
--------> BoxCollider
41+
----> Coin
42+
--------> Transform
43+
--------> SpriteRender
44+
--------> BoxCollider
45+
... etc
46+
```
47+
48+
In order to build a game, you would mix and match Parts together to build your game. There are a few prebuilt parts that exist to simplify the development process, but you will need to build the core functionality yourself.
49+
50+
## Your First Game: "Hello, World!"
51+
52+
Let's create the simplest possible game to see the engine in action. This game will display the text "Hello, World!" on the screen.
53+
54+
### 1. Set up your HTML file
55+
56+
First, you need an HTML file to host your game. Create an `index.html` in your project's `public` directory.
57+
58+
```html
59+
<!DOCTYPE html>
60+
<html lang="en">
61+
<head>
62+
<meta charset="UTF-8">
63+
<title>My Forge Game</title>
64+
<style>
65+
body { margin: 0; overflow: hidden; background-color: #333; }
66+
canvas { display: block; }
67+
</style>
68+
</head>
69+
<body>
70+
<canvas id="game-canvas"></canvas>
71+
<script src="script.js"></script>
72+
</body>
73+
</html>
74+
```
75+
76+
### 2. Create the main script
77+
78+
Next, create a `script.js` file (or `main.ts` if you are using TypeScript) in your `src` directory. This is where your game's code will live.
79+
80+
```javascript
81+
import { Game } from '../Parts/Game';
82+
import { Scene } from '../Parts/Scene';
83+
import { GameObject } from '../Parts/GameObject';
84+
import { Transform } from '../Parts/Children/Transform';
85+
import { TextRender } from '../Parts/Children/TextRender';
86+
import { Vector } from '../Math/Vector';
87+
88+
// 1. Create the Game instance
89+
const myGame = new Game({
90+
name: 'HelloWorldGame',
91+
canvas: 'game-canvas', // The ID of the canvas in your HTML
92+
width: 800,
93+
height: 600,
94+
devmode: true // Enable debug tools
95+
});
96+
97+
// 2. Create a Scene
98+
const mainScene = new Scene({ name: 'MainScene' });
99+
100+
// 3. Create a GameObject for our text
101+
const helloText = new GameObject({ name: 'HelloWorldText' });
102+
103+
// 4. Add components to the GameObject
104+
helloText.addChildren(
105+
// All GameObjects need a Transform for position, rotation, and scale
106+
new Transform({
107+
position: new Vector(400, 300) // Center of the 800x600 canvas
108+
}),
109+
// The TextRender component will draw our text
110+
new TextRender({
111+
name: 'MyText',
112+
textContent: 'Hello, World!',
113+
font: '48px Arial',
114+
color: 'white',
115+
align: 'center'
116+
})
117+
);
118+
119+
// 5. Add the GameObject to the Scene
120+
mainScene.addChild(helloText);
121+
122+
// 6. Add the Scene to the Game
123+
myGame.addChild(mainScene);
124+
125+
// 7. Start the game!
126+
myGame.start(mainScene);
127+
128+
```
129+
130+
### 3. Run your game
131+
132+
You will need a local development server to run your game. If you have one set up, navigate to your `public/index.html`. You should see "Hello, World!" displayed in the center of the canvas.
133+
134+
Because `devmode` is on, you can also see the node tree on the right side of the screen, which is a powerful tool for debugging and understanding your game's structure.
135+
136+
Congratulations, you've made your first game with the Forge engine! From here, you can explore the other `Part`s to add images, player input, physics, and more.

0 commit comments

Comments
 (0)