9

Part Two: Creating Games using Vite, BabylonJS, and TypeScript - The Game World...

 1 year ago
source link: https://www.willieliwa.com/posts/gameworld
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
January 4, 2023

The Core Element of Every Game

The core element of every game is the player's character and how it feels to control. Having a clunky character can make a game feel more difficult than it needs to be, although this can also be a design choice. We'll start here and make a player that can move around using a basic Entity class and a World class to manage everything. Next, we'll add monsters to create a basic arcade game.

1. Creating the Classes

All code available on github

We'll keep everything in modular code files located in ./src/world/. This will have the Entity, World, and Player classes to make clear up some code in Game.ts.

Here's how the structure of your project will look like:



1tree 2devlon 3|... 4├── src 5│   ├── game.ts 6│   ├── main.ts 7│   ├── style.css 8│   ├── typescript.svg 9│   ├── vite-env.d.ts 10│   └── world <---------- we'll create these. 11│   ├── entity.ts <--| basic entity class. 12│   ├── player.ts <--| player with w,a,s,d movement. 13│   └── world.ts <--| manages entities and player. 14|... 15

The following will all happen inside the ./src/world/ folder. Create it now if you don't have it already. Check out the github repo to see the project structure.

Create the class Entity inside /world/entity.ts

The Entity class defines common properties and methods that we want to be shared between all our entities.



1// entity.ts 2 3import { Mesh, MeshBuilder, Scene, Vector3 } from "babylonjs"; 4 5export default class Entity { 6 mesh: Mesh; 7 moveDirection: Vector3 = new Vector3(); 8 constructor( 9 public id: string, 10 public position: Vector3 = new Vector3(), 11 public diameter: number = 2, 12 public moveSpeed: number = 0.2, 13 public scene?: Scene 14 ) { 15 this.mesh = MeshBuilder.CreateSphere( 16 this.id, 17 { diameter: diameter }, 18 scene 19 ); 20 } 21 22 move(direction: Vector3) { 23 this.position.addInPlace(direction.scale(this.moveSpeed)); 24 } 25 update() { 26 this.mesh.moveWithCollisions(this.moveDirection.scale(this.moveSpeed)); 27 } 28} 29

Create the class World inside /world/world.ts

The World class manages a list of all the entities in the game including the player.



1// world.ts 2 3import { Scene } from "babylonjs"; 4import Entity from "./entity"; 5import Player from "./player"; 6 7export default class World { 8 player: Player; 9 constructor(scene: Scene, public entities: Entity[] = []) { 10 this.player = new Player(scene); 11 } 12 13 addEntity(entity: Entity) { 14 this.entities.push(entity); 15 } 16 17 removeEntity(entity: Entity) { 18 const index = this.entities.indexOf(entity); 19 if (index !== -1) { 20 this.entities.splice(index, 1); 21 } 22 } 23 update() { 24 this.player.update(); 25 this.entities.forEach((e) => e.update()); 26 } 27} 28

Create the class Player inside /world/player.ts

The Player class inherits from the Entity class and handles specific player-related functionality, such as movement controlled by the keyboard. We'll add the keyboard controls later in the tutorial.

Since we defined everything in Entity it starts off pretty simple. We'll add the keyboard movement next chapter.



1// player.ts 2 3import { Scene, Vector3 } from "babylonjs"; 4import Entity from "./entity"; 5 6export default class Player extends Entity { 7 constructor(public scene: Scene) { 8 super("player", new Vector3(), 0.2); 9 // Change the color to green. 10 var playerMat = new StandardMaterial("player", scene); 11 playerMat.diffuseColor = new Color3(0.5, 1, 0.5); 12 this.mesh.material = material; 13 } 14} 15

Adding everything to the game

The Game class will have a World instanced. This way we can update the state of each entity in the game world in the game loop.

In ./src/game.ts

108 Lines of code! When you see this // .. it means some of the code is cut for clarity. You don't need to copy it.

It takes on ~10,000 lines of code to make a simple iPhone game.



