Skip to content

Unity-inspired, reactive Game Framework for pixi.js - GameObject, Prefab, StateMachine, Sprite, CircleBody, PolygonBody, Physics, Sprite, Container, Animator

License

Notifications You must be signed in to change notification settings

Prozi/oneforall

Repository files navigation

One For All

All Might from Boku No Hero Academia holds One For All in his palm

TypeScript gamedev library inspired by Unity

npm version build status license: MIT

Demo

[1x Scene]
  ├──[1x HTML Canvas]
  ├──[1x Collision Detection]
  └──[50x GameObject (Player)]
       ├──[1x CircleBody]
       └──[1x Animator]
            └──[1x StateMachine]

Tiny code, big results! Check out the demo to see below code in action.

Also, here is the documentation.

Demo Code

src/demo/index.ts

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

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
      );
    }
  }
}

Features

Classes this library exports

  • 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.

Installation

npm i @pietal.dev/engine -D

We also have tests

 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