preface
Recently, I was working on a h5 game project, so I needed to look at the front-end framework related to the game, and finally chose PIXI (I didn’t bother to pick). The fastest way to learn a framework is development, so I spent some time writing a simple game to get a general understanding of PIXI. There should be no problem dealing with some simple projects.
The demo address
- Please set the PC to mobile mode
- Image resources are web images for learning only
The game screen
preparation
Pixi basis
The Chinese document on Pixi.js (Huashengweilai.com) is relatively simple and only covers the basics of the game, but that’s enough for me
Stage 1.
Use pixi to create a stage where all game elements are displayed
import * as Pixi from 'pixi.js';
const app = new Pixi.Application();
// Put the stage in HTML
document.body.appendChild(app.view)
Copy the code
2. Load resources
Static resources need to be loaded by loader before they can be used. If we directly introduce resources to create sprites, it will have no effect. If the static resources are too large, we can wait for the loader to finish loading and perform page rendering.
import img from './assets/image/1.png';
// the loader can be chained, and the load must be executed at the end, otherwise it will not be loaded
// When loading resources, it is recommended to give a name, which is convenient to reference later
app.scene.loader.add('img', img).load();
// Loader loading progress
app.scene.loader.onProgress.add(() = > {
console.log('loading... ')})// The loader is loaded successfully
app.scene.loader.onComplete.add(() = > {
console.log('loading cpmplete')})Copy the code
3. Create a Sprite
After static resources are loaded, sprites are created and displayed in the stage. We can easily control the properties of sprites
// Create a Sprite
const img = new Pixi.Sprite(app.scene.loader.resources['img'].texture);
// Specify Sprite properties
/ wide/high
img.width = 100;
img.height = 100;
// x,y position
img.x = 100;
img.y = 100;
// Set the origin of the Sprite
img.anchor.x = 0.5;
img.anchor.y = 0.5;
// Set the zoom of the Sprite
img.scale.set(1);
// Set the Sprite rotation Angle
img.rotation = 1.2;
// Set the transparency of the Sprite
img.alpha = 0.9
// Put the Sprite on the stage
app.scene.stage.addChild(img);
Copy the code
4. ticker
Using the Ticker loop system, it’s easy to do some simple animations that are essentially similar to requestAnimationFrame
const ticker = Pixi.Ticker.shared;
const run = () = > {
img.x += 1;
img.y += 1;
if (img.x > 100) {
// Move the loop function out of ticker
ticker.remove(run);
};
};
// Add the loop function to the ticker
ticker.add(run);
// Stop all looping functions in ticker
ticker.stop();
// Start the loop
ticker.start();
Copy the code
5. Text elements
Construct a Text element with pixi.text
const options = {
fontFamily: "Arial".fontSize: 48.fill: "white"};const msg = new Pixi.Text('wenben', options);
// Modify the text
msg.text = 'wenben1';
// Modify the style
msg.style = {};
Copy the code
Game design
For the first time to make a game, it is correct to start from simple. I choose to make a shooting game, with the content as simple as possible. The game ideas and gameplay are as follows:
- Fix the cannon position, tap the area on the screen, rotate the muzzle to the corresponding Angle, and fire a shell
- Two monster types, moving from top to bottom, removed after off-screen
- Monster 1 collides with shell once, monster 1 and shell disappear, player scores one point
- Monster 2 collides once with the cannonball, the monster becomes smaller, continues to move, the cannonball disappears, a second collision occurs, the monster disappears, and the player scores two points
- Top left scoreboard
Matters needing attention:
- Random spawning of monster types and corresponding hit counts (health)
- Collision detection of monsters and shells
- Scoreboard updated
- Barrel rotation
Now that you know where you’re going, get started!
The game development
Project structure and development approach
This project uses singleton pattern and object orientation (false), the project construction is JS + webpack, originally used TS to write, but found that there is no need to write, so changed to JS. The project structure is as follows:
--- pixi-game
--- src
// Static resources
--- assets
/ / constant
--- constant
/ / component
--- components
/ / method
--- utils
/ / webpack configuration
--- config
// html
--- public
/ / the entry
--- index.js
Copy the code
Create a global APP instance
A game has only one app instance and can only initialize one, so I used singleton mode here. All components will then bind this instance to themselves by default, making it easy to call app methods
class Game {
// The default width of the stage
stageWitdh = window.innerWidth;
stageHeight = window.innerHeight;
// State of the game
state = {
reword: 0.play: false.over: false.paused: false};// When creating the scoreboard, assign it to game
msg = null;
// Create an array of shells for collision detection
bullets = [];
// Initialize the pixi stage
scene = new Pixi.Application({
width: this.stageWitdh,
height: this.stageHeight,
transparent: true.backgroundColor: 0x00000});// Dynamically change the stage size as the window changes
rerender() {
this.scene.view.width = this.window.innerWidth;
this.scene.view.height = this.window.innerHeight; }}/ / the singleton
function createGame() {
let game;
return () = > {
if(! game) { game =new Game();
}
return game;
};
};
export default createGame();
Copy the code
The cannon part
The whole cannon is divided into two parts, a fixed base and a barrel that can be rotated. It should be noted that by default, the rotation of Sprite is based on the upper left corner, which obviously does not meet the rotation of my cannon. Therefore, it is necessary to modify the origin of the barrel to make it rotate based on the center of the base.
class Connon {... instance = Game() ...constructor() {
this.cannonBase = new Sprite('game_cannon_base');
this.cannonBody = new Sprite('game_cannon_body'); .// Set the origin of the gun
this.cannonBody.anchor.x = 0.5;
this.cannonBody.anchor.y = 0.8;
// Gun body origin coordinates, convenient later calculation Angle
this.anchorPos = {
x: window.innerWidth / 2.y: window.innerHeight - this.cannonBody.height * 2.
}
// Set a safe height to prevent the gun from rotating at will
this.safeHeight = window.innerHeight - this.cannonBody.height*3/2
// Enable listening
this.listener();
}
listener() {
window.addEventListener('touchstart'.this.touchEvent.bind(this))}/ / touch events
touchEvent(e) {
// Get the touch coordinates
const { clientX, clientY } = e.changedTouches[0];
if (clientY > this.safeHeight) {
return;
}
// Calculate the rotation Angle
const x = this.anchorPos.x - clientX;
const y = this.anchorPos.y - clientY;
const rotate = this.cannonBody.rotation ? (+this.cannonBody.rotation).toFixed(2) : 0;
let nextRotate = -(x/y).toFixed(2); .// Rotation function
const run = () = >{...if (rotationStep == nextRotate.toFixed(2) | |Math.abs(rotationStep) >= 1.2) {
// When the rotation reaches the specified Angle or maximum Angle, it stops spinning and generates a shell
ticker.remove(run);
ticker.addOnce(() = > {
new Bullet({
rotation: nextRotate,
y: this.anchorPos.y,
})}
);
return; }...this.cannonBody.rotation = rotationStep; } ticker.remove(run); ticker.add(run); }}export default Connon;
Copy the code
The shell parts
When the rotation stops, a shell needs to be generated and fired at the specified Angle. The Angle of rotation of the shell is the same as the gun body. When the shell misses the target and exceeds the screen, it will be automatically removed.
class Bullet {
// Shell generation is relatively simple, deleted
// Focus on self detection
run(r) {
let stepX = r;
let stepY = -1;
const move = () = > {
stepX += r;
stepY += -1;
this.bullet.x += stepX;
this.bullet.y += stepY;
if (this.bullet.x < -this.bullet.width || this.bullet.y < -this.bullet.height) { ticker.remove(move); }}const remove = () = > {
ticker.remove(move);
this.instance.scene.stage.removeChild(this.bullet);
}
// Bind a removal method to the shell itself, which will be used later
this.bullet.remove = remove; ticker.add(move); }}export default Bullet;
Copy the code
The monster part
In this project, I found two pictures of monsters and used random numbers to create a type of monster randomly. The monster fell from different heights, causing a sense of delay. On the way of monster movement, I conducted collision detection and compared coordinates of all bullets on the current screen, and then made corresponding operations:
class Monster{
instance = Game()
speed = 6
// Monster info
monsterNames = [
{name: 'game_monster_b'.hit: 2.scale: 1.8.reword: 2},
{name: 'game_monster_s'.hit: 1.scale: 1.reword: 1}
]
hitCount = 0;
alphaStep = 0.1;
constructor(props) {
this.init();
}
init() {
// Select a random index
const randomIndex =Math.floor( Math.random() * 1 + 0.4);
const info = this.monsterNames[randomIndex];
this.ms = new Sprite(info.name);
this.ms.info = info; .// Set random y coordinates
this.ms.y = Math.random() * 200 - 400;
this.instance.scene.stage.addChild(this.ms);
this.run();
}
run() {
// Move the method
const move = () = > {
this.ms['y'] + =this.speed;
// Check for bullets
if (this.instance.bullets.length) {
for(let i = 0; i < this.instance.bullets.length; i++) {
// Perform collision detection
const isHit = bulletHit(this.ms, this.instance.bullets[i]);
if (isHit) {
// There is a collision, remove shells
this.hitCount += 1;
this.instance.bullets[i].remove();
this.instance.bullets.splice(i, 1);
i --;
// Determine if the maximum number of monster collisions is exceeded
if (this.hitCount >= this.ms.info.hit) {
// Remove monster, execute destroy function
ticker.remove(move);
this.destory();
return; }}}}// Make a simple animation of the hit count twice
if (this.hitCount) {
this.ms.alpha = 0.8;
this.ms.scale.set(SCALE);
}
// Off screen, remove
if (this.ms.y >= window.innerHeight) {
ticker.remove(move);
this.instance.scene.stage.removeChild(this.ms);
}
}
ticker.add(move);
}
destory() {
this.instance.state.reword += this.ms.info.reword;
this.instance.msg.update();
this.instance.scene.stage.removeChild(this.ms); }}export default Monster;
Copy the code
Collision function
Collision detection is an integral part of the game, many game engines have collision detection, but pixi does not have it, we need to develop our own. This game development only involves the collision of two objects, monster and shell. The origin of the shell is set as the center of the graph. We only need to judge whether the center point of the bullet is in the monster’s body during the monster’s movement.
export const bulletHit = (m, b) = > {
const m_left = m.x;
const m_right = m.x + m.width;
const m_top = m.y;
const m_bottom = m.y + m.height;
// As long as the bullet's center point coordinates are included in the monster's body coordinates, the two are considered to have collided
return m_left < b.x && m_right > b.x && m_top < b.y && m_bottom > b.y;
}
Copy the code
The scoreboard
The scoreboard is relatively simple, create a text, every time a monster is hit dead, the score increases, call the update method of text, display the latest score. The initialized scoreboard is mounted directly to the global app for ease of call.
class Text {
instance = Game();
constructor(options = {}) {
this.text = new Pixi.Text(` total score:The ${this.instance.state.reword}`, {
fontFamily: "Arial".fontSize: 48.fill: "white"});this.instance.msg = this;
this.instance.scene.stage.addChild(this.text);
}
update() {
this.text.text = ` total score:The ${this.instance.state.reword}`; }}Copy the code
complete
The game has been basically developed. Although the operation is stiff and there are still bugs in gun rotation, AS the first project of personal learning and development, I have taken the first step successfully. I learned a new technology stack from my work, improved my ability and expanded my knowledge reserve.
If you are interested, you can download the source code directly to GitLab (the code may be confused, there are some things I tested). Thank you!