1// game.ts 2 3import { 4 Engine, 5 Scene, 6 Vector3, 7 MeshBuilder, 8 FreeCamera, 9 HemisphericLight, 10} from "babylonjs"; 11 12import World from "./world/world"; // <--- import world 13 14export default class Game { 15 engine: Engine; 16 scene: Scene; 17 world: World; 18 19 constructor(readonly canvas: HTMLCanvasElement) { 20 this.engine = new Engine(canvas); 21 window.addEventListener("resize", () => { 22 this.engine.resize(); 23 }); 24 this.scene = createScene(this.engine, this.canvas); 25 this.world = new World(this.scene); // <--- init world. 26 } 27 28 // ... <--- collapsed debug() {...} 29 30 run() { 31 this.debug(true); 32 this.engine.runRenderLoop(() => { 33 this.scene.render(); 34 this.world.update(); // <--- add update method to loop. 35 }); 36 } 37} 38 39// ... 40

Nice! We can now remove all the CreateSphere code inside the createScene function.

We can also change the color's of the game world to make the entities stand out more.



1// ... game.ts 2 3var createScene = function (engine: Engine, canvas: HTMLCanvasElement) { 4 var scene = new Scene(engine); 5 scene.clearColor = new Color4(0.9, 1, 1, 1); // <--- Change color of sky. 6 7 var camera = new FreeCamera("camera1", new Vector3(0, 5, -10), scene); 8 9 camera.setTarget(Vector3.Zero()); 10 camera.attachControl(canvas, true); 11 12 var light = new HemisphericLight("light", new Vector3(0, 1, 0), scene); 13 light.intensity = 0.7; 14 15 // + add new ambient light to make colors pop. 16 var ambient = new HemisphericLight("ambient", new Vector3(1, 1, 0), scene); 17 ambient.groundColor = new Color3(0.9, 1, 1); 18 ambient.intensity = 0.2; 19 20 /// - Remove `CreateSphere` code 21 22 /// + Make the ground object much bigger. 23 var ground = MeshBuilder.CreateGround( 24 "ground", 25 { width: 25, height: 25 }, 26 scene 27 ); 28 ground.position.y = -1; 29 30 return scene; 31}; 32

2. Player Movement

Player movement uses the keyboard key-down and key-up events to update the movementDirection property we defined in the Entity class. It's very basic and doesn't have any physics involved but it'll work for demonstrating KeyboardEventTypes

KeyboardEventTypes

Go back to ./src/world/player.ts and add the following below the constructor



1// ... player.ts 2 3import { KeyboardEventTypes, Scene, Vector3 } from "babylonjs"; // <--+ Imports 4import Entity from "./entity"; 5 6export default class Player extends Entity { 7 constructor(public scene: Scene) { 8 super("player", new Vector3(), 0.2); 9 var material = new StandardMaterial("material", scene); 10 material.diffuseColor = new Color3(0.5, 1, 0.5); 11 this.mesh.material = material; 12 // Register event handlers. 13 scene.onKeyboardObservable.add((kbInfo) => { 14 switch (kbInfo.type) { 15 case KeyboardEventTypes.KEYDOWN: 16 this.onKeyDown(kbInfo.event.keyCode); 17 break; 18 case KeyboardEventTypes.KEYUP: 19 this.onKeyUp(kbInfo.event.keyCode); 20 break; 21 } 22 }); 23 } 24 25 // When a key is pressed and held. 26 onKeyDown(keyCode: any) { 27 // Set direction based on the key pressed. 28 switch (keyCode) { 29 case 87: // W key 30 this.moveDirection.z = 1; 31 break; 32 case 65: // A key 33 this.moveDirection.x = -1; 34 break; 35 case 83: // S key 36 this.moveDirection.z = -1; 37 break; 38 case 68: // D key 39 this.moveDirection.x = 1; 40 break; 41 } 42 } 43 44 // When a key is released. 45 onKeyUp(keyCode: any) { 46 // Reset the direction based on the key released 47 switch (keyCode) { 48 case 87: // W key 49 this.moveDirection.z = 0; 50 break; 51 case 65: // A key 52 this.moveDirection.x = 0; 53 break; 54 case 83: // S key 55 this.moveDirection.z = 0; 56 break; 57 case 68: // D key 58 this.moveDirection.x = 0; 59 break; 60 } 61 } 62} 63 64// ... 65

Can we move yet?

