01.MVC architecture
It is an idea / routine of designing programs
Meaning of MVC
- M-Model (data)
The location of the bird in FlappyBird, the currently active node, and so on - V-View visual layer
The displayed interface is usually processed by the engine - C-Controller (logic)
Write down the code, the bird meets the tube to judge the end of the game
Summary connotation: take data - according to logic - refresh interface
02. Singleton mode
Characteristics of single instance
- A singleton class has only one object globally, and it is impossible to generate multiple objects
- This object is easily accessible anywhere in the code
effect
Prevent frequent implementation and destruction of a globally used class
Control the number of instances and save system resources
realization
/**Singleton class */ export default class InstanceDemo { /**Globally unique object */ private static _instance: InstanceDemo = null; //private variable names are usually preceded by_ /**Get singleton object */ public static getInstance(): InstanceDemo{ if (InstanceDemo._instance == null){ InstanceDemo._instance = new InstanceDemo(); } return InstanceDemo._instance; } /**Prevent the creation of a second object */ constructor(){ if (InstanceDemo._instance != null) { throw Error("Already has InstanceDemo._instance"); } } num: number = 0; trace(){ this.num ++; console.log("trace",this.num); } }
call
import InstanceDemo from "./instanceDemo"; const {ccclass, property} = cc._decorator; @ccclass export default class HelloWorld extends cc.Component { start () { /**Get singleton object */ let instance = InstanceDemo.getInstance(); /**Call singleton method */ instance.trace(); //create object new InstanceDemo(); } // update (dt) {} }
result
supplement
Implement the singleton pattern and privatize the constructor
/**Privatized constructor*/ private constructor(){}
03. Observer mode - subscription publishing mode
technological process
- A subscribed to the start game event
- B throws (releases) the start game event
- A response event
Features: A is only responsible for accepting the event when it is triggered no matter when it starts. B does not know who registered the event and is only responsible for triggering
realization
Change WebStorm to ES6:File → Setting → Languages & framework → JavaScript → select ES6
Also add typescriptconfig Change "ES5" in JSON to "ES6"
EventCenter.ts event control center
EventHandler records event information
/**Observer mode */ export default class EventCenter { //The event data stores the event name and the registration information of the event private static events: Map<string,Array<EventHandler>> = new Map<string,Array<EventHandler>>(); /**Registration event * eventName: string Event name * target: object Who registers the event for callback binding * callBack: Function Callback */ static registerEvent(eventName: string,target: object,callBack: Function): void { if (eventName == undefined || target == undefined || callBack == undefined) { throw Error("regsiter event error"); } /**Judge whether this event has been registered */ if (EventCenter.events[eventName] == undefined){ EventCenter.events[eventName] = new Array<EventHandler>(); } /**Store the information of this registration event in the Map */ let handler = new EventHandler(target,callBack); EventCenter.events[eventName].push(handler); } /**Trigger event * eventName: string Event name * param?: any Callback Arguments */ static postEvent(eventName: string,param?: any): void { let handlers = EventCenter.events[eventName]; if (handlers == undefined){ return; } //Traverse all eventhandlers registered with the event for (let i = 0; i < handlers.length; i++){ let handler = handlers[i]; if (handler){ //Call event callback //Use try catch to prevent the callback from reporting an error but no information try { //. call (bound this, parameter) calls the method handler.function.call(handler.target,param); }catch (e){ console.log(e.message); console.log(e.stack.toString()); //Output stack information } } } } /**Remove registration event * eventName: string Event name * target: object Who registers the event for callback binding * callBack: Function Register event callback */ static removeEvent(eventName: string,target: object,callBack: Function): void { if (eventName == undefined || target == undefined || callBack == undefined) { throw Error("destory event failed"); } let handlers = EventCenter.events[eventName]; if (handlers){ for (let i = 0; i < handlers.length; i++){ let handler = handlers[i]; if (handler && target == handler.target && callBack == handler.function){ //There are two removal methods "= undefined" has better performance and "split" needs to save memory space handlers[i] = undefined; // handlers.splice(i,1); break; } } } } } /**Registration information class */ class EventHandler { /**Record who registered the event */ target: object; /**The method called when the logging event is triggered */ function: Function; constructor(target: object,func: Function){ this.target = target; this.function = func; } }
Panel.ts is used to register events
import EventCenter from "./EventCenter"; /**The observer pattern used to register event validation */ const {ccclass, property} = cc._decorator; @ccclass export default class Panel extends cc.Component { @property(cc.Label) label: cc.Label = null; onLoad () { EventCenter.registerEvent("gameStart",this,this.onGameStart); //Remove event registration after 5s this.scheduleOnce(function() { this.onDestroy(); }.bind(this),5) } /**Register event callback */ onGameStart(str: string){ console.log("event callBack"); this.label.string = str; } onDestroy(){ //Remove gameStart event console.log("remove event gameStart"); EventCenter.removeEvent("gameStart",this,this.onGameStart); } }
Trigger event
EventCenter.postEvent("gameStart","game is start!");
result
Click the button
output
Note: after the target node is destroyed, remember to unregister the event. Otherwise, callBack will make an error
04. Factory mode
Characteristics and functions
The operation object itself only realizes the function method, and the specific operation is realized by the factory
This does not expose objects and creation logic
realization
/**Factory mode */ //c: {new(): t} tells ide that C of type T can be instantiated export function createAttack<T extends IActor>(c: {new (): T},life: number): T { let object = new c(); object.attack(); object.life = life; return object; } export function createDie<T extends IActor>(c: {new (): T}): T { let object = new c(); object.die(); return object; } /**Role interface */ interface IActor { attack: Function; die: Function; life: number; } /**Thieves */ export class Thief implements IActor { life: number; attack() { console.log("thief attack"); } die() { console.log("thief is die"); } } /**warrior */ export class Warrior implements IActor { life: number; attack() { console.log("warrior attack"); } die() { console.log("warrior is die"); } }
call
createAttack<Thief>(Thief,10); createDie<Warrior>(Warrior);
05. Agency mode
Agent (additional controller)
- Single principle, do not give a class too many functions
Its own functions are left in the class and some logical controls are put outside
High cohesion and low coupling - When it is inconvenient to access a class, give a proxy
For example: the relationship between stars and agents;
In express delivery, consignor - Express - consignee;
cc. loader. The inside of load (...) is very complex, but the caller doesn't care about the internal logic
06. Recursive pathfinding
subject
You need to walk from ① to ②, and how to walk (you can walk obliquely, but Brown can't walk)
step
- Take the starting point as the current node
- Repeat the following steps
a. Add the current node to the openList and mark it Open
b. Find the next node to go
c. Sort the nodes that can be taken in the next step
d. Take the point closest to the end point that can be taken in the next step as the current wayfinding node
e. Mark the nodes passed as Close - Until the target node is found
actual combat
First make the grid nodegrid ts
import FindPath from "./FindPath"; /**Wayfinding map grid */ const {ccclass, property} = cc._decorator; /**Grid display layer */ @ccclass export default class NodeGrid extends cc.Component { dataGrid: DataGrid = null; findPathController: FindPath; onLoad () { this.node.on(cc.Node.EventType.TOUCH_END,this.onBtnGrid,this); } /**Click the grid to determine the starting point and end point to generate the route */ onBtnGrid(){ this.findPathController.onTouch(this); } /**Refresh grid color */ updateGridColor(){ if (this.dataGrid.type == GrideType.Normal){ this.node.color = new cc.Color().fromHEX("#fffff9"); } else if (this.dataGrid.type == GrideType.Wall){ this.node.color = new cc.Color().fromHEX("#151513"); } else if (this.dataGrid.type == GrideType.Road){ this.node.color = new cc.Color().fromHEX("#41ff0b"); } else { this.node.color = new cc.Color().fromHEX("#fff42d"); } } } /**Grid data layer */ export class DataGrid { type: GrideType; //coordinate x: number; y: number; /**Is it the current node */ inOpenList: boolean = false; /**Path node tag */ inCloseList: boolean = false; } /**Lattice type enumeration */ export enum GrideType { Normal, //ordinary Wall, //wall Start, //Starting point, current node End, //End Road, //Route }
Create a 40x40 grid in the scene and mount nodegrid ts
Making maps
/**Generate 8x8 map randomly */ generateMap () { for (let x = 0;x < 8;x ++){ this.dataGrids[x] = []; this.nodeGrids[x] = []; for (let y = 0;y < 8;y ++){ let rand = Math.random(); let grideType: GrideType = GrideType.Normal; if (rand < 0.2) { //1 / 5 probability generated wall grideType = GrideType.Wall; } //Data layer let grid: DataGrid = new DataGrid(); grid.x = x; grid.y = y; grid.type = grideType; this.dataGrids[x][y] = grid; //View layer let gridNode: NodeGrid = cc.instantiate(this.nodeGridPrefab).getComponent(NodeGrid); gridNode.node.position = cc.v3(50 * (x - 4),50 * (y - 4),0); this.nodeGrids[x][y] = gridNode; gridNode.dataGrid = grid; gridNode.findPathController = this; gridNode.updateGridColor(); gridNode.node.parent = this.node; } } }
Pathfinding (full code FindPath.ts)
import NodeGrid, { DataGrid, GrideType } from "./NodeGrid"; /**Recursive pathfinding */ const {ccclass, property} = cc._decorator; @ccclass export default class NewClass extends cc.Component { /**Lattice node */ @property(cc.Node) nodeGridPrefab: cc.Node = null; dataGrids: DataGrid[][] = []; nodeGrids: NodeGrid[][] = []; /**Record starting point */ startGrid: DataGrid = null; /**End point record */ endGrid: DataGrid = null; onLoad () { this.generateMap(); } /**Generate 8x8 map randomly */ generateMap () { for (let x = 0;x < 8;x ++){ this.dataGrids[x] = []; this.nodeGrids[x] = []; for (let y = 0;y < 8;y ++){ let rand = Math.random(); let grideType: GrideType = GrideType.Normal; if (rand < 0.2) { //1 / 5 probability generated wall grideType = GrideType.Wall; } //Data layer let grid: DataGrid = new DataGrid(); grid.x = x; grid.y = y; grid.type = grideType; this.dataGrids[x][y] = grid; //View layer let gridNode: NodeGrid = cc.instantiate(this.nodeGridPrefab).getComponent(NodeGrid); gridNode.node.position = cc.v3(50 * (x - 4),50 * (y - 4),0); this.nodeGrids[x][y] = gridNode; gridNode.dataGrid = grid; gridNode.findPathController = this; gridNode.updateGridColor(); gridNode.node.parent = this.node; } } } /**Click grid */ onTouch(nodeGrid: NodeGrid){ if (!this.startGrid) { //Set start point this.startGrid = nodeGrid.dataGrid; this.startGrid.type = GrideType.Start; nodeGrid.updateGridColor(); }else if (!this.endGrid) { //Set end point this.endGrid = nodeGrid.dataGrid; this.endGrid.type = GrideType.End; nodeGrid.updateGridColor(); //Pathfinding this.startFindPath(); } } openPath: DataGrid[] = []; /**Pathfinding */ startFindPath(){ if (this.find(this.startGrid)) { for (let i = 0; i < this.openPath.length; i++) { let path = this.openPath[i]; path.type = GrideType.Road; this.nodeGrids[path.x][path.y].updateGridColor(); } }else { console.log("Unable to reach the end"); } } find(base: DataGrid) { this.openPath.push(base); base.inOpenList = true; if (base == this.endGrid){ //End of pathfinding return true; } let round = this.getRoundGrid(base); for (let i = 0;i < round.length;i ++) { let nextBaseGride = round[i]; if (this.find(nextBaseGride)) { return true; } } base.inCloseList = true; this.openPath.splice(this.openPath.length - 1,1); return false; } /**Gets the walkable nodes around the current node */ getRoundGrid(grid: DataGrid): DataGrid[] { let arr: DataGrid[] = []; //Surrounding grid this.addToRoundIfNeed(arr,this.getGrid(grid.x,grid.y + 1)); this.addToRoundIfNeed(arr,this.getGrid(grid.x - 1,grid.y)); this.addToRoundIfNeed(arr,this.getGrid(grid.x,grid.y - 1)); this.addToRoundIfNeed(arr,this.getGrid(grid.x + 1,grid.y)); this.addToRoundIfNeed(arr,this.getGrid(grid.x - 1,grid.y - 1)); this.addToRoundIfNeed(arr,this.getGrid(grid.x + 1,grid.y + 1)); this.addToRoundIfNeed(arr,this.getGrid(grid.x + 1,grid.y - 1)); this.addToRoundIfNeed(arr,this.getGrid(grid.x - 1,grid.y + 1)); //It will compare the elements in the array in pairs. If - 1 is returned in the user-defined method, the exchange position will not be returned, and 1 exchange position will be returned arr.sort(this.compareGrids.bind(this)); return arr; } /**Put the grid in the array */ addToRoundIfNeed(arr: DataGrid[],roundGrid: DataGrid) { //Current node and path node are not included if (!roundGrid || roundGrid.type == GrideType.Wall || roundGrid.inCloseList || roundGrid.inOpenList){ return; } if (roundGrid) { arr.push(roundGrid); } } /**Obtain grid data according to coordinates */ getGrid(x: number,y: number): DataGrid { //Boundary judgment if (x < 0 || x >= 8 || y < 0 || y >=8){ return null; } return this.dataGrids[x][y]; } /**Distance between grid comparison and end point * Put the one with a small distance in front */ compareGrids(grid0: DataGrid,grid1: DataGrid): number{ let grid0Dis = this.getDistance(grid0); let grid1Dis = this.getDistance(grid1); if (grid0Dis > grid1Dis) { return 1; }else{ return -1; } } /**Get the distance from the node to the end point */ getDistance(grid: DataGrid){ return Math.abs(grid.x - this.endGrid.x) + Math.abs(grid.y - this.endGrid.y); } /**Click restart*/ onBtnRestart(){ for (let x = 0;x < this.dataGrids.length;x ++){ for (let y = 0;y < this.dataGrids[x].length;y ++){ let dataGrid = this.dataGrids[x][y]; dataGrid.inOpenList = false; dataGrid.inCloseList = false; if (dataGrid.type != GrideType.Wall){ dataGrid.type = GrideType.Normal; } this.nodeGrids[x][y].updateGridColor(); } } this.startGrid = null; this.endGrid = null; this.openPath = []; } }
Operation results
Insufficient
The current algorithm only looks for the optimal solution of the next step, not the global optimal solution, sometimes not the optimal path.
07.A star pathfinding
You can read it written by the great God Principle of star A routing algorithm
step
- Take the starting point as the current node
- Repeat the following steps
a. Add the current node to the openList and mark it Open
b. Find the next node to go
c. Set the parent node of the next node as the current node
d. Add the nodes that can go next to openList and sort them
e. Take the first node in openList as the current node
f. Mark the nodes passed as Close - Until the target node is found
realization
Add a field to the DataGrid
/**The parent node is used for star A routing */ fatherGrid: DataGrid = null;
FindPathAX.ts complete code
/**A Star pathfinding */ import NodeGrid, { DataGrid, GrideType } from "./NodeGrid"; const {ccclass, property} = cc._decorator; @ccclass export default class FindPathAX extends cc.Component { /**Lattice node */ @property(cc.Node) nodeGridPrefab: cc.Node = null; dataGrids: DataGrid[][] = []; nodeGrids: NodeGrid[][] = []; /**Record starting point */ startGrid: DataGrid = null; /**Record end point */ endGrid: DataGrid = null; onLoad () { this.generateMap(); } /**Generate 8x8 map randomly */ generateMap () { for (let x = 0;x < 8;x ++){ this.dataGrids[x] = []; this.nodeGrids[x] = []; for (let y = 0;y < 8;y ++){ let rand = Math.random(); let grideType: GrideType = GrideType.Normal; if (rand < 0.2) { //1 / 5 probability generated wall grideType = GrideType.Wall; } //Data layer let grid: DataGrid = new DataGrid(); grid.x = x; grid.y = y; grid.type = grideType; this.dataGrids[x][y] = grid; //View layer let gridNode: NodeGrid = cc.instantiate(this.nodeGridPrefab).getComponent(NodeGrid); gridNode.node.position = cc.v3(50 * (x - 4),50 * (y - 4),0); this.nodeGrids[x][y] = gridNode; gridNode.dataGrid = grid; gridNode.findPathController = this; gridNode.updateGridColor(); gridNode.node.parent = this.node; } } } /**Click grid */ onTouch(nodeGrid: NodeGrid){ if (!this.startGrid) { //Set start point this.startGrid = nodeGrid.dataGrid; this.startGrid.type = GrideType.Start; nodeGrid.updateGridColor(); }else if (!this.endGrid) { //Set end point this.endGrid = nodeGrid.dataGrid; this.endGrid.type = GrideType.End; nodeGrid.updateGridColor(); //Pathfinding this.startFindPathAStar(); } } /**List of nodes to be considered */ openPath: DataGrid[] = []; /**A Star pathfinding */ startFindPathAStar(){ this.openPath.push(this.startGrid); this.startGrid.inOpenList = true; while (this.openPath.length > 0) { let current = this.openPath.shift(); //shift -- take out the first element in the array if (current == this.endGrid) { break; } let round = this.getRoundGrid(current); for (let i = 0;i < round.length;i ++) { let r = round[i]; r.fatherGrid = current; r.inOpenList = true; } this.openPath = this.openPath.concat(round); //Splice array this.openPath.sort(this.compareGridsAStar.bind(this)); current.inCloseList = true; } if (this.endGrid.fatherGrid) { let pathGrid = this.endGrid; while (pathGrid) { pathGrid.type == GrideType.Road; this.nodeGrids[pathGrid.x][pathGrid.y].updateGridColor(); pathGrid = pathGrid.fatherGrid; } }else { console.log("There is no path to go"); } } /**Gets the walkable nodes around the current node */ getRoundGrid(grid: DataGrid): DataGrid[] { let arr: DataGrid[] = []; //Surrounding grid this.addToRoundIfNeed(arr,this.getGrid(grid.x,grid.y + 1)); this.addToRoundIfNeed(arr,this.getGrid(grid.x - 1,grid.y)); this.addToRoundIfNeed(arr,this.getGrid(grid.x,grid.y - 1)); this.addToRoundIfNeed(arr,this.getGrid(grid.x + 1,grid.y)); this.addToRoundIfNeed(arr,this.getGrid(grid.x - 1,grid.y - 1)); this.addToRoundIfNeed(arr,this.getGrid(grid.x + 1,grid.y + 1)); this.addToRoundIfNeed(arr,this.getGrid(grid.x + 1,grid.y - 1)); this.addToRoundIfNeed(arr,this.getGrid(grid.x - 1,grid.y + 1)); //It will compare the elements in the array in pairs. If - 1 is returned in the user-defined method, the exchange position will not be returned, and 1 exchange position will be returned arr.sort(this.compareGridsAStar.bind(this)); return arr; } /**Put the grid in the array */ addToRoundIfNeed(arr: DataGrid[],roundGrid: DataGrid) { //Current node and path node are not included if (!roundGrid || roundGrid.type == GrideType.Wall || roundGrid.inCloseList || roundGrid.inOpenList){ return; } if (roundGrid) { arr.push(roundGrid); } } /**Obtain grid data according to coordinates */ getGrid(x: number,y: number): DataGrid { //Boundary judgment if (x < 0 || x >= 8 || y < 0 || y >=8){ return null; } return this.dataGrids[x][y]; } /**Lattice sorting optimization * Put the one with a small distance in front */ compareGridsAStar(grid0: DataGrid,grid1: DataGrid): number{ let grid0Dis = this.getDistanceAStar(grid0,this.startGrid,this.endGrid); let grid1Dis = this.getDistanceAStar(grid1,this.startGrid,this.endGrid); if (grid0Dis > grid1Dis) { return 1; }else{ return -1; } } /**Obtain comprehensive distance optimization * grid Current node * start Starting node * end Target node */ getDistanceAStar(grid: DataGrid,start: DataGrid,end: DataGrid) { let endDis = Math.abs(grid.x - end.x) + Math.abs(grid.y - end.y); let startDis = Math.abs(grid.x - start.x) + Math.abs(grid.y - start.y); return endDis + startDis; } /**Click restart*/ onBtnRestart(){ for (let x = 0;x < this.dataGrids.length;x ++){ for (let y = 0;y < this.dataGrids[x].length;y ++){ let dataGrid = this.dataGrids[x][y]; dataGrid.inOpenList = false; dataGrid.inCloseList = false; if (dataGrid.type != GrideType.Wall){ dataGrid.type = GrideType.Normal; } this.nodeGrids[x][y].updateGridColor(); } } this.startGrid = null; this.endGrid = null; this.openPath = []; } }
08. Object pool mode
significance
It is used to avoid repeated creation and save energy when a large number of identical objects need to be created
flow chart
Create objects directly
/**Object pool mode */ const {ccclass, property} = cc._decorator; @ccclass export default class PoolDemo extends cc.Component { @property(cc.Node) nodeIcon: cc.Node = null; onLoad () { } shoot() { let node = cc.instantiate(this.nodeIcon); //It's too cumbersome to move out of the parent node after creating the node node.runAction(cc.sequence(cc.moveBy(1,0,300),cc.removeSelf())); node.parent = this.node; } update() { this.shoot(); } }
It will constantly create nodes and move them out of the parent node, consuming performance and memory
Use object pool
/**Object pool mode */ const {ccclass, property} = cc._decorator; @ccclass export default class PoolDemo extends cc.Component { @property(cc.Node) nodeIcon: cc.Node = null; /**Object pool */ pool: cc.Node[] = []; onLoad () { } shoot() { let node = this.getNode(); //It's too cumbersome to move out of the parent node after creating the node node.runAction(cc.sequence(cc.moveBy(1,0,300),cc.removeSelf(),cc.callFunc(function () { node.position = cc.Vec2.ZERO; //Node position reset //Put the node back into the object pool after use this.pool.push(node); }.bind(this)))); node.parent = this.node; } /**Get node * If there are nodes in the object pool, take them out for use * If not, instantiate one */ getNode(): cc.Node { if (this.pool.length > 0) { return this.pool.shift(); }else { console.log("Created a node"); return cc.instantiate(this.nodeIcon); } } update() { this.shoot(); } }
Operation effect
Only 62 nodes need to be created