@pietal.dev/engine
Version:
PIXI v8, v7, v6 compatible: inspired by Unity, reactive Game Framework: GameObject, StateMachine, CircleBody, PolygonBody, Physics, Sprite, Container, Animator, TextureAtlas, Resources loading
265 lines (214 loc) • 8.6 kB
Markdown
"https://raw.githubusercontent.com/Prozi/oneforall/main/all-might.png" alt="All Might from Boku No Hero Academia holds One For All in his palm" width="456" height="456" style="image-rendering: pixelated; max-width: 100%;" />
[<img src="https://img.shields.io/npm/v/@pietal.dev/engine?style=for-the-badge&color=success" alt="npm version" />](https://www.npmjs.com/package/@pietal.dev/engine?activeTab=versions)
[<img src="https://img.shields.io/circleci/build/github/Prozi/oneforall/main?style=for-the-badge" alt="build status" />](https://app.circleci.com/pipelines/github/Prozi/oneforall)
[<img src="https://img.shields.io/npm/l/@pietal.dev/engine.svg?style=for-the-badge&color=success" alt="license: MIT" />](https://github.com/Prozi/oneforall/blob/master/LICENSE)
```
[ ]
├──[1x WebGL Canvas (pixi.js)]
├──[1x Collision Detection]
└──[50x GameObject (Player)]
├──[1x CircleBody]
└──[1x Animator]
└──[1x StateMachine]
```
Tiny code, big results! Check out the [demo](https://prozi.github.io/oneforall/demo/?fps&debug) to see below code in action.
Also, here is the [documentation](https://prozi.github.io/oneforall/modules.html).
## Demo Code
`src/demo/index.ts`
```typescript
async function start(): Promise<void> {
const queryParams = Scene.getQueryParams();
// create main Scene
const scene: Scene = new Scene({
visible: true,
autoSort: true,
showFPS: 'fps' in queryParams,
debug: 'debug' in queryParams
});
// initialize scene async - new since pixi 7/8
await scene.init({
resizeTo: window,
autoDensity: true,
autoStart: false,
sharedTicker: false
});
// wait to load cave-boy.json and cave-boy.png, uses PIXI.Loader inside
const data = await Resources.loadResource('./cave-boy.json');
const texture = await Resources.loadResource(data.tileset);
// create 50 sprites from template
Array.from({ length: Number(queryParams.limit || 50) }, () => {
create({ scene, data, texture });
});
scene.start();
scene.update$.pipe(takeUntil(scene.destroy$)).subscribe(() => {
scene.physics.separate();
});
}
start();
```
`src/demo/sprite.prefab.ts`
```typescript
export function create({ scene, data, texture }): TGameObject {
// create game object
const gameObject = new GameObject('Player') as TGameObject;
// create body
gameObject.body = new CircleBody(gameObject, 20, 14, 20, { padding: 7 });
gameObject.body.setPosition(
Math.random() * innerWidth,
Math.random() * innerHeight
);
// create animator with few animations from json + texture
gameObject.sprite = new Animator(gameObject, data, texture);
gameObject.sprite.setState('idle');
// insert body to physics and game object to scene
scene.addChild(gameObject);
// subscribe to *own* update function until *own* destroy
gameObject.update$
.pipe(takeUntil(gameObject.destroy$))
.subscribe((deltaTime) => {
update(gameObject, deltaTime);
});
return gameObject;
}
export function update(gameObject: TGameObject, deltaTime: number): void {
const scene = gameObject.scene;
const scale = scene.stage.scale;
const gameObjects = scene.children as TGameObject[];
// at 60fps deltaTime = 1.0, at 30fps deltaTime = 2.0
const safeDelta = Math.min(deltaTime, 2);
const chance = safeDelta * 0.01;
if (Math.random() < chance) {
// goto random place
gameObject.target = {
x: (Math.random() * innerWidth) / scale.x,
y: (Math.random() * innerHeight) / scale.y
};
} else if (Math.random() < chance) {
// goto random target
gameObject.target =
gameObjects[Math.floor(Math.random() * gameObjects.length)];
} else if (Math.random() < chance) {
// stop
gameObject.target = null;
}
if (gameObject.target && distance(gameObject.target, gameObject) < 9) {
gameObject.target = null;
}
if (!gameObject.target) {
gameObject.sprite.setState('idle');
} else {
gameObject.sprite.setState('run');
const angle: number = Math.atan2(
gameObject.target.y - gameObject.y,
gameObject.target.x - gameObject.x
);
if (!isNaN(angle)) {
const offsetX: number = Math.cos(angle);
const offsetY: number = Math.sin(angle);
if (gameObject.sprite instanceof Animator) {
const flipX: number =
Math.sign(offsetX || gameObject.sprite.scale.x) *
Math.abs(gameObject.sprite.scale.x);
// flip x so there is no need to duplicate sprites
gameObject.sprite.setScale(flipX, gameObject.sprite.scale.y);
}
// update body which updates gameObject game object
gameObject.body.setPosition(
gameObject.body.x + safeDelta * offsetX,
gameObject.body.y + safeDelta * offsetY
);
}
}
}
```
- [Unity-inspired architecture](https://docs.unity3d.com/Manual/CreatingGameplay.html)
- [State management](https://gamedevelopment.tutsplus.com/tutorials/finite-state-machines-theory-and-implementation--gamedev-11867)
- [Reactive events](https://www.learnrxjs.io/learn-rxjs/subjects)
- [Lifecycle cleanup management](https://www.html5gamedevs.com/topic/44780-best-way-to-remove-objects-from-the-stage/)
- [Collision detection](https://npmjs.com/package/detect-collisions)
- [Drawing on WebGL canvas](https://npmjs.com/package/pixi.js)
- Compatible with pixi version 6, 7 and 8 - at the same time!
- **Resources:** Handles loading game assets like images and JSON files.
- **Scene:** Sets the stage for gameplay, where all the action takes place.
- **GameObject:** Represents characters, objects, or items in the game world.
- **Prefab:** Instantiates ready-made templates for creating game elements.
- **Sprite:** Displays static 2D graphics in the game.
- **Container:** Organizes and manages groups of game objects for easier handling.
- **Animator:** Useful JSON to Container with AnimatedSprite children.
- **StateMachine:** Controls how game objects transition between actions.
- **CircleBody:** Adds physics properties and interactions for round-shaped objects.
- **PolygonBody:** Adds physics properties and interactions for polygonal objects.
- **TextureAtlas:** Allows easy slicing of texture into smaller cached slices.
```bash
npm i @pietal.dev/engine -D
```
```
PASS src/state-machine.spec.ts
GIVEN StateMachine
✓ THEN you can set validators (4 ms)
✓ THEN you can't change state to invalid state
✓ THEN you can change state to valid state
PASS src/component.spec.ts
GIVEN Component
✓ THEN update publishes update$ (1 ms)
✓ THEN destroy publishes destroy$
PASS src/circle-body.spec.ts
GIVEN CircleBody
✓ THEN it has set property radius (5 ms)
✓ THEN it can't have zero radius (9 ms)
✓ THEN update propagates x/y changes
PASS src/scene-ssr.spec.ts
GIVEN SceneBase
✓ THEN it works (3 ms)
✓ THEN it can have children (1 ms)
✓ THEN scene propagates update to gameobject to component (1 ms)
PASS src/scene.spec.ts
GIVEN Scene
✓ THEN it works (2 ms)
✓ THEN it can have children
✓ THEN scene propagates update to gameobject to component (1 ms)
PASS src/sprite.spec.ts
GIVEN Sprite
✓ THEN update propagates x/y changes (3 ms)
✓ THEN removeChild works
✓ THEN destroy works (1 ms)
✓ THEN destroy works extended (1 ms)
PASS src/prefab.spec.ts
GIVEN Prefab
✓ THEN can be instantiated (3 ms)
✓ THEN can create 100 instances (10 ms)
PASS src/game-object.spec.ts
GIVEN GameObject
✓ THEN you can add component (5 ms)
✓ THEN update propagates to components (1 ms)
✓ THEN you can remove component
✓ THEN destroy removes component (1 ms)
✓ THEN you can get component by label
✓ THEN you can get components by label
✓ THEN you can destroy 1000 bodies without problem (106 ms)
PASS src/polygon-body.spec.ts
GIVEN PolygonBody
✓ THEN update propagates x/y changes
PASS src/container.spec.ts
GIVEN Container
✓ THEN update propagates x/y changes
✓ THEN destroy works (1 ms)
PASS src/resources.spec.ts
GIVEN Resources
✓ THEN it silently fails and proceeds (5 ms)
PASS src/index.spec.ts
GIVEN index.ts
✓ THEN basic imports work (1 ms)
PASS src/application.spec.ts
GIVEN Application
✓ THEN it works
Test Suites: 13 passed, 13 total
Tests: 33 passed, 33 total
```
<img src=