Yes! Run the code and try to move the player with W, A, S, D. You can move the camera with your keyboard's arrow keys, but we will disable this is when we implement the camera following logic in the next chapter.

Check the GitHub repo to see if your progress matches so far.

3. Making the Camera Follow the Player

The camera following logic simply updates the position to match the player's position. BabylonJS has a built-in FollowCamera class but we won't be using that that way we have more control over the behavior. We'll use the UniversalCamera which is best for most games as it works with a keyboard, mouse, touchscreen, and gamepad.

Refactor camera code and create a UniversalCamera



1// ... game.ts 2 3export default class Game { 4 engine: Engine; 5 scene: Scene; 6 world: World; 7 camera: UniversalCamera; // <--+ Game now has camera instance. 8 camRoot: TransformNode; // <--+ Used to track camera position. 9 10 constructor(readonly canvas: HTMLCanvasElement) { 11 // ... 12 this.camera = new UniversalCamera( 13 "cam", 14 new Vector3(0, 10, -10), <--- Top down view. 15 this.scene 16 ); 17 this.camRoot = new TransformNode("root"); 18 this.setupCamera(); // <----------------| We will make these next. 19 this.camera = this.registerCamera(); <---| 20 } 21} 22 23// ... debug() {...} 24 25// Remove `canvas: HTMLCanvasElement` | 26// it's not needed inside this function. v 27var createScene = function (engine: Engine) { 28 var scene = new Scene(engine); 29 30 // ^ ^ ^ 31 // | | | 32 /// Move all camera code to `constructor()`. 33 34 var light = new HemisphericLight("light", new Vector3(0, 1, 0), scene); 35 light.intensity = 0.7; 36 37 // ... 38

This way we have access to the camera through the Game class. We'll use the update method next to match its position with the player.

Follow the Player

Using the camRoot we'll move the camera to match the player's position before we render the scene. We create 4 methods in the Game class to help us do this:



1// ... game.ts/Game 2 3// Registers `beforeRenderUpdate` and `updateCamera` on the scene 4// to run before the main game loop. 5public registerCamera(): UniversalCamera {} 6 7// Runs code that must happen before the main loop. 8private beforeRenderUpdate(): void {} 9 10// Creates the camera. 11private setupCamera(): UniversalCamera {} 12 13// Camera follow logic. 14private updateCamera(): void {} 15 16// ... 17

Camera Update Loop.

Add these methods to your class Game right below the debug method.



1// ... game.ts/Game 2 3// debug() {...} 4 5public registerCamera(): UniversalCamera { 6 this.scene.registerBeforeRender(() => { 7 this.beforeRenderUpdate(); 8 this.updateCamera(); 9 }); 10 return this.camera; 11} 12 13private beforeRenderUpdate(): void { 14 this.world.update(); 15} 16 17private setupCamera(): UniversalCamera { 18 // Root camera parent that follows the player. 19 this.camRoot = new TransformNode("root"); 20 this.camRoot.position = new Vector3(0, 0, 0); 21 this.camRoot.rotation = new Vector3(0, 0, 0); 22 23 // Rotations along the x-axis (up/down tilting) 24 let yTilt = new TransformNode("ytilt"); 25 yTilt.parent = this.camRoot; 26 27 // The camera points to the root's position. 28 this.camera.lockedTarget = this.camRoot.position; 29 this.camera.fov = 1.1; // <--- zoom out a bit, default is 0.8 30 this.camera.parent = yTilt; 31 32 this.scene.activeCamera = this.camera; 33 return this.camera; 34} 35 36private updateCamera(): void { 37 let player = this.world.player; 38 // Smooth movement towards the player's position. 39 this.camRoot.position = Vector3.Lerp( 40 this.camRoot.position, 41 player.mesh.position, 42 0.4 43 ); 44} 45 46run() { 47 this.debug(true); 48 this.engine.runRenderLoop(() => { 49 this.scene.render(); 50 // ^^^ Move `this.world.update` to `beforeRenderUpdate()` 51 }); 52} 53 54

4. Run the Game

You should see a more zoomed-out version of what we started with. You can move around with the W, A, S, D keys:

Player moving around

Cool! Let's fight some monsters --> Part Three: Randomly Spawning Monsters


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK