The other
Some time ago, I don’t know where I got the courage to design a small game to save bubble fish, dreaming of the beautiful prospect of surging traffic. I started to design and realize several kinds of gameplay, and played with a lot of friends. I was full of expectations but was drowned by the ridicule. Silence for a period of time, one day suddenly found that wechat circle of friends has been appeared in the tower defense small game advertising, whim, or do a tower defense game try. This time I learned my lesson and found an off-the-shelf tower defense game called Field Runner on the Internet and decided to implement it all over again.
This article explains the experience of copy a tower defense game, which also contains some design ideas, hoping to help fans who want to develop small games. Some of the code and design ideas of the game are borrowed from other authors, and I point this out just to show that learning from others is a quick way to get started.
Game Reference:
instructions
- The code examples in this article are based on the Egret game engine, and I will try to minimize the reading impact of using the engine API. If you are still not clear, please visit the egret engine website.
- The code is in typescript and egret objects are provided by the game engine.
- The code in this article is just a sample, only the key code is retained, not guaranteed to work correctly.
design
I remember that I often played tower defense game with my roommates when I was in college. In this game, players build various defense towers to resist the attacks of the enemy in batches and protect the base. Every time I saw a large number of enemies attacking at the beginning, I would desperately build a defense tower, not to fill the screen to appease my uneasy heart.
Looking back at the game scene above, a group of enemy soldiers started from the left to attack the base. The player placed a large number of Green machine gun turrets almost all over the screen in every position of the map, and intercepted and hit to stop the enemy’s attack trend, during which the enemy constantly exploded and died. The top column of the game is gold, score, game rounds, player (base) health, the next column is pause, fast forward, weapon type.
Next we step by step design this single player game.
start
Every time I develop a small game, I like to add a background image to the game scene. It’s fun to refresh and see the image, and I can also adjust the UI display.
// Main.ts
class Main extends eui.UILayer {
// code omitted...
// Add a background to the game
// Map class is a custom Map class
protected createGameScene(): void {
this.addChild(new Map()); }}Copy the code
Map implementation ideas
Next, we’ll implement the Map class. Based on the game scenario above, I needed to mark several locations on the map, and the first thing that came to mind was the way the code would write dead coordinates (code example) :
// Map.ts
// Note: Write the following coordinates freely
class Map extends egret.DisplayObjectContainer {
// code omitted...
// Enemy start coordinates
public static startPoint:[number, number] = [1.11];
// Map height and width
public static tileWidth:number = 800;
public static tileHeight:number = 500;
// Map tile number, grid number row 20, column 10
public static mapWidth:number = 20;
public static mapHeight:number = 10;
// Weapon selection bar coordinates, currently define a weapon type
public static weaponPoinit:[number, number] = [100.111];
// Pause button
public static stopPoinit:[number, number] = [10.111];
// Display gold position
public static moneyPoinit:[number, number] = [10.10];
// Displays the fractional position
public static scorePoinit:[number, number] = [30.10];
// Displays player health
public static lifePoinit:[number, number] = [80.10];
// code omitted...
}
Copy the code
The meaning of these coordinates is clear, then add the corresponding icon to each coordinate. For example, I now add an icon for weapon selection (code example) :
// Weapon.ts
private onAddToStage() {
this.gatingdIcon = this.createBitmapByName("gatingdIcon_png");
this.gatingdIcon.x = Map.weaponPoinit[0];
this.gatingdIcon.y = Map.weaponPoinit[1];
this.gatingdIcon.width = 100;
this.gatingdIcon.height = 100;
// Add the icon to the parent layer
this.parent.addChild(this.gatingdIcon);
}
Copy the code
This kind of code to write dead coordinates is simple, but need to adjust the position, if later change the map, also have to readjust the calculation of each Sprite position, is not conducive to expansion. Later, I switched to Tiled Map, which is a 2D map editor. You can check the official address for details. In the TiLED editor, I can mark the coordinate information of each icon and name it. After loading the exported map file in the game, I can directly search for coordinates according to the name. I can also design a variety of complex terrain.
Limited by space, I will not share the use of the map editor, there are many tutorials online, it is very easy to use.
The exported map files need to be parsed by the developers themselves, but EGREt engine provides a third-party parsing library that I can use with a few modifications to the engine documentation. In the Map class, I provide the getMapObj method to get the markup I designed on the TiLED Map. This method takes two parameters, the first representing the name of the category to which the tag belongs and the second representing the tag name (code example) :
// Map.ts
class Map extends egret.DisplayObjectContainer {
public static tmxTileMap: tiled.TMXTilemap;
public static getMapObj(parentName:string, targetName: string) {
let toolMap:any = Map.tmxTileMap.getChildByName(parentName);
let childrens = toolMap._childrens || [];
let targetObj;
childrens.map(child= > {
if(child.$name == targetName) { targetObj = child; }});returntargetObj; }}Copy the code
Again using the weapon selection icon, in the Tiled map I created a parent layer (category) called ‘tool’ and then a layer called ‘gatingdIcon’ underneath this layer. Next add the weapon selection icon to the scene (code example) :
// Weapon.ts
private onAddToStage() {
const tagetMap = Map.getMapObj('tool'.'gatingdIcon');
if (tagetMap) {
this.gatingdIcon = this.createBitmapByName("gatingdIcon_png");
this.gatingdIcon.x = targetMap.$x;
this.gatingdIcon.y = targetMap.$y;
this.gatingdIcon.width = targetMap.$width;
this.gatingdIcon.height = targetMap.$height;
// Add the icon to the parent layer, this.parent I passed in externally
this.parent.addChild(this.gatingdIcon); }}Copy the code
Coordinate information of other ICONS is also obtained according to the above, and will not be repeated.
In the game, there are limits to where enemy soldiers can walk and where weapons can be placed, such as map boundaries and obstacles. Tiled exports tile maps, meaning that the map has two coordinates to represent the position of game elements, tile tile grid coordinates and pixel XY coordinates.
Design ideas for weapon placement areas
As mentioned before, the map is divided into small squares (tiles), and the place for placing the weapon should be one grid (because my weapon is as big as one grid). Then I only need to obtain the coordinate of the map grid where the weapon is moving to judge whether to place it or not.
Get the grid coordinates of the finger (or mouse) :
// x and y represent the coordinates of the current finger (or mouse)
private getAvailablePositionNearby(pointX:number, pointY:number) {
// tx ty stands for grid coordinates, so I'm rounding them off
const tx = Math.round(pointX - startPointX / tileWidth);
const tx = Math.round(pointY - startPointY / tileHeight);
// x, y represents the pixel coordinates of the actual grid (relative to the stage)
const x = startPointX + tx * tileWidth;
const y = startPointY + ty * tileHeight;
return {x: x, y: y, tx: tx, ty: ty};
}
Copy the code
After obtaining the coordinates of the currently placed weapon from above, assign the value directly to the currently dragged weapon (code example) :
// Place the weapon
private placeWeapon(weapon:Gatling) {
const point = this.getAvailablePositionNearby(pointX, pointY);
if (point) {
this.dragWeapon.x = point.x;
this.dragWeapon.y = point.y;
this.dragWeapon.tx = point.tx;
this.dragWeapon.ty = point.ty; }}Copy the code
However, as mentioned earlier, there are many places on the actual map where weapons are not allowed, such as map boundaries, obstacles, the grid already has weapons, no roads, etc. (code example) :
private allowBoolean(point) {
let bool = false;
if (point) {
if (
(point.tx==0 && point.ty==1) ||
point.tx < 0 ||
point.tx > mapWidth ||
point.ty < mapHeight ||
// This grid player has already placed weapons
this.player.getWeaponAt(point.tx, point.ty) ||
// This grid is not on the enemy's pathfinding path.
!this.player.buildPath(point.tx, point.ty)
) {
bool = false;
} else {
bool = true; }}return bool;
}
Copy the code
Design ideas for enemy soldier walking area
Start by thinking about the enemy’s route: the enemy will attack in waves from the beginning, take a detour to the weapon tower, stay within the boundaries of the map, and then disappear at the player’s base. So when editing the map, mark the enemy’s birthplace and the player’s base.
Get location in code (code example) :
private setPoint() {
const startMap = Map.getMapObj('soldierBirthPool'.'pointStart');
const endMap = Map.getMapObj('soldierBirthPool'.'pointEnd');
this.startPoint = [parseInt(startMap.$x, 10), parseInt(startMap.$y, 10)];
this.endPoint = [parseInt(endMap.$x, 10), parseInt(endMap.$y, 10)];
}
Copy the code
As I said, enemies move from point to point, and their route changes as they encounter weapon towers. This is usually done using pathfinding algorithms. To implement the path – finding algorithm, rely on a data structure – graph. The background map does not have a clear road plan, so the map can be divided into small squares, and enemies marching on one of the squares can only move in four directions: up, down, left and right. The graph is composed of vertices and edges, so each cell is regarded as a vertex. Between two adjacent cells, two directed edges are connected, and the weight of the edge is 1. Find the path closest to the shortest path, use Astar algorithm to achieve, and calculate the distance between the vertex heuristic function, here use Manhattan distance (the distance between two points and the horizontal and vertical coordinates), calculation is simple and time-consuming.
Manhattan distance calculation (sample code) :
private manhattan(start, end) {
return Math.abs(start.x - end.x) + Math.abs(start.y - end.y);
}
Copy the code
After understanding this aspect of knowledge, not to implement A* algorithm, such development costs do not pay. In the game, I introduced the existing Astar algorithm library. Based on the above, the heuristic function selected the Manhattan distance calculation, and then the construction diagram.
Since the map is actually divided into tiles, it can be handled simply. It should be noted that routes are designed for each enemy soldier in the game, and as the player adds weapon towers to the map and progresses, routes need to be updated in real time until the enemy is no longer on the map.
Generate a collection of routes (code example) :
// Generate a path coordinate setpublic buildPath(tx? , ty? , start? , end?) {/ / figure
let map = [];
// Current enemy start coordinates
let s = start;
// The current enemy endpoint
let e = end;
if(! s || s[0] < 0) {
s = this.startPoint;
}
if(! end) { e =this.endPoint;
}
// Walk through the map grid to generate a collection
for (let i = 0; i < this.mapHeight; i++) {
map[i] = [];
for (let j = 0; j < this.mapWidth; j++) {
let hasWeaponAt = this.getWeaponAt(j, i);
// Note: If you find a weapon defense tower on the grid, set it to 1, otherwise set it to 0. 1 indicates that the grid enemy cannot move
map[i][j] = hasWeaponAt ? 1 : 0; }}// If the specified area is passed outside, the value of this area is set to 1
if (tx || ty) {
map[ty][tx]= 1;
}
// The Astar algorithm is used to find the set of paths closest to the optimal solution
let pathArr = Astar.findPath(map, s, e) || [];
return pathArr;
}
Copy the code
By calling the buildPath method, the soldier’s path is set and the soldier’s pixel coordinates are constantly changed to move forward as the game’s frame rate changes. In the principle of graph generation, the enemy marching on a certain grid can only move up, down and left in four directions in the grid. In the process of enemy movement (coordinate change), when the enemy finishes the grid, it has to judge the next direction of walking (up, down, left and right), which is actually traversing the enemy path (tile) set, by finding the current grid position of walking, to find the next adjacent grid.
Find the next cell the enemy moves on, the direction (code example) :
/ / solider soldiers
private getNextDirection(solider:Soldier) {
for(let i = 0; i < solider.path.length - 1; i++) {
let path = solider.path[i];
// Find the current location of the soldier's cell, so that we can determine the next cell position I +1
if (path[0] == solider.tx && path[1] == solider.ty) {
// Next: the next cell to walk (tile)
let next = solider.path[i+1];
return [next[0]-solider.tx, next[1]-solider.ty]; }}return null;
}
Copy the code
Enemy soldiers get to travel route, is one by one grid coordinates collection, real soldiers can’t teleport from one panel to the next grid, soldiers are marching pace, in order to achieve a smooth marching effect, need to determine whether or not the enemy has been through the grid, and then according to the marching speed and marching enemy x, y coordinates azimuth Settings.
First define the marching direction representation of the soldier, when the marching direction changes, the marching animation direction of the soldier also changes (code example) :
// Soldier.ts
// direction[0] === 1 direction[0] === -1 direction[0] === -1 left direction
// direction[1] indicates that soldiers march left and right, direction[1] === 1 down, direction[1] === -1 up
// 0 means stop marching
public direction: [number, number] = [1.0];
Avatar represents the position, orientation, motion, and posture of the avatar
public avatar: egret.MovieClip;
// The marching speed of the soldiers
public speed:number = 0:
private setDirection() {
// code omitted...
if (this.direction[0] = =1) {
// Move to the right and play the corresponding animation. GotoAndPlay is egret's method for playing frames
this.avatar.gotoAndPlay("solider_walk_right".- 1);
}
// code omitted...
}
Copy the code
Set the xy coordinates of the soldier based on the obtained marching direction value multiplied by the marching speed and coordinate values (code example) :
// Player.ts
// Set the enemy soldier's XY coordinates according to the moving position
private moveByDirection(solider:Soldier) {
if(! solider.direction) {return;
}
if (solider.direction[0] != 0) {
solider.x += solider.speed * solider.direction[0];
} else
if (solider.direction[1] != 0) {
solider.y += solider.speed * solider.direction[1]; }}// The soldier moves
private moveSoldier(solider:Soldier) {
// direction[0] ! =0 indicates that the target is moving in the X-axis
if (solider.direction[0] != 0) {
// Check whether the grid is finished
let dx = target.x - ( this.startPoint[0] + tile[0] * this.tileWidth );
if (dx === 0) {
// Set the enemy soldier's XY coordinates according to the moving position
solider.setDirection(this.getNextDirection(target)); }}// code omitted...
}
Copy the code
The above direction[0] needs to be explained. Since the enemy’s marching cells are adjacent to each other (see the explanation of the data structure diagram above), the enemy will move in either left and right direction or up and down direction, and the xy axis will not move at the same time.
At this point, the enemy soldier walking area design implementation is done. The main points of this design are:
- Analyze the enemy’s walking strategy and use Astar pathfinding algorithm to get the marching route.
- The map is divided into grids to construct the data graph required by pathfinding algorithm.
- Design the enemy’s walking mode, marching direction to obtain the way by calculating the next walking grid tile coordinate minus the current walking grid tile coordinate, namely [next[0]-solider.tx, next[1]-solider.ty];
- The design of enemy smooth marching;
The realization idea of weapon defense tower
Once the map is designed, the next step is to add weapon defense towers to the map. In order to improve the fun of the game, the game designed a variety of weapons for players to choose, this article explains the design ideas of machine gun weapons. The machine gun is familiar to all, is a continuous automatic fire weapon, and is usually fixed on a chassis. As mentioned in the map design chapter, soldiers can walk in four directions in the game, so the chassis of the machine gun should also support 360 degree rotation, and the rotation Angle of the weapon changes with the soldier’s movement.
First, in the weapon bar at the bottom right corner of the map, add the icon (sample code) :
private onAddToStage() {
this.gatingdIcon = this.createBitmapByName("gatingdIcon_png");
const targetMap = Map.getMapObj('tool'.'gatingdIcon');
if (targetMap) {
this.gatingdIcon.x = targetMap.$x;
this.gatingdIcon.y = targetMap.$y;
this.gatingdIcon.width = targetMap.$width;
this.gatingdIcon.height = targetMap.$height;
}
this.parent.addChild(this.gatingdIcon);
}
Copy the code
Add a touch listening event (sample code) :
private onAddToStage() {
// omit code...
/ / move
this.stage.addEventListener(egret.TouchEvent.TOUCH_MOVE, this.touchMoveHandler, this);
// Start (press -down)
this.stage.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.touchBeginHandler, this);
// End (leave-up)
this.stage.addEventListener(egret.TouchEvent.TOUCH_END, this.touchEndHandler, this);
}
Copy the code
After selecting the icon, the effect of moving the weapon will be triggered. When released, the weapon will be added to the map. The placement of the weapon was explained in the previous section ‘Design Ideas for Weapon Placement Areas’.
Once the weapon is added, the next step is to design the weapon’s attributes.
damage
The simplest is to give the weapon a constant attack power:
public damage:number = 10;
Copy the code
Then, after the soldier is attacked, the health minus the damage:
// The soldier was shot
public getShot(damage:number) {
// After being shot, health is deducted
this.health -= damage;
}
Copy the code
In reality, the severity of injuries suffered by soldiers after being shot is not fixed, which can be simulated by designing weapon floating attack force in the game. Set a maximum damage, and then set a minimum damage, with random value:
private maxDamage: number = 20:
private minDamage: number = 10;
// Get a random attack value
public getDamange() {
return Math.round(Math.random()*(this.maxDamage - this.minDamage)) + this.minDamage;
}
Copy the code
Attack range
To give a weapon a fixed range of attack:
public attackRadius: nunmber = 200;
Copy the code
Then detect if any enemies are within range of attack:
To determine whether the enemy has entered the weapon’s attack range, calculate the distance between the weapon and the soldier in the plane rectangular coordinate system, and then compare the distance value with the attack range. The mathematical formula for distance calculation is as follows:
Implement the detection function (sample code) :
public isInAttackRadius(solider: Solider) {
// Calculate the distance between the weapon and the soldier.
const dx = solider.x - this.x;
const dy = solider.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= this.attackRadius;
}
Copy the code
The bullet hits the enemy’s judgment
As mentioned earlier, the chassis of the weapon in the game automatically rotates 360 degrees as the enemy moves. When the enemy is in range of attack, the weapon automatically rotates to point at the enemy and then fires.
The firing process is as follows:
There is also a time delay when hitting an enemy. To simulate this scenario, firing for a time greater than a millisecond indicates hitting an enemy. After firing, the time variable is reset to 0 and the rotation time of the weapon is reset.
Partial implementation (sample code) :
// Test whether hit, add a 300ms judgment, simulation
private checkShot() {
if (this.fireTime > 300) {
this.fireTime = 0;
this.turnTime = new Date().getTime();
return true;
}
return false;
}
//
public hit(soldier: Soldier) {
let status;
// Weapon points to the left by default (+180)
const angle = (180 / Math.PI) * Math.atan2(dy, dx) + 180;
// frames, assuming each frame is equal to 10 angles
const frame = Math.round(angle/10);
const hitBool = this.isInAttackRadius(soldier);
this.status = status;
this.currentAngleFrame = frame;
if (hitBool) {
if (this.status === 'idle') {
this.trunTime = new Date().getTime();
this.fireTime = 0;
} else {
this.fireTime = new Date().getTime() - this.trunTime;
}
return this.checkShot();
}
return false;
}
Copy the code
Note: Angle calculation through atan2 function, not ATan function, ATan function of (y/x), (-y/-x) is no way to distinguish, calculated Angle is not the actual value.
Weapon upgrades
In the game, the player can upgrade the weapon by spending a certain amount of game currency. The higher the upgrade, the more the game currency is spent, and the attributes of the upgraded weapon are also improved (sample code) :
// The general weapon level is limited
private canUpgrade() {
return this.level < 8;
}
public upgrade() {
if (!this.canUpgrade()) {
return false;
}
this.level ++;
this.cost += 20;
this.minDamage += 12;
// code omitted...
}
Copy the code
When a player clicks on a weapon upgrade button, the price of the current weapon upgrade is deducted from the game coin.
other
Each weapon is automatically tracking an enemy soldier, the code needs to make some simple changes, in the player class, need to make some simple judgments, automatically assign the weapon to the target:
public autoAttack() {
// omit code...
if (weapon.solider === null| |! weapon.checkInAttackRadius(solider) || (weapon.solider.x >= (this.stage.stageWidth + weapon.width))
) {
weapon.solider = this.findSolider(weapon); }}Copy the code
The enemy soldier’s realization idea
Soldiers have several main points: route, speed, health, health bar, death burst gold, and level up. In fact, the partial implementation of soldiers is no different from that of weapons. The main design difficulty of soldiers is marching, which has been explained in detail in the map section of this paper. So I won’t go into details about the soldier’s design.
Health bar changes after taking damage:
public getShot(damage:number) {
// After being shot, health is deducted
this.health -= damage;
// When deductible health is less than 0, health is reset to 0.
if (this.health < 0) {
this.health = 0;
}
const percent = this.health / this.maxHealth;
// Update health bar, rounded to 60 by default
let healthWidth = Math.round(60 * percent);
// this.healthBar soldier healthBar icon
if (this.healthBar) {
this.healthBar.width = healthWidth; }}Copy the code
Game points, game currency, rounds, end
Integral game currency
public autoAttack() {
if (solider.isDead()) {
/ / integral
this.score += solider.score;
/ / game currency
this.money += solider.money; }}Copy the code
rounds
The number of rounds is the number of rounds plus one for each round of soldiers.
The end of the
The end is when the player reaches zero health.
When the enemy attacks the base, the player deducts health:
public autoAttack() {
// omit code...
if (solider.x >= this.stage.stageWidth + solider.width) {
this.life-- ; }}Copy the code
Player design
All of the above design, are integrated in the player class, the actual article has explained a lot of examples, I leave it to you to design.
conclusion
The design of the game revolves around the player class, and the summary draws a picture, hoping to help understand the thinking:
@Author: White Clouds floating ([email protected])
Github: github.com/534591395 Welcome to follow my wechat official account:
The new dream of the rabbit