AlexandreBillereau / open-source-game-rogue-like

Game open source - rogue like
1 stars 0 forks source link

Proposition about Entity implementation. #5

Closed AlexandreBillereau closed 5 months ago

AlexandreBillereau commented 5 months ago

GitHub Issue - Entity Implementation Discussion

look branch : [Feature/player_movment]

Abstract:

We're discussing the implementation of an entity system for a game using Phaser. The proposed approach involves creating an Entity class to serve as a base for game entities, combining features from Phaser.GameObjects.Sprite and Phaser.Physics.Arcade.Sprite for animations and interactions with the game world.


Code Overview:

We've introduced an Entity class as an abstraction to inherit essential functionalities. It throws errors for unimplemented methods, enforcing a form of interface, commonly known as duck typing.

A static load method is implemented to load sprites once, avoiding memory saturation from repeated loading.

An example implementation of a Player class inheriting from Entity is provided. It sets up player animations and entity physics, along with an update method to handle input.

Sprites are mapped using the Leshy SpriteSheet tool.


Scene Composition:

In the game scene, sprites associated with players are loaded, and players are instantiated. Each player can be controlled independently, and their animations are managed accordingly.


English Translation:

I started by considering how an entity works in Phaser and concluded it's a mix of Phaser.GameObjects.Sprite and Phaser.Physics.Arcade.Sprite to enable animation and interaction with the world and collisions.

So, I created an abstraction, Entity, to inherit the essentials.

Here's my code:

/**
 * @class Enity - Represente an entitiy in the game.
 * @extends Phaser.GameObjects.Sprite
 */
export class Entity extends Phaser.GameObjects.Sprite {
  /** @type {Phaser.Scene} give by the parent*/

  /** @type {Phaser.Physics.Arcade.Sprite} */
  entity;

  /**
   * @param {Phaser.Scene} scene
   * @param {number} x position x in the scene
   * @param {number} y position y in the scene
   */
  constructor(scene, x, y) {
    super(scene, x, y);
    if (this.constructor === Entity) {
      throw new Error(
        "Entity is an abstract class and cannot be instantiated directly."
      );
    }

    this.setUpAnimation();
    this.setUpEntity();
  }

  setUpAnimation() {
    throw new Error("Method createAnimation() not implemented.");
  }

  setUpEntity() {
    throw new Error("Method create() not implemented.");
  }

  /**
   * @param {Phaser.Scene} scene
   * this function supposed to load atlas assosiated with player
   */
  static load(scene) {
    throw new Error(`Method static load() not implemented. ${scene}`);
  }
}

Explanation of the code:

The throw functions are for creating a sort of interface that forces developers to implement the essential functions; this is what we call duck typing.

Later, I created a static load function because we want to load the sprites only once and not multiple times for each object creation to avoid memory saturation.

For implementing a class inherited from Entity, let's take the example of the Player class:

import { Entity } from "../entity/Entity";

/**
 * @class Player - Represente a player in the game.
 * @extends Entity
 */
export class Player extends Entity {
  /** @type {Phaser.Scene} give by the parent */
  constructor(scene, x, y) {
    super(scene, x, y);
    this.cute = 0;
  }

  /**
   * Set up the animation of the player
   * @override
   */
  setUpAnimation() {
    this.anims.create({
      key: "walk",
      frames: this.anims.generateFrameNames("gameSprites", {
        prefix: "walk",
        end: 3,
        zeroPad: 3,
      }),
      frameRate: 4,
      repeat: Number.POSITIVE_INFINITY,
    });

    this.scene.anims.create({
      key: "stand",
      frames: this.scene.anims.generateFrameNames("gameSprites", {
        prefix: "stand",
        end: 3,
        zeroPad: 3,
      }),
      frameRate: 4,
      repeat: Number.POSITIVE_INFINITY,
    });
  }

  /**
   * Set up the entity of the player
   * @override
   */
  setUpEntity() {
    this.entity = this.scene.physics.add.sprite(this.x, this.y, "gameSprites");
  }

  /**
   *
   * @param {Phaser.Types.Input.Keyboard.CursorKeys} input
   */
  update(input) {
    if (input.left.isDown) {
      this.entity.setVelocityX(-200);
    } else if (input.right.isDown) {
      this.entity.setVelocityX(200);
    } else if (input.down.isDown) {
      this.entity.setVelocityY(200);
    } else if (input.up.isDown) {
      this.entity.setVelocityY(-200);
    } else {
      this.entity.setVelocityX(0);
      this.entity.setVelocityY(0);
    }
  }

