I am participating in the team competition of Nuggets Community Game Creative Submission Contest. For details, please see: Game Creative Submission Contest
preface
In this article, I will develop an H5 game from scratch, primarily using Phaser3 to make the game. Considering the severe situation of the current epidemic, I also integrated some elements into this game. Meanwhile, I hope the epidemic will end as soon as possible, and I can take off masks as soon as possible, so that WE can see the smiles on each other’s faces.
- Element 1: Wear a mask when you go out
- Element two: struggle for life, is to collect food
- Element 3: Brave pingbo, to kill the demon monster, fight against various dark forces
I don’t think it’s much fun, because I didn’t design too many levels, but this article is definitely a good tutorial. By reading this article, you can master the whole process of H5 game development, so that you can quickly develop a similar RPC game. Let’s see the effect first!
Demo address: game.runjs.cool/
Code repository: github.com/maqi1520/ph…
Using the Technology stack
- Phaser: Game engine
- Vite: Project scaffolding for quick startup of web development servers for quick hot updates
- Typescript: Ts provides powerful type hints that reduce the need to check API documentation
Phaser profile
Phaser is an open source JavaScript 2D game development framework. It uses Canvas and WebGL to render our game, but we don’t have to use Canvas and WebGL apis directly. It encapsulates a large number of game development classes and methods, making it very easy to get started and a good choice for those who want to use JS to develop their game.
Initialization engineering
yarn create vite@latest game-phaser3 --template vanilla-ts
yarn add phaser
cd game-phaser3
mkdir public/assets src src/classes src/scenes
Copy the code
Create a native typescript template using Vite and install phaser,
assets
— Used to store game materials. For game materials, we can find them on game sharing websites, such as:itch.ioDownload above.classes
— Separate classes for game characters, monsters, etcscenes
– Used to store the game scene
Initialize the game
Next we need to initialize a gameobject in SRC /index.ts
import { Game, Types, WEBGL } from "phaser";
import { LoadingScene } from "./scenes";
export const gameConfig: Types.Core.GameConfig = {
type: WEBGL,
parent: "app".backgroundColor: "#9bd4c3".scale: {
mode: Scale.ScaleModes.NONE,
width: window.innerWidth,
height: window.innerHeight,
},
physics: {
default: "arcade".arcade: {
debug: false,}},render: {
antialiasGL: false.pixelArt: true,},callbacks: {
postBoot: () = >{ sizeChanged(); }},canvasStyle: `display: block; width: 100%; height: 100%; `.autoFocus: true.audio: {
disableWebAudio: false,},scene: [LoadingScene, GameScene, UIScene],
};
window.game = new Game(gameConfig);
Copy the code
-
Type: The game render type, which can be CANVAS, WEBGL or AUTO. Many effects may not be supported in CANVAS mode, so we use WEBGL
-
Parent: The parent DOM ID of the game rendering canvas element
-
BackgroundColor: backgroundColor of the canvas
-
Scale: Adjusts the scale of the game canvas.
-
Physics: Set up the game physics engine
-
Render: An additional attribute to the game render
-
Callbacks: Callbacks that will be triggered either before (preBoot) or after (postBoot) game initialization
-
CanvasStyle: CSS style of the Canvas element
-
AutoFocus: autoFocus on the game canvas
-
Audio: Game audio Settings
-
Scene: List of scenes to load in the game.
Windows does not have a game object. You need to extend the Window object in viet-env.d.ts
interface Window {
game: Phaser.Game;
}
Copy the code
Add a method that lets the browser adapt when zooming
function sizeChanged() {
if (window.game.isBooted) {
setTimeout(() = > {
window.game.scale.resize(window.innerWidth, window.innerHeight);
window.game.canvas.setAttribute(
"style".`display: block; width: The ${window.innerWidth}px; height: The ${window.innerHeight}px; `
);
}, 100); }}window.onresize = () = > sizeChanged();
Copy the code
Create a new scene
A game is composed of many scenes, and at least one scene is added to a game, usually divided into three scenes: Loading, game and UI
- Loading scenarios are used to load game resources
- The game scene is the main part of the game and can be divided into several
- UI scenarios are used for page UI elements, text prompts, and so on
The following code is an example of a simple scenario
import { Scene } from 'phaser';
export class LoadingScene extends Scene {
constructor() {
super('loading-scene');
}
init(data) {}
preload() {
this.load.baseURL = "assets/";
this.load.image("king"."sprites/king.png");
}
create(data): void {
this.add.sprite(100.100."king");
}
update(time, delta){}}Copy the code
Scenarios also have lifecycle functions
- Init: the scenario is initialized
- Preload: What resources need to be loaded before the scene loads
- Create: Triggered when the scene is created
- Update: The scene is triggered every render frame is updated (approximately 60 frames per second)
Run Yarn Dev to start. At this point, you should see something like this in your browser
Create the role
Now that the scene is set up, it’s time for the hero to step out. Create the SRC /classes/player.ts file
import { Physics } from "phaser";
export class Player extends Physics.Arcade.Sprite {
private cursors: Phaser.Types.Input.Keyboard.CursorKeys;
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, "king");
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setSize(30.30);
this.body.setOffset(8.0);
this.cursors = this.scene.input.keyboard.createCursorKeys();
}
protected checkFlip(): void {
if (this.body.velocity.x < 0) {
this.scaleX = -1;
} else {
this.scaleX = 1;
}
}
update(): void {
this.setVelocity(0);
if (this.cursors.up.isDown) {
this.body.velocity.y = -110;
}
if (this.cursors.left.isDown) {
this.body.velocity.x = -110;
this.checkFlip();
this.setOffset(48.15);
}
if (this.cursors.down.isDown) {
this.body.velocity.y = 110;
}
if (this.cursors.right.isDown) {
this.body.velocity.x = 110;
this.checkFlip();
this.setOffset(15.15); }}}Copy the code
Keyboard events
Player inheritance Physics. Arcade. Sprite class, in the instantiation incoming coordinates x, y, and resource ID, by enclosing scene. The input. The rid_device_info_keyboard. CreateCursorKeys keyboard direction key, when the direction key is pressed, Change the speed in Player X and y.
Add a scene game SRC /scenes/game.ts
import { Scene } from "phaser";
import { Player } from ".. /.. /classes/Player";
export class GameScene extends Scene {
privateplayer! : Player;constructor() {
super("game-scene");
}
create(): void {
this.player = new Player(this.100.100);
}
update(): void {
this.player.update(); }}Copy the code
Initialize a hero PLayer by calling the update method of the PLayer in the update function.
Then change the create method in loading scenarios to transition from loading scenarios to game scenarios.
create(): void {
this.scene.start("game-scene");
}
Copy the code
At this point, we can control the hero through the keyboard orientation.
Use Tiled to draw tile map
Next is the map, we need to download Tiled (free) first, to create the game map
Create a new project first, and the gallery layer must select CSV, otherwise Phaser3 will not parse.
Next, build the map block set. Note that you must select the embedded map, otherwise it will not be able to parse.
Tiled is divided into properties area, layer area and image block area. You can select the image block by Commond +A, and then design the game map freely with the stamp tool and rectangle tool.
To prevent the character from moving outside the map, divide the layers into Ground and Walls.
To keep moving objects like characters and monsters from leaving the map, we need to edit the block properties.
Set custom properties on some graph blockscollides
为 true
Later code can turn on collision detection with this property.
Added anchor points for monsters and food
Right click on the new object layer and rename it to Enimes to add some anchor points. These anchor points can be rendered as monster points in the game, and also need to add some food points.
Select the object layer, the anchor can change the name, according to the name, we can render different objects.
The last step is to export the file as JSON to our assets folder. File -> Export as… -> format.json.
Load tile map
Now that the map is designed, we need to render our map in the game.
Resources are first loaded in the preload method in the loading scenario.
this.load.image({
key: "Grass".url: "tilemaps/json/Grass.png"});this.load.tilemapTiledJSON("tilemapGrass"."tilemaps/json/Grass.json");
this.load.spritesheet("water"."spritesheets/Water.png", {
frameWidth: 64.frameHeight: 16});Copy the code
Then add an initMap method to the game to initialize the map
private initMap(): void {
// Add water as background
this.add.tileSprite(0.0.window.innerWidth, window.innerHeight, "water");
this.map = this.make.tilemap({
key: "tilemapGrass".tileWidth: 16.tileHeight: 16});this.tileset = this.map.addTilesetImage("Grass"."Grass");// The first parameter is the name of the image block, and the second parameter is the key of the image
this.groundLayer = this.map.createLayer("Ground".this.tileset, 0.0);
this.wallsLayer = this.map.createLayer("Walls".this.tileset, 0.0);
// Set the wall with the map block property and enable the collision property
this.wallsLayer.setCollisionByProperty({ collides: true });
// Set the edges of the world
this.physics.world.setBounds(
0.0.this.wallsLayer.width,
this.wallsLayer.height
);
}
Copy the code
Note that the first name of addTilesetImage must be the same as the design-time block name.
The map is then initialized in the Create method.
create(): void {
this.initMap();
this.player = new Player(this.100.100);
}
Copy the code
In Phaser, functions are also executed sequentially, with the method executed first rendering first, at the bottom. So you load the map first, and then you initialize the Player object.
Now you can see a hero in the game scene.
Collision detection
But moving the character, the character walks into the water, so we need to enable collision detection,
In the create method, add the following code to enable collision detection so that the hero cannot step out of the water through the keyboard.
this.physics.add.collider(this.player, this.wallsLayer);
Copy the code
To prevent blocks from having collides attributes set while designing the map, we can highlight the walls that we are colliding so that we can debug them easily.
private initMap(): void{...this.showDebugWalls(); }...private showDebugWalls(): void {
const debugGraphics = this.add.graphics().setAlpha(0.7);
this.wallsLayer.renderDebug(debugGraphics, {
tileColor: null.collidingTileColor: new Phaser.Display.Color(24.234.48.255)}); }Copy the code
The effect is as follows:
Create frame-by-frame animations using Sprite diagrams
Currently our hero is static, to make our hero move when running, we can use the Sprite map, first look at our Sprite map, specially added a mask to the Sprite map.
We also need to load a JSON that describes the Sprite diagram. Let’s take a look at the JSON data structure
JSON describes the position and center point of each frame of the Sprite. Of course, the JSON is not handwritten, so you can use the Texture Packer tool to package it.
Load the Sprite and JSON in Preload
...this.load.atlas(
"a-king"."spritesheets/a-king_withmask.png"."spritesheets/a-king_atlas.json"); ...Copy the code
Then load the initialization animation in player.js
constructor(scene: Phaser.Scene, x: number, y: number){...this.initAnimations();
}
private initAnimations(): void {
this.scene.anims.create({
key: "run".frames: this.scene.anims.generateFrameNames("a-king", {
prefix: "run-".end: 7,}).frameRate: 8});this.scene.anims.create({
key: "attack".frames: this.scene.anims.generateFrameNames("a-king", {
prefix: "attack-".end: 2,}).frameRate: 8}); }Copy the code
Finally, in the update function, the animation is called when the arrow key is pressed.
update(): void {
this.setVelocity(0);
if (this.cursors.up.isDown) {
this.body.velocity.y = -110;
!this.anims.isPlaying && this.anims.play("run".true); }...if (this.cursors.space.isDown) {
this.anims.play("attack".true); }}Copy the code
The animation of the character is successful!
Create a monster
As with Player, we can create the same class called Enemy to write monster logic. You just need to load different Sprite resources. Another difference is that the monster’s movements are not controlled by the keyboard, but automatically. So we need to implement the logic of monsters running automatically.
Monster automatic movement mainly has the following two points:
- Monsters move around in place when they don’t see a character.
- When the hero is found, the monster will chase the hero. Its principle is to judge the distance between the monster and the player. If the distance is less than a certain value, the monster will set the movement speed.
enum Direction {
UP,
DOWN,
LEFT,
RIGHT,
}
// Randomly generate different directions
const randomDirection = (exclude: Direction) = > {
let newDirection = Phaser.Math.Between(0.3);
while (newDirection === exclude) {
newDirection = Phaser.Math.Between(0.3);
}
returnnewDirection; }; .// Change the direction of the monster every 2 seconds
this.moveEvent = this.scene.time.addEvent({
delay: 2000.callback: () = > {
this.direction = randomDirection(this.direction);
},
loop: true}); .Copy the code
private AGRESSOR_RADIUS = 100;
preUpdate(t: number, dt: number) {
super.preUpdate(t, dt);
// If the distance is less than 100, set a speed
if (
Math.Distance.BetweenPoints(
{ x: this.x, y: this.y },
{ x: this.target.x, y: this.target.y }
) < this.AGRESSOR_RADIUS
) {
this.getBody().setVelocityX(this.target.x - this.x);
this.getBody().setVelocityY(this.target.y - this.y);
} else {
// If the value is greater than 100, it will randomly go up or down
const speed = 50;
switch (this.direction) {
case Direction.UP:
this.getBody().setVelocity(0, -speed);
break;
case Direction.DOWN:
this.getBody().setVelocity(0, speed);
break;
case Direction.LEFT:
this.getBody().setVelocity(-speed, 0);
break;
case Direction.RIGHT:
this.getBody().setVelocity(speed, 0);
break; }}}Copy the code
In the preUpdate function set the monster to automatically move logic, distance less than 100, set a speed towards the hero, greater than 100, random 4 directions automatically move.
Render monsters according to anchor points
Next we need to instantiate the monster based on the anchor points created on the map. Add an initEnemies method to the Game scene to initialize monsters.
private initEnemies(): void {
// EnemyPoint on the past map
const enemiesPoints = this.map.filterObjects(
"Enemies".(obj) = > obj.name === "EnemyPoint"
);
// instantiate the monster
this.enemies = enemiesPoints.map(
(enemyPoint) = >
new Enemy(
this,
enemyPoint.x as number,
enemyPoint.y as number."lizard".this.player
)
);
// Monsters and walls added collision checks
this.physics.add.collider(this.enemies, this.wallsLayer);
// Monsters and monsters added collision checks
this.physics.add.collider(this.enemies, this.enemies);
// Added collision checks for monsters and characters
this.physics.add.collider(
this.player,
this.enemies,
(obj1, obj2) = > {
// After the collision callback, the character receives damage -1
(obj1 as Player).getDamage(1);
},
undefined.this
);
}
Copy the code
This is where collision checking and post-collision callbacks are concerned. We can create characters and monsters on the map, and monsters can attack heroes, but our heroes can’t attack monsters.
Event notification
Therefore, we need to add event monitoring to the monster. When the distance between the monster and the character is less than the width of the character, it indicates a hit
this.attackHandler = () = > {
if (
Math.Distance.BetweenPoints(
{ x: this.x, y: this.y },
{ x: this.target.x, y: this.target.y }
) < this.target.width
) {
this.getDamage();
this.disableBody(true.false);// Stops the monster object body without disappearing
this.scene.time.delayedCall(300.() = > {
this.destroy();// Disappear after 300 milliseconds}); }};// EVENTS
this.scene.game.events.on(EVENTS_NAME.attack, this.attackHandler, this);
// Cancel listening after destruction
this.on("destroy".() = > {
this.scene.game.events.removeListener(
EVENTS_NAME.attack,
this.attackHandler
);
});
Copy the code
When the space bar is pressed, the Player tool animation is played and a global event is sent
if (this.cursors.space.isDown) {
this.anims.play("attack".true);
this.scene.game.events.emit(EVENTS_NAME.attack);
}
Copy the code
Show food according to anchor points
Similar to rendering monsters, we can render some food on the map.
These materials were downloaded from Iconfont, and then assembled into sprites through Figma.
private initChests(): void {
const chestPoints = this.map.filterObjects(
"Chests".(obj) = > obj.name === "ChestPoint"
);
this.chests = chestPoints.map((chestPoint) = >
this.physics.add
.sprite(
chestPoint.x as number,
chestPoint.y as number."food".Math.floor(Math.random() * 8)
)
.setScale(0.5));this.chests.forEach((chest) = > {
this.physics.add.overlap(this.player, chest, (obj1, obj2) = > {
this.game.events.emit(EVENTS_NAME.chestLoot);
obj2.destroy();
});
});
}
Copy the code
Like the monster, the food is rendered first according to the anchor point, but the callback is different when the hero and food collide, when the hero and food overlap, the player gets 10 points
Text display
Now let’s display an HP value above the character’s head.
import { Text } from './text'; . private hpValue: Text; .this.hpValue = new Text(this.scene, this.x, this.y - this.height, this.hp.toString())
.setFontSize(12)
.setOrigin(0.8.0.5); .update(){...this.hpValue.setPosition(this.x, this.y - this.height * 0.4);
this.hpValue.setOrigin(0.8.0.5);
}
...
public getDamage(value?: number): void {
super.getDamage(value);
this.hpValue.setText(this.hp.toString());
}
Copy the code
The HP value will now be displayed above the game character, and in the update method we have updated the position of the HP text value so that the PLayer will not have a problem even if it moves.
The UI display
Finally, let’s add a UI scenario for displaying system prompts.
import { Scene } from "phaser";
import { EVENTS_NAME, GameStatus } from ".. /.. /consts";
import { Score, ScoreOperations } from ".. /.. /classes/score";
import { Text } from ".. /.. /classes/text";
import { gameConfig } from ".. /.. /";
export class UIScene extends Scene {
privatescore! : Score;privategameEndPhrase! : Text;private chestLootHandler: () = > void;
private gameEndHandler: (status: GameStatus) = > void;
constructor() {
super("ui-scene");
this.chestLootHandler = () = > {
this.score.changeValue(ScoreOperations.INCREASE, 10);
if (this.score.getValue() === gameConfig.winScore) {
this.game.events.emit(EVENTS_NAME.gameEnd, "win"); }};this.gameEndHandler = (status) = > {
this.cameras.main.setBackgroundColor("Rgba (0,0,0,0.6)");
this.game.scene.pause("game-scene");
this.gameEndPhrase = new Text(
this.this.game.scale.width / 2.this.game.scale.height * 0.4,
status === GameStatus.LOSE
? ` failed! \n\n Click the screen to start again
: ` victory! \n\n Click the screen to start again
)
.setAlign("center")
.setColor(status === GameStatus.LOSE ? "#ff0000" : "#ffffff");
this.gameEndPhrase.setPosition(
this.game.scale.width / 2 - this.gameEndPhrase.width / 2.this.game.scale.height * 0.4
);
this.input.on("pointerdown".() = > {
this.game.events.off(EVENTS_NAME.chestLoot, this.chestLootHandler);
this.game.events.off(EVENTS_NAME.gameEnd, this.gameEndHandler);
this.scene.get("game-scene").scene.restart();
this.scene.restart();
});
};
}
create(): void {
this.score = new Score(this.20.20.0);
this.initListeners();
}
private initListeners(): void {
this.game.events.on(EVENTS_NAME.chestLoot, this.chestLootHandler, this);
this.game.events.once(EVENTS_NAME.gameEnd, this.gameEndHandler, this); }}Copy the code
Loading in a Loading scenario with a game scenario.
...this.scene.start("game-scene");
this.scene.start("ui-scene"); ...Copy the code
When the hero’s HP is 0, the screen will say “fail”.
The deployment of
I used Vercel deployment. Just upload Github and Vercel will deploy automatically. Then the domain name CNAME goes to cname.vercel-dns.com.
Demo address: game.runjs.cool/
Code repository: github.com/maqi1520/ph…
I have also deployed the following applications
Editor.runjs. cool/ MDX typeset editor CV. Runjs. cool/ online resume generator low-code.runjs.cool/ easy version low code platform and open source, if you help remember to click a star, thank you!
summary
At this point, the Phaser 3 mini game is 90 percent complete. The remaining 10 percent needs to be polished and refined to make the game more enjoyable, and more levels need to be designed to make it more rewarding for users. Through this article, we implemented a Phaser.js development H5 game from scratch. Includes Sprite map, Sprite table, design map, animation, collision check, event notification, etc.
I believe that through the above learning, IN the future work, I will have a certain understanding of similar H5 games, and can quickly develop a small game.
The last
Thanks to @dashuai old ape to help design the mask wizard figure, Dashuai also created the “Ape creative camp”, the group has a lot of development leaders can help each other to answer questions and exchange technology, at the same time, Dashuai will share outsourcing, do sideline, interested partners can leave a message “into the group”.
That’s all the content of this article. I hope this article is helpful to you. You can also refer to my previous articles or share your thoughts and experiences in the comments section.
This article first nuggets platform, source ma blog