第六季-常用编程框架和算法
01.MVC架构
是一种设计程序的思路/套路.
MVC的含义
-
M-Model模型(数据)
FlappyBird中小鸟的位置,当前激活的节点等等 -
V-View视觉层
展示出来的界面,通常是引擎进行处理 -
C-Controller控制器(逻辑)
写下的代码,小鸟碰到管子判断游戏结束
总结内涵:拿数据-根据逻辑-刷新界面
02.单例模式
单例(instance)的特点
- 单例类,全局只有一个对象,不可能产生多个
- 在代码任意位置容易获取这个对象
作用
防止一个全局使用的类频繁的实现与销毁
控制实例数目,节省系统资源
实现
/**单例类 */
export default class InstanceDemo {
/**全局唯一的对象 */
private static _instance: InstanceDemo = null; //private的变量名前一般加个_
/**获取单例对象 */
public static getInstance(): InstanceDemo{
if (InstanceDemo._instance == null){
InstanceDemo._instance = new InstanceDemo();
}
return InstanceDemo._instance;
}
/**防止创建第二个对象 */
constructor(){
if (InstanceDemo._instance != null) {
throw Error("Already has InstanceDemo._instance");
}
}
num: number = 0;
trace(){
this.num ++;
console.log("trace",this.num);
}
}
调用
import InstanceDemo from "./instanceDemo";
const {ccclass, property} = cc._decorator;
@ccclass
export default class HelloWorld extends cc.Component {
start () {
/**获取单例对象 */
let instance = InstanceDemo.getInstance();
/**调用单例方法 */
instance.trace();
//创建对象
new InstanceDemo();
}
// update (dt) {}
}
结果
补充
实现单例模式,将构造函数私有化就好了
/**私有化构造函数*/
private constructor(){}
03.观察者模式-订阅发布模式
流程
- A订阅了开始游戏事件
- B抛出(发布)了开始游戏事件
- A响应事件
特点:A不管什么时候出发,只负责触发时接受,B不知道谁注册了事件,只负责触发.
实现
将WebStorm改为ES6:File→Setting→Languages&Framework→JavaScript→选择ES6
还要将typescriptconfig.json里的“ES5”改为“ES6”
EventCenter.ts 事件控制中心
EventHandler 记录事件信息
/**观察者模式 */
export default class EventCenter {
//事件数据 存放事件名和注册该事件的注册信息们
private static events: Map<string,Array<EventHandler>> = new Map<string,Array<EventHandler>>();
/**注册事件
* eventName: string 事件名
* target: object 谁注册的事件 用于回调绑定
* callBack: Function 回调
*/
static registerEvent(eventName: string,target: object,callBack: Function): void {
if (eventName == undefined || target == undefined || callBack == undefined) {
throw Error("regsiter event error");
}
/**判断是否已经有该事件被注册过 */
if (EventCenter.events[eventName] == undefined){
EventCenter.events[eventName] = new Array<EventHandler>();
}
/**将此次注册事件的信息存入Map中 */
let handler = new EventHandler(target,callBack);
EventCenter.events[eventName].push(handler);
}
/**触发事件
* eventName: string 事件名
* param?: any 回调参数
*/
static postEvent(eventName: string,param?: any): void {
let handlers = EventCenter.events[eventName];
if (handlers == undefined){
return;
}
//遍历所有注册了该事件的eventHandler
for (let i = 0; i < handlers.length; i++){
let handler = handlers[i];
if (handler){
//调用事件回调
//使用try-catch防止回调有报错但没有信息
try {
//.call(绑定的this,参数) 调用方法
handler.function.call(handler.target,param);
}catch (e){
console.log(e.message);
console.log(e.stack.toString()); //输出堆栈信息
}
}
}
}
/**移除注册事件
* eventName: string 事件名
* target: object 谁注册的事件 用于回调绑定
* callBack: Function 注册事件回调
*/
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){
//有两种移除方法 "= undefined"性能要好 "splice"要省内存空间
handlers[i] = undefined;
// handlers.splice(i,1);
break;
}
}
}
}
}
/**注册信息类 */
class EventHandler {
/**记录谁注册了事件 */
target: object;
/**记录事件触发时调用的方法 */
function: Function;
constructor(target: object,func: Function){
this.target = target;
this.function = func;
}
}
Panel.ts 用来注册事件
import EventCenter from "./EventCenter";
/**用来注册事件 检验观察者模式 */
const {ccclass, property} = cc._decorator;
@ccclass
export default class Panel extends cc.Component {
@property(cc.Label)
label: cc.Label = null;
onl oad () {
EventCenter.registerEvent("gameStart",this,this.onGameStart);
//5s后移除事件注册
this.scheduleOnce(function() {
this.onDestroy();
}.bind(this),5)
}
/**注册事件回调 */
onGameStart(str: string){
console.log("event callBack");
this.label.string = str;
}
onDestroy(){
//移除gameStart事件
console.log("remove event gameStart");
EventCenter.removeEvent("gameStart",this,this.onGameStart);
}
}
触发事件
EventCenter.postEvent("gameStart","game is start!");
结果
点击按钮
输出
注意:target节点destory后,一定要记得注销其注册的事件。否则callBack会出错
04.工厂模式
特点和作用
操作的对象本身只实现功能方法,具体的操作由工厂实现
这样不会暴露对象及创建逻辑
实现
/**工厂模式 */
//c: {new ():T} 告诉ide这个T类型的c是可以被实例化的
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;
}
/**角色接口 */
interface IActor {
attack: Function;
die: Function;
life: number;
}
/**盗贼 */
export class Thief implements IActor {
life: number;
attack() {
console.log("thief attack");
}
die() {
console.log("thief is die");
}
}
/**战士 */
export class Warrior implements IActor {
life: number;
attack() {
console.log("warrior attack");
}
die() {
console.log("warrior is die");
}
}
调用
createAttack<Thief>(Thief,10);
createDie<Warrior>(Warrior);
05.代理模式
代理 (额外的控制器)
- 单一原则,不给一个类太多功能.
自己的功能留在类里面,将一些逻辑控制放到外面.
高内聚,低耦合. - 不方便访问一个类的时候,给个代理
例如:明星和经纪人的关系;
快递中发货人-快递-收货人;
cc.loader.load(…)内部非常复杂,但对于调用的人来说并不关 心内部逻辑.
06.递归寻路
题目
需要从①走到②,要怎么走(可以斜着走,褐色是不能走的)
步骤
- 把起点当作当前节点
- 重复以下步骤
a. 把当前节点加入openList,标记Open
b.查找可以走的下一步节点
c.把下一步可以走的节点排序
d.把下一步可以走的离终点最近的点,当做当前的寻路节点
e.把走过的节点标记为Close - 直到查找到目标节点
实战
先制作格子 NodeGrid.ts
import FindPath from "./FindPath";
/**寻路地图格子 */
const {ccclass, property} = cc._decorator;
/**格子 显示层 */
@ccclass
export default class NodeGrid extends cc.Component {
dataGrid: DataGrid = null;
findPathController: FindPath;
onl oad () {
this.node.on(cc.Node.EventType.TOUCH_END,this.onBtnGrid,this);
}
/**点击格子 确定起点终点 生成路线 */
onBtnGrid(){
this.findPathController.onTouch(this);
}
/**刷新格子颜色 */
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");
}
}
}
/**格子数据 数据层 */
export class DataGrid {
type: GrideType;
//坐标
x: number;
y: number;
/**是否为当前节点 */
inOpenList: boolean = false;
/**路径节点标记 */
inCloseList: boolean = false;
}
/**格子类型枚举 */
export enum GrideType {
Normal, //普通
Wall, //墙
Start, //起点,当前节点
End, //终点
Road, //路线
}
在场景下创建一个40x40的格子,并挂载NodeGrid.ts
制作地图
/**随机生成8x8地图 */
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的概率生成墙
grideType = GrideType.Wall;
}
//数据层
let grid: DataGrid = new DataGrid();
grid.x = x;
grid.y = y;
grid.type = grideType;
this.dataGrids[x][y] = grid;
//视图层
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;
}
}
}
寻路(完整代码 FindPath.ts)
import NodeGrid, { DataGrid, GrideType } from "./NodeGrid";
/**递归寻路 */
const {ccclass, property} = cc._decorator;
@ccclass
export default class NewClass extends cc.Component {
/**格子节点 */
@property(cc.Node)
nodeGridPrefab: cc.Node = null;
dataGrids: DataGrid[][] = [];
nodeGrids: NodeGrid[][] = [];
/**记录起点 */
startGrid: DataGrid = null;
/**记录终点 */
endGrid: DataGrid = null;
onl oad () {
this.generateMap();
}
/**随机生成8x8地图 */
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的概率生成墙
grideType = GrideType.Wall;
}
//数据层
let grid: DataGrid = new DataGrid();
grid.x = x;
grid.y = y;
grid.type = grideType;
this.dataGrids[x][y] = grid;
//视图层
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;
}
}
}
/**点击格子 */
onTouch(nodeGrid: NodeGrid){
if (!this.startGrid) { //设置起点
this.startGrid = nodeGrid.dataGrid;
this.startGrid.type = GrideType.Start;
nodeGrid.updateGridColor();
}else if (!this.endGrid) { //设置终点
this.endGrid = nodeGrid.dataGrid;
this.endGrid.type = GrideType.End;
nodeGrid.updateGridColor();
//寻路
this.startFindPath();
}
}
openPath: DataGrid[] = [];
/**寻路 */
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("无法走到终点");
}
}
find(base: DataGrid) {
this.openPath.push(base);
base.inOpenList = true;
if (base == this.endGrid){ //寻路结束
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;
}
/**获取当前节点周围可走的节点 */
getRoundGrid(grid: DataGrid): DataGrid[] {
let arr: DataGrid[] = [];
//周围的格子
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));
//会将数组里元素两两进行比较,自定义方法里返回-1就不交换位置 返回1交换位置
arr.sort(this.compareGrids.bind(this));
return arr;
}
/**将格子放到数组里 */
addToRoundIfNeed(arr: DataGrid[],roundGrid: DataGrid) {
//当前节点和路径节点都不计入
if (!roundGrid || roundGrid.type == GrideType.Wall || roundGrid.inCloseList || roundGrid.inOpenList){
return;
}
if (roundGrid) {
arr.push(roundGrid);
}
}
/**根据坐标获取格子数据 */
getGrid(x: number,y: number): DataGrid {
//边界判断
if (x < 0 || x >= 8 || y < 0 || y >=8){
return null;
}
return this.dataGrids[x][y];
}
/**格子比较和终点的距离
* 距离小的放在前面
*/
compareGrids(grid0: DataGrid,grid1: DataGrid): number{
let grid0Dis = this.getDistance(grid0);
let grid1Dis = this.getDistance(grid1);
if (grid0Dis > grid1Dis) {
return 1;
}else{
return -1;
}
}
/**获取节点到终点距离 */
getDistance(grid: DataGrid){
return Math.abs(grid.x - this.endGrid.x) + Math.abs(grid.y - this.endGrid.y);
}
/**点击重新开始*/
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 = [];
}
}
运行结果
不足
当前算法只是寻找下一步的最优解,并不是全局的最优解,有时并不是最优路径。
07.A星寻路
可以看下大神写的A星寻路算法原理
步骤
- 把起点当做当前节点
- 重复以下步骤
a.把当前节点加入openList,标记Open
b.查找可以走的下一步节点
c.把下一步可以走的节点的父节点,设置为当前节点
d.把下一步可以走的节点加入到openList,排序
e.把openList中的第一个节点,当做当前节点
f.把走过的节点标记为Close - 直到查找到目标节点
实现
DataGrid添加一个字段
/**父节点 用于A星寻路 */
fatherGrid: DataGrid = null;
FindPathAX.ts完整代码
/**A星寻路 */
import NodeGrid, { DataGrid, GrideType } from "./NodeGrid";
const {ccclass, property} = cc._decorator;
@ccclass
export default class FindPathAX extends cc.Component {
/**格子节点 */
@property(cc.Node)
nodeGridPrefab: cc.Node = null;
dataGrids: DataGrid[][] = [];
nodeGrids: NodeGrid[][] = [];
/**记录起点 */
startGrid: DataGrid = null;
/**记录终点 */
endGrid: DataGrid = null;
onl oad () {
this.generateMap();
}
/**随机生成8x8地图 */
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的概率生成墙
grideType = GrideType.Wall;
}
//数据层
let grid: DataGrid = new DataGrid();
grid.x = x;
grid.y = y;
grid.type = grideType;
this.dataGrids[x][y] = grid;
//视图层
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;
}
}
}
/**点击格子 */
onTouch(nodeGrid: NodeGrid){
if (!this.startGrid) { //设置起点
this.startGrid = nodeGrid.dataGrid;
this.startGrid.type = GrideType.Start;
nodeGrid.updateGridColor();
}else if (!this.endGrid) { //设置终点
this.endGrid = nodeGrid.dataGrid;
this.endGrid.type = GrideType.End;
nodeGrid.updateGridColor();
//寻路
this.startFindPathAStar();
}
}
/**待考虑的节点列表 */
openPath: DataGrid[] = [];
/**A星寻路 */
startFindPathAStar(){
this.openPath.push(this.startGrid);
this.startGrid.inOpenList = true;
while (this.openPath.length > 0) {
let current = this.openPath.shift(); //shift--取出数组中首个元素
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); //拼接数组
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("没有路径可走");
}
}
/**获取当前节点周围可走的节点 */
getRoundGrid(grid: DataGrid): DataGrid[] {
let arr: DataGrid[] = [];
//周围的格子
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));
//会将数组里元素两两进行比较,自定义方法里返回-1就不交换位置 返回1交换位置
arr.sort(this.compareGridsAStar.bind(this));
return arr;
}
/**将格子放到数组里 */
addToRoundIfNeed(arr: DataGrid[],roundGrid: DataGrid) {
//当前节点和路径节点都不计入
if (!roundGrid || roundGrid.type == GrideType.Wall || roundGrid.inCloseList || roundGrid.inOpenList){
return;
}
if (roundGrid) {
arr.push(roundGrid);
}
}
/**根据坐标获取格子数据 */
getGrid(x: number,y: number): DataGrid {
//边界判断
if (x < 0 || x >= 8 || y < 0 || y >=8){
return null;
}
return this.dataGrids[x][y];
}
/**格子排序 优化
* 距离小的放在前面
*/
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;
}
}
/**获取综合距离 优化
* grid 当前节点
* start 起始节点
* end 目标节点
*/
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;
}
/**点击重新开始*/
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.对象池模式
意义
用于解决当需要创建大量相同对象的时候,避免重复创建,节能.
流程图
直接创建对象
/**对象池模式 */
const {ccclass, property} = cc._decorator;
@ccclass
export default class PoolDemo extends cc.Component {
@property(cc.Node)
nodeIcon: cc.Node = null;
onl oad () {
}
shoot() {
let node = cc.instantiate(this.nodeIcon);
//创建完节点还要从父节点上移出,太费事了
node.runAction(cc.sequence(cc.moveBy(1,0,300),cc.removeSelf()));
node.parent = this.node;
}
update() {
this.shoot();
}
}
会不断创建节点,并将节点从父节点移出,耗费性能,内存.
使用对象池
/**对象池模式 */
const {ccclass, property} = cc._decorator;
@ccclass
export default class PoolDemo extends cc.Component {
@property(cc.Node)
nodeIcon: cc.Node = null;
/**对象池 */
pool: cc.Node[] = [];
onl oad () {
}
shoot() {
let node = this.getNode();
//创建完节点还要从父节点上移出,太费事了
node.runAction(cc.sequence(cc.moveBy(1,0,300),cc.removeSelf(),cc.callFunc(function () {
node.position = cc.Vec2.ZERO; //节点位置重置
//用完后将节点放回对象池
this.pool.push(node);
}.bind(this))));
node.parent = this.node;
}
/**获取节点
* 如果对象池里有节点的话就取出来用
* 没有的话就实例化一个
*/
getNode(): cc.Node {
if (this.pool.length > 0) {
return this.pool.shift();
}else {
console.log("创建了一个节点");
return cc.instantiate(this.nodeIcon);
}
}
update() {
this.shoot();
}
}
运行效果
只需创建了62个节点