  /**
   * @param {Phaser.Scene} scene
   * This function supposed to load atlas assosiated with player
   */
  static load(scene) {
    scene.load.atlas(
      "gameSprites",
      "assets/spritesPlayer.png",
      "assets/mapPlayer.json"
    );
  }
}

I've mapped the sprites using a tool called Leshy SpriteSheet; we can discuss this further in another thread if needed.

Finally, here's how it all comes together in the scene:

import { Scene, Game, WEBGL } from "phaser";
import { Player } from "./entities/player/player";

const canvas = document.getElementById("game");

class GameScene extends Scene {
  /** @type {Player} */
  #player2;

  constructor() {
    super("scene-game");
  }

  preload() {
    Player.load(this);
  }

  create() {
    this.#player2 = new Player(this, 400, 300);

    this.cursor = this.input.keyboard.createCursorKeys();
    this.#player2.entity.anims.play("stand");
  }

  update(_time, _delta) {
    this.#player2.update(this.cursor);
  }
}

There's just a call to load the associated sprites, and we can create as many players as we want with their starting positions as parameters.

AlexandreBillereau commented 5 months ago

btw we can opt for "preload" instead of "load" to be more consistent.

idk what you think about this but its work pretty well.

I'm just a bit worried about the instance inheritance for calculating the position based on the entity. I haven't looked into it yet.

DrainGK commented 5 months ago

I globaly agree with this code!

However, I have an Idea for the input handler.

Creating an Input Manager and then using it where we need to use it (usually in the player class but also in the menu for exemple)

There is my input Manager

class InputManager {
    constructor(scene, onMove) {
        this.scene = scene;
        this.onMove = onMove; // Callback function for movement

        // Default to "WASD" layout, can be changed as needed
        this.setLayout("WASD");
    }

    setLayout(layout) {
        // Clears previous key listeners if they exist
        if (this.keys) {
            Object.values(this.keys).forEach(key => {
                key.removeAllListeners();
            });
        }

        // Use switch-case to handle different layouts
        switch (layout) {
            case "WASD":
                this.keys = this.scene.input.keyboard.addKeys({
                    up: 'W', down: 'S', left: 'A', right: 'D'
                });
                break;
            case "ZQSD":
                this.keys = this.scene.input.keyboard.addKeys({
                    up: 'Z', down: 'S', left: 'Q', right: 'D'
                });
                break;
            case "ARROWS":
                this.keys = this.scene.input.keyboard.addKeys({
                    up: Phaser.Input.Keyboard.KeyCodes.UP,
                    down: Phaser.Input.Keyboard.KeyCodes.DOWN,
                    left: Phaser.Input.Keyboard.KeyCodes.LEFT,
                    right: Phaser.Input.Keyboard.KeyCodes.RIGHT
                });
                break;
            default:
                console.warn(`Layout ${layout} is not recognized. Falling back to default WASD layout.`);
                this.keys = this.scene.input.keyboard.addKeys({
                    up: 'W', down: 'S', left: 'A', right: 'D'
                });
        }

        this.listenForInput();
    }

    listenForInput() {
        // Register 'down' event for each direction
        Object.keys(this.keys).forEach(direction => {
            this.keys[direction].on('down', () => {
                this.onMove(direction);
            });
        });
    }
}

and then we can use it that way inside the player class:

move(direction) {
    const speed = 300;

    // Reset velocity each time to stop moving when keys are released
    this.player.body.setVelocity(0);

    switch (direction) {
        case 'up':
            this.player.body.setVelocityY(-speed);
            break;
        case 'down':
            this.player.body.setVelocityY(speed);
            break;
        case 'left':
            this.player.body.setVelocityX(-speed);
            break;
        case 'right':
            this.player.body.setVelocityX(speed);
            break;
    }
}
AlexandreBillereau commented 5 months ago

So i'm aggree for the Input manager we can't stay with arrow 😂😂😂

to finalize do we change some syntax about Entity like preload is better than load ? name are good ?

So if you okey with all, i finish this issue i push the Entity on dev

what's next :

DrainGK commented 5 months ago

nd: The input manager does not work!

"load" is to general and might be confusion for the future! "hmm load what's does it mean, where, how, when" etc... so many question, it's better to use prefix in my eyes. like preload and onload, it will help to be clear in the first sight.

If on your side those Entity and Player classes does work then let's push them on dev!

My next task will be the input manager to finally work and apply it into your Player class.

nd: also we should keep in mind to work with simple shapes with our game. It should work as an MVP and then implementing sprites on the next