• 《游戏脚本的设计与开发》-(战棋部分)第九章 战场上的寻路和移动


    上次已经让我军,友军和敌军都出现在了战场上,本章来说说如何让一个部队在战场上进行移动。在战棋游戏中,我军回合行动的时候,点击我军的某一个部队,会出现选择列表,选择【部队移动】一项后,会出现该部队可能移动的范围,然后点击范围内的某一位置,则部队就会向着这个位置移动。在这一过程中涉及到两个算法,一个是部队移动范围的搜索,另一个就是部队移动时的寻路算法。复杂指数来说,寻路算法相对复杂一些,之前研究AS3的时候,曾经写过一篇A*寻路的分析文章《A*寻路算法与它的速度》,有兴趣的朋友可以看一下。

    javascript中的A*算法

    其实A*寻路,主要应用在RPG或即时战略等游戏中,用于快速寻找最短路径,战棋游戏中在这方面要求并不高,所以广度优先搜索和深度优先搜索等算法都是无所谓的,不过由于我已经有了之前对AS3版本的A*算法的研究,就直接移植过来了。

    原理,我就不多说了,想了解的可以直接看我的《A*寻路算法与它的速度》一文,下面我直接贴出完整代码,有需要的朋友可以直接拿去用。

    function LStarQuery(){
    	var self = this;
    	self._map = [];//地图
    	self._w = 0;//地图的宽
    	self._h = 0;//地图的高
    	self._open = [];//开放列表
    	self._starPoint = null;//起点
    	self._endPoint = null;//目标点
    	self._path = [];//计算出的路径
    	self.queryType = 0;//寻路方式[0:八方向,1:上下四方向,2:斜角四方向]
    }
    LStarQuery.prototype = {
    	drawPath:function(node){
    		var self = this;
    		var pathNode = node;
    		//倒过来得到路径
    		while (pathNode != self._starPoint) {
    			self._path.unshift(pathNode);
    			pathNode = pathNode.nodeparent;
    		}
    	},
    	setStart:function(){
    		var self = this;
    		for (var y=0; y<self._h; y++) {
    			for (var x=0; x<self._w; x++) {
    				self._map[y][x].init();
    			}
    		}
    		self._open = [];
    	},
    	/*计算每个节点*/
    	count:function(neighboringNode,centerNode,eight){
    		var self = this;
    		//是否已经检测过
    		if (neighboringNode.isChecked)return;
    		var g = eight ? centerNode.value_g + 14:centerNode.value_g + 10;
    		//不在关闭列表里才开始判断
    		if (neighboringNode.open) {
    			//如果该节点已经在开放列表里
    			if (neighboringNode.value_g >= g) {
    				//如果新G值小于或者等于旧值,则表明该路更优,更新其值
    				neighboringNode.value_g = g;
    				self.ghf(neighboringNode);
    				neighboringNode.nodeparent = centerNode;
    				self.setOpen(neighboringNode);
    			}
    		} else {
    			//如果该节点未在开放列表里
    			//计算GHF值
    			neighboringNode.value_g = g;
    			self.ghf(neighboringNode);
    			neighboringNode.nodeparent = centerNode;
    			//添加至列表
    			self.setOpen(neighboringNode,true);
    		}
    	},
    	/*计算ghf各值*/
    	ghf:function(node){
    		var self = this;
    		var dx = Math.abs(node.x - self._endPoint.x);
    		var dy = Math.abs(node.y - self._endPoint.y);
    		node.value_h = 10*(dx+dy);
    		node.value_f = node.value_g + node.value_h;
    	},
    	/*加入开放列表*/
    	setOpen:function(newNode,newFlg){
    		var self = this;
    		var new_index;
    		if (newFlg) {
    			newNode.open = true;
    			var new_f = newNode.value_f;
    			self._open.push(newNode);
    			new_index = self._open.length - 1;
    		} else {
    			new_index = newNode.index;
    		}
    		while (true) {
    			//找到父节点
    			var f_note_index = new_index * 0.5 >>> 0;
    			if (f_note_index <= 0) break;
    			//如果父节点的F值较大,则与父节点交换
    			if (self._open[new_index].value_f >= self._open[f_note_index].value_f) break;
    			var obj_note = self._open[f_note_index];
    			self._open[f_note_index] = self._open[new_index];
    			self._open[new_index] = obj_note;
    			self._open[f_note_index].index = f_note_index;
    			self._open[new_index].index = new_index;
    			new_index = f_note_index;
    		}
    	},
    	/*取开放列表里的最小值*/
    	getOpen:function(){
    		var self = this;
    		var change_note;
    		//将第一个节点,即F值最小的节点取出,最后返回
    		var obj_note = self._open[1];
    		self._open[1] = self._open[self._open.length - 1];
    		self._open[1].index = 1;
    		self._open.pop();
    		var this_index = 1;
    		while (true) {
    			var left_index = this_index * 2;
    			var right_index = this_index * 2 + 1;
    			if (left_index >= self._open.length) break;
    			if (left_index == self._open.length - 1) {
    				//当二叉树只存在左节点时,比较左节点和父节点的F值,若父节点较大,则交换
    				if (self._open[this_index].value_f <= self._open[left_index].value_f) break;
    				change_note = self._open[left_index];
    				self._open[left_index] = self._open[this_index];
    				self._open[this_index] = change_note;
    				self._open[left_index].index = left_index;
    				self._open[this_index].index = this_index;
    				this_index = left_index;
    			} else if (right_index < self._open.length) {
    				//找到左节点和右节点中的较小者
    				if (self._open[left_index].value_f <= self._open[right_index].value_f) {
    					//比较左节点和父节点的F值,若父节点较大,则交换
    					if (self._open[this_index].value_f <= self._open[left_index].value_f) break;
    					change_note = self._open[left_index];
    					self._open[left_index] = self._open[this_index];
    					self._open[this_index] = change_note;
    					self._open[left_index].index = left_index;
    					self._open[this_index].index = this_index;
    					this_index = left_index;
    				} else {
    					//比较右节点和父节点的F值,若父节点较大,则交换
    					if (self._open[this_index].value_f <= self._open[right_index].value_f) break;
    					change_note = self._open[right_index];
    					self._open[right_index] = self._open[this_index];
    					self._open[this_index] = change_note;
    					self._open[right_index].index = right_index;
    					self._open[this_index].index = this_index;
    					this_index = right_index;
    				}
    			}
    		}
    		return obj_note;
    	},
    	/*开始寻路*/
    	queryPath:function (star,end){
    		var self = this;
    		self._path = [];
    		if(end.x >= self._map[0].length)end.x = self._map[0].length - 2;
    		if(end.y >= self._map.length)end.y = self._map.length - 2;
    		if (star.x == end.x && star.y == end.y) return self._path;
    		self.setStart();
    		self._starPoint = self._map[star.y][star.x];
    		self._endPoint = self._map[end.y][end.x];
    		self._open = [];
    		self._open.push(null);
    		var isOver = false;
    		var thisPoint = self._starPoint;
    		var firstCheck = true;
    		while (!isOver) {
    			thisPoint.isChecked = true;
    			var checkList = [];
    			if(self.queryType == 0 || self.queryType == 2){
    				if (thisPoint.x > 0 && thisPoint.y > 0) {
    					checkList.push(self._map[(thisPoint.y-1)][thisPoint.x - 1]);
    				}
    				if (thisPoint.x < self._w - 1 && thisPoint.y < self._h - 1) {
    					checkList.push(self._map[thisPoint.y + 1][(thisPoint.x+1)]);
    				}
    				if (thisPoint.x > 0 && thisPoint.y < self._h - 1) {
    					checkList.push(self._map[(thisPoint.y+1)][thisPoint.x - 1]);
    				}
    				if (thisPoint.x < self._w - 1 && thisPoint.y > 0) {
    					checkList.push(self._map[(thisPoint.y-1)][thisPoint.x + 1]);
    				}
    			}
    			if(self.queryType == 0 || self.queryType == 1){
                                    if (thisPoint.y > 0) {
                                            checkList.push(self._map[(thisPoint.y-1)][thisPoint.x]);
                                    }
                                    if (thisPoint.x > 0) {
                                            checkList.push(self._map[thisPoint.y][(thisPoint.x-1)]);
                                    }
                                    if (thisPoint.x < self._w - 1) {
                                            checkList.push(self._map[thisPoint.y][(thisPoint.x+1)]);
                                    }
                                    if (thisPoint.y < self._h - 1) {
                                            checkList.push(self._map[(thisPoint.y+1)][thisPoint.x]);
                                    }
    			}
    			//检测开始
    			var startIndex = checkList.length;
    			for (var i = 0; i<startIndex; i++) {
    				//周围的每一个节点
    				var checkPoint = checkList[i];
    				if (self.isWay(checkPoint,thisPoint)) {
    					//如果坐标可以通过,则首先检查是不是目标点
    					if (checkPoint == self._endPoint) {
    						//如果搜索到目标点,则结束搜索
    						checkPoint.nodeparent = thisPoint;
    						isOver = true;
    						break;
    					}
    					self.count(checkPoint, thisPoint);
    				} 
    			}
    			if (! isOver) {
    				//如果未到达指定地点则取出f值最小的点作为循环点
    				if (self._open.length > 1) {
    					thisPoint = self.getOpen();
    				} else {
    					//开发列表为空,寻路失败
    					return [];
    				}
    			}
    		}
    		//路径做成
    		self.drawPath(self._endPoint);
    		return self._path;
    	
    	},
    	/*判断是否可通过*/
    	isWay:function(checkPoint,thisPoint){
    		var self = this;
    		if(self.queryType == 0){
    			if (self._map[checkPoint.y][thisPoint.x].value == 0 && self._map[thisPoint.y][checkPoint.x].value == 0 && self._map[checkPoint.y][checkPoint.x].value == 0) return true;
    		}else if(self.queryType == 1){
    			if (self._map[checkPoint.y][checkPoint.x].value == 0) return true;
    		}else if(self.queryType == 2){
    			if (self._map[checkPoint.y][checkPoint.x].value == 0) return true;
    		}
    		return false;
    	}
    };
    function LNode(_x,_y,_v){
    	var self = this;
    	self.x = _x;
    	self.y = _y;
    	self.value = _v?_v:0;
    	self.isChecked = false;
    	self.value_g = 0;
    	self.value_h = 0;
    	self.value_f = 0;
    	self.nodeparent = null;
    	self.index = 0;
    	self.open = false;
    
    }
    LNode.prototype = {
    	init:function(){
    		var self = this;
    		self.open = false;
    		self.isChecked = false;
    		self.value_g = 0;
    		self.value_h = 0;
    		self.value_f = 0;
    		self.nodeparent = null;
    		self.index = -1;
    	}
    };

    这个寻路类通过设置queryType属性的值,可以实现【上下四方向】,【斜角四方向】和【八方向】三种寻路,下面我主要说说,这个类的用法。

    LNode类是地图中的坐标节点,使用LStarQuery类来搜索路径的话,必须先给它指定地图,就是LStarQuery类中的_map属性,这个_map是一个地图数组,它内部的地图节点是由LNode或者LNode的子类来组成的。

    比如说我随机生成了一个地图,0表示可通过,1表示障碍物。

    	var map = [];
    	//随机地图
    	for(var i=0;i<40;i++){
    		var childData = [];
    		for(var j=0;j<80;j++){
    			childData.push(Math.random() > 0.2 ? 0:1);
    		}
    		map.push(childData);
    	}

    这个地图显然跟LNode没有任何关系,自然也就无法直接在LStarQuery中使用,需要进行下面的变换。

    	//初始化寻路类
    	query = new LStarQuery();
            query._map = [];
            query._w = map[0].length;
            query._h = map.length;
    	//初始化寻路类的地图
            for (var y=0; y<query._h; y++) {
    		query._map.push([]);
    	        for (var x=0; x<query._w; x++) {
    			query._map[y].push(new LNode(x,y,map[y][x]));
    	        }
    	}

    经过上面的变换,就可以直接使用LStarQuery类的queryPath(star,end)来搜索路径了。参数star和end是两个坐标点,你可以使用lufylegend.js引擎中的LPoint类,也可以直接使用Object对象,只要带有x,y属性就可以了,其中star是起始点,end是目标点。
    比如

    var returnList = query.queryPath(new LPoint(2,3),new LPoint(20,16));

    就是搜索,坐标(2,3)到坐标(20,16)之间的最短路径。

    我写了一个测试的小demo,连接如下

    http://lufylegend.com/demo/test/lsharp/10/game/astar.html
    想看效果的朋友可以点上面的连接自己试验一下,障碍物是随机生成的,如果一开始刚好没有可走的路径的话,刷新一下就好了。

    效果如下



    地图移动和选择列表的添加

    所谓选择列表,就是当点击战场人物的时候,出现的移动,情报,待命等选择指令的列表,首先必须要有点击事件,在这里为了管理方便,我新建一个LSouSouSMapClick类来控制点击事件,首先添加和移除点击事件如下。

    function LSouSouSMapClick(){
    	var self = this;
    	base(self,LSprite,[]);
    }
    LSouSouSMapClick.prototype.setClickEvent = function(){
    	var self = this;
    	LSouSouObject.sMap.addEventListener(LMouseEvent.MOUSE_UP,self.onUp); 
    	LSouSouObject.sMap.addEventListener(LMouseEvent.MOUSE_DOWN,self.onDown); 
    };
    LSouSouSMapClick.prototype.removeClickEvent = function(){
    	var self = this;
    	LSouSouObject.sMap.removeEventListener(LMouseEvent.MOUSE_UP,self.onUp); 
    	LSouSouObject.sMap.removeEventListener(LMouseEvent.MOUSE_DOWN,self.onDown); 
    };


    所以,当点击战场的时候,会调用下面的onUp函数和onDown函数

    LSouSouSMapClick.prototype.onDown = function(e){
    	if(LSouSouObject.sMap.menu == null)LSouSouObject.sMap.mouseIsDown = true;
    	LSouSouObject.sMap.mapIsMove = false;
    };
    LSouSouSMapClick.prototype.onUp = function(e){
    	LSouSouObject.sMap.mouseIsDown = false;
    	var mx = e.selfX;
    	var my = e.selfY;
    	var self = LSouSouObject.sMap.smapClick;
    	if(LSouSouObject.sMap.roadList != null){
    		self.clickRoad(mx,my);
    	}else if(LSouSouObject.sMap.menu != null){
    		LSouSouObject.sMap.sMenu.onClick(LSouSouObject.sMap.menu,mouseX,mouseY);
    	}else{
    		var isClick = false;
    		//是否点击我军
    		isClick = self.checkCharacter(LSouSouObject.sMap.ourlist,mx,my);
    		if(isClick)return;
    		//是否点击友军
    		isClick = self.checkCharacter(LSouSouObject.sMap.friendlist,mx,my);
    		if(isClick)return;
    		//是否点击敌军
    		isClick = self.checkCharacter(LSouSouObject.sMap.enemylist,mx,my);
    		if(isClick)return;
                    //点击战场,显示地形 暂略...
    	}
    };

    先来解释一下LSouSouObject.sMap.mouseIsDown这个变量的用处,它主要来控制地图的显示范围的移动,在Flash版的《三国记》中,点击地图的边缘部分,地图会向相应的方向移动,这里使用同样的方法。

    具体的做法是,在LSouSouSMap的构造器中,添加时间轴

    self.addEventListener(LEvent.ENTER_FRAME,self.onframe);
    LSouSouSMap.prototype.onframe = function(self){
    	if(self.mouseIsDown){
    		self.mapMoveCheck();
    	}
    	self.mapMove();
    };

    由mapMove函数和mapMoveCheck函数来实现地图的移动。

    LSouSouSMap.prototype.mapMove = function(){
    	var self = this;
    	if(self.mapMove["left"]){
    		self.mapIsMove = true;
    		self.backLayer.x += self.nodeLength/4;
    		self.mapToCoordinate.x = self.backLayer.x;
    		if(self.backLayer.x % self.nodeLength == 0){
    			self.mapMove["left"] = false;
    		}
    	}else if(self.mapMove["right"]){
    		self.mapIsMove = true;
    		self.backLayer.x -= self.nodeLength/4;
    		self.mapToCoordinate.x = self.backLayer.x;
    		if(self.backLayer.x % self.nodeLength == 0){
    			self.mapMove["right"] = false;
    		}
    	}
    	if(self.mapMove["up"]){
    		self.mapIsMove = true;
    		self.backLayer.y += self.nodeLength/4;
    		self.mapToCoordinate.y = self.backLayer.y;
    		if(self.backLayer.y % self.nodeLength == 0){
    			self.mapMove["up"] = false;
    		}
    	}else if(self.mapMove["down"]){
    		self.mapIsMove = true;
    		self.backLayer.y -= self.nodeLength/4;
    		self.mapToCoordinate.y = self.backLayer.y;
    		if(self.backLayer.y % self.nodeLength == 0){
    			self.mapMove["down"] = false;
    		}
    	}
    };
    LSouSouSMap.prototype.mapMoveCheck = function(){
    	var self = this;
    	if(mouseX < self.nodeLength && self.backLayer.x < 0 && !self.mapMove["left"]){
    		self.mapMove["left"] = true;
    	}
    	if(mouseY < self.nodeLength && self.backLayer.y < 0 && !self.mapMove["up"]){
    		self.mapMove["up"] = true;
    	}
    	if(mouseX > self.SCREEN_WIDTH - self.nodeLength && mouseX < self.SCREEN_WIDTH 
    		&& self.backLayer.x > self.SCREEN_WIDTH - self.mapW && !self.mapMove["right"]){
    		self.mapMove["right"] = true;
    	}
    	if(mouseY > self.SCREEN_HEIGHT - self.nodeLength && mouseY < self.SCREEN_HEIGHT 
    		&& self.backLayer.y > self.SCREEN_HEIGHT - self.mapH && !self.mapMove["down"]){
    		self.mapMove["down"] = true;
    	}
    };


    其实原理也简单,就是当鼠标按下的时候,判断一下,鼠标是否点击在游戏画面的边缘部分,是的话,则通过设置self.backLayer的坐标让地图向相应的方向移动。

    以上是地图移动部分,下面看看具体如何来添加一个选择列表。

    看一下LSouSouSMapClick.prototype.onUp函数中的判断,得知一开始的所进行的判断是下面的部分

    		var isClick = false;
    		//是否点击我军
    		isClick = self.checkCharacter(LSouSouObject.sMap.ourlist,mx,my);
    		if(isClick)return;
    		//是否点击友军
    		isClick = self.checkCharacter(LSouSouObject.sMap.friendlist,mx,my);
    		if(isClick)return;
    		//是否点击敌军
    		isClick = self.checkCharacter(LSouSouObject.sMap.enemylist,mx,my);
    		if(isClick)return;
                    //点击战场,显示地形 暂略...


    checkCharacter函数用来判断是否点击了相应的我军,友军,或敌军,如下

    LSouSouSMapClick.prototype.checkCharacter = function(list,mx,my){
    	var i,isChara,sx,sy,act,_characterS;
    	//是否点击我军
    	for(i=0;i<list.length;i++){
    		_characterS = list[i];
    		if(!_characterS.visible)continue;
    		if(mx > _characterS.x + LSouSouObject.sMap.backLayer.x && 
    			mx < _characterS.x + LSouSouObject.sMap.backLayer.x + LSouSouObject.sMap.nodeLength && 
    			my > _characterS.y + LSouSouObject.sMap.backLayer.y && 
    			my < _characterS.y + LSouSouObject.sMap.backLayer.y + LSouSouObject.sMap.nodeLength){
    			LSouSouObject.charaSNow = _characterS;
    			sx = LSouSouObject.charaSNow.x;
    			sy = LSouSouObject.charaSNow.y;
    			act = LSouSouObject.charaSNow.action;
    			LSouSouObject.returnFunction = function (){
    				LSouSouObject.charaSNow.x = sx;
    				LSouSouObject.charaSNow.y = sy;
    				LSouSouObject.charaSNow.action = act;
    				LSouSouObject.charaSNow.tagerCoordinate=new LPoint(LSouSouObject.charaSNow.x,LSouSouObject.charaSNow.y);
    				LSouSouObject.sMap.menu = LSouSouSMapMenu.addSMenu(LSouSouObject.charaSNow.x,LSouSouObject.charaSNow.y,"select");
    				LSouSouObject.sMap.menuLayer.addChild(LSouSouObject.sMap.menu);
    			}
    			LSouSouObject.returnFunction();
    			return true;
    		}
    	}
    	return false;
    };

    当点中了战场上某一个军队,则会调用LSouSouSMapMenu.addSMenu函数,LSouSouSMapMenu类是为了管理战场上的选择列表而专门创建的,代码如下。

    LSouSouSMapMenu = function(){};
    LSouSouSMapMenu.addSMenu = function(x,y,value){
            var _menu;
    	switch(value){
    		case "select":
    			_menu = new LSouSouSMapMenuSelect(x,y);
    			break;
    	}
    	_menu.name = value;
    	return _menu;
    };


    本次只用到了其中的一小部分,以后会继续完善,上面代码中的LSouSouSMapMenuSelect就是一个选择列表,代码如下

    function LSouSouSMapMenuSelect(x,y){
    	var self = this;
    	base(self,LSprite,[]);
    	LSouSouObject.sMap.setLocation();
    	if(LSouSouObject.charaSNow.belong == LSouSouObject.BELONG_SELF){
    		self.addMenuOur(x,y);
    	}else{
    		
    	}
    }
    LSouSouSMapMenuSelect.prototype.addMenuOur = function(x,y){
    	var self = this;
    	LSouSouObject.sMap.smapClick.removeClickEvent(); 
    	var menuLayer = new LSprite();
    	var buttonMove = new LButtonSample1("武将移动");
    	buttonMove.x = 15;
    	buttonMove.y = 15;
    	menuLayer.addChild(buttonMove);
    	var buttonDetailed = new LButtonSample1("武将情报");
    	buttonDetailed.x = 15;
    	buttonDetailed.y = buttonMove.getHeight() + buttonMove.y;
    	menuLayer.addChild(buttonDetailed);
    	var buttonStandby = new LButtonSample1("原地待命");
    	buttonStandby.x = 15;
    	buttonStandby.y = buttonMove.getHeight()*2 + buttonMove.y;
    	menuLayer.addChild(buttonStandby);
    	var buttonCancel = new LButtonSample1("行动取消");
    	buttonCancel.x = 15;
    	buttonCancel.y = buttonMove.getHeight()*3 + buttonMove.y;
    	menuLayer.addChild(buttonCancel);
    	var selfWidth = buttonMove.getWidth()+30;
    	var selfHeight = buttonMove.getHeight()*4+30;
            var bar = LSouSouObject.getBar(selfWidth,selfHeight);
    	menuLayer.addChild(bar);
    	self.addChild(menuLayer);
    	x += LSouSouObject.sMap.nodeLength;
    	if(x + LSouSouObject.sMap.backLayer.x + selfWidth > LSouSouObject.sMap.SCREEN_WIDTH){
    		x -= (selfWidth + LSouSouObject.sMap.nodeLength); 
    	}
    	if(y + LSouSouObject.sMap.backLayer.y + selfHeight > LSouSouObject.sMap.SCREEN_HEIGHT){
    		y = LSouSouObject.sMap.SCREEN_HEIGHT - selfHeight - LSouSouObject.sMap.backLayer.y;
    	}
    	self.x = x + LSouSouObject.sMap.backLayer.x;
    	self.y = y + LSouSouObject.sMap.backLayer.y;
    	
    	buttonMove.addEventListener(LMouseEvent.MOUSE_UP,self.onclickMove);
    };
    LSouSouSMapMenuSelect.prototype.onclickMove = function(e){
    	LSouSouObject.sMap.roadList = LSouSouObject.sQuery.makePath(LSouSouObject.charaSNow);
    	var i,nodeChild;
    	for(i=0;i<LSouSouObject.sMap.roadList.length;i++){
    		nodeChild = LSouSouObject.sMap.roadList[i];
    		LSouSouObject.sMap.roadLayer.graphics.drawRect(1,"#000000",[nodeChild.x*LSouSouObject.sMap.nodeLength,nodeChild.y*LSouSouObject.sMap.nodeLength,LSouSouObject.sMap.nodeLength,LSouSouObject.sMap.nodeLength],true,"#FFFFFF");
    	}
    	LSouSouObject.sMap.menuLayer.removeChild(LSouSouObject.sMap.menu);
    	LSouSouObject.sMap.menu = null;
    	LSouSouObject.sMap.smapClick.setClickEvent(); 
    };


    上面代码可以看到,只是当点击我军的情况下,才添加了选择列表,后面会继续添加友军和敌军的列表,有了上面的代码,就可以添加一个选择列表了,效果如下。

    移动范围之宽度优先

    有了选择列表,当点击选择列表的【武将移动】按钮的时候,应该出现该武将的可移动的范围了,通过前面的代码可以知道,当点击【武将移动】按钮时,会调用LSouSouSMapMenuSelect.prototype.onclickMove函数,而在这个函数里又是通过LSouSouObject.sQuery.makePath函数来确定可移动路径的范围的,下面来说明一下makePath函数是如何具体来实现的。

    为了便于控制战场上的寻路和寻路范围的确定,先来创建一个LSouSouSQuery类,并继承自A*算法类LStarQuery,如下

    function LSouSouSQuery(map){
    	var self = this;
    	base(self,LStarQuery,[]);
    	self.queryType = 1;
    	self._map = [];
    	self._w = map[0].length;
    	self._h = map.length;
            for (var y=0; y<self._h; y++) {
    		self._map.push([]);
    	        for (var x=0; x<self._w; x++) {
    			self._map[y].push(new LSouSouNode(x,y,map[y][x]));
    	        }
    	}
    }


    这里,不但继承了A*算法类LStarQuery,并对其进行了初始化,设定了_map的值,里面的LSouSouNode等,一会儿讲寻路的时候再具体说。首先这个LSouSouSQuery就拥有了A*算法的寻路功能,下面主要为它添加一个搜索范围的功能。

    移动范围的确定,主要通过makePath,setPathAll和loopPath三个函数来确定,具体代码如下

    LSouSouSQuery.prototype.setPathAll = function(px,py,value){
    	var self = this;
    	if(self._enemyCost[px+"-"+py] != null && self._enemyCost[px+"-"+py] >= 200)return;
    	if(value == -1){
    		self._enemyCost[px+"-"+py] = "all";
    		return;
    	}
    	self._enemyCost[px+"-"+py] = value;
    };
    LSouSouSQuery.prototype.makePath = function(chara){
    	var self = this;
    	self._chara = chara;
    	self._path = [];
    	var isOver = false;
    	self.setStart();
    	self._enemyCost = {};
    	var thisChara;
    	if(chara.belong == LSouSouObject.BELONG_SELF || chara.belong == LSouSouObject.BELONG_FRIEND){
    		for(var i=0;i<LSouSouObject.sMap.enemylist.length;i++){
    			thisChara = LSouSouObject.sMap.enemylist[i];
    			if(thisChara.visible){
    				self._enemyCost[thisChara.locationX() + "-" + thisChara.locationY()] = 255;
    				self.setPathAll((thisChara.locationX() - 1) , thisChara.locationY() , -1);
    				self.setPathAll((thisChara.locationX() + 1) , thisChara.locationY() , -1);
    				self.setPathAll(thisChara.locationX() , (thisChara.locationY() - 1) , -1);
    				self.setPathAll(thisChara.locationX() , (thisChara.locationY() + 1) , -1);
    			}
    		}
    	}else if(chara.belong == LSouSouObject.BELONG_ENEMY){
    		for(var i=0;i<LSouSouObject.sMap.ourlist.length;i++){
    			thisChara = LSouSouObject.sMap.ourlist[i];
    			if(thisChara.visible){
    				self._enemyCost[thisChara.locationX() + "-" + thisChara.locationY()] = 255;
    				self.setPathAll((thisChara.locationX() - 1) , thisChara.locationY() , -1);
    				self.setPathAll((thisChara.locationX() + 1) , thisChara.locationY() , -1);
    				self.setPathAll(thisChara.locationX() , (thisChara.locationY() - 1) , -1);
    				self.setPathAll(thisChara.locationX() , (thisChara.locationY() + 1) , -1);
    			}
    		}
    		for(var i=0;i<LSouSouObject.sMap.friendlist.length;i++){
    			thisChara = LSouSouObject.sMap.friendlist[i];
    			if(thisChara.visible){
    				self._enemyCost[thisChara.locationX() + "-" + thisChara.locationY()] = 255;
    				self.setPathAll((thisChara.locationX() - 1) , thisChara.locationY() , -1);
    				self.setPathAll((thisChara.locationX() + 1) , thisChara.locationY() , -1);
    				self.setPathAll(thisChara.locationX() , (thisChara.locationY() - 1) , -1);
    				self.setPathAll(thisChara.locationX() , (thisChara.locationY() + 1) , -1);
    			}
    		}
    	}
    	self._starPoint = self._map[chara.locationY()][chara.locationX()];
    	self._starPoint.moveLong = chara.member.getDistance();
    	self.loopPath(self._starPoint);
    	return self._path;
    };
    LSouSouSQuery.prototype.loopPath = function(thisPoint){
    	var self = this;
    	if (thisPoint.moveLong <= 0)return;
    	if (!thisPoint.isChecked) {
    		self._path.push(thisPoint);
    		thisPoint.isChecked = true;
    	}
    	var checkList = [];
    	//获取周围四个点
    	if (thisPoint.y > 0)checkList.push(self._map[(thisPoint.y-1)][thisPoint.x]);
    	if (thisPoint.x > 0)checkList.push(self._map[thisPoint.y][(thisPoint.x-1)]);
    	if (thisPoint.x < self._w - 1)checkList.push(self._map[thisPoint.y][(thisPoint.x+1)]);
    	if (thisPoint.y < self._h - 1)checkList.push(self._map[(thisPoint.y+1)][thisPoint.x]);
    	var i;
    	for (i=0; i<checkList.length; i++) {
    		var checkPoint = checkList[i];
    		if(!checkPoint.moveLong)checkPoint.moveLong = 0;
    		if(checkPoint.isChecked && checkPoint.moveLong >= thisPoint.moveLong)continue;
    		var cost = parseInt(LGlobal.arms["Arms" + self._chara.member.getArms()]["Terrain"]["Terrain" + self._map[checkPoint.y][checkPoint.x].value]["Cost"]);
    		cost += self._enemyCost[checkPoint.x + "-" + checkPoint.y] != null && self._enemyCost[checkPoint.x + "-" + checkPoint.y] != "all" ? self._enemyCost[checkPoint.x + "-" + checkPoint.y]:0;
    		checkPoint.moveLong = thisPoint.moveLong - cost;
    		if (self._enemyCost[checkPoint.x + "-" + checkPoint.y] == "all" && checkPoint.moveLong > 1)checkPoint.moveLong = 1;
    		self.loopPath(checkPoint);
    	}
    };

    setPathAll函数,将地图上有敌军的地方,设置直接消耗移动力为剩余所有移动力。

    loopPath函数,以当前搜索点为中心,像上下左右四个方向扩散,每扩散一个范围,消耗相应的地形所在的移动力,直到移动力消耗为0。

    makePath函数,主要进行开始搜索时的初始化工作。
    因为在战场上,不同的地形所对应的移动消耗是不同的,不同的兵种在不同的地形上的移动消耗也是不同的,下面是兵种设定。

    {
    "Arms1":{
    	"Name":"群雄",
    	"Arms_type":0,
    	"MoveType":0,
    	"Property":{
    		"Attack":"A",
    		"Spirit":"A",
    		"Defense":"A",
    		"Breakout":"A",
    		"Morale":"A",
    		"Troops":"5",
    		"Strategy":"1"
    	},
    	"Distance":6,
    	"Helmet":0,
    	"Equipment":0,
    	"Weapon":0,
    	"Horse":0,
    	"AttackLong":1,
    	"Restrain":{},
    	"Terrain":{
    		"Terrain0":{"Addition":110,"Cost":1},
    		"Terrain1":{"Addition":110,"Cost":1},
    		"Terrain2":{"Addition":80,"Cost":2},
    		"Terrain3":{"Addition":100,"Cost":100},
    		"Terrain4":{"Addition":100,"Cost":100},
    		"Terrain5":{"Addition":120,"Cost":1},
    		"Terrain6":{"Addition":100,"Cost":1},
    		"Terrain7":{"Addition":110,"Cost":1},
    		"Terrain8":{"Addition":80,"Cost":3},
    		"Terrain9":{"Addition":100,"Cost":1},
    		"Terrain10":{"Addition":100,"Cost":100},
    		"Terrain11":{"Addition":100,"Cost":100},
    		"Terrain12":{"Addition":100,"Cost":1},
    		"Terrain13":{"Addition":80,"Cost":2}
    	},
    	"RangeAttack":[{"x":0,"y":-1},{"x":0,"y":1},{"x":-1,"y":0},{"x":1,"y":0}],
    	"RangeAttackTarget":[{"x":0,"y":0}],
    	"Strategy":[{"lv":0,"value":"2"},{"lv":2,"value":"1"},{"lv":6,"value":"6"}],
    	"Introduction":"各方面都较为突出的兵种。"
    },
    ......
    }

    里面包含了该兵种的各种属性,其中Terrain属性是该兵种在不同地形上的移动消耗(Cost)和适应性(Addition)

    当然,s01.smap地图中的地形设定,也要完善一下。

    {"data":[
    [1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1],
    [3,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1],
    [3,3,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1],
    [3,3,3,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,3,3],
    [3,3,3,3,1,1,1,0,0,0,0,1,1,1,1,1,1,3,3,3],
    [3,3,3,3,3,1,1,0,0,0,0,0,1,1,1,1,1,3,3,3],
    [3,3,3,3,3,4,4,4,4,0,0,4,4,4,4,1,1,3,3,3],
    [3,3,3,3,3,4,6,0,0,0,0,0,0,6,4,1,3,3,3,3],
    [3,3,3,3,3,4,6,0,0,0,0,0,0,6,4,1,3,3,3,3],
    [3,3,3,3,0,4,6,0,0,0,0,0,0,0,4,1,3,3,3,3],
    [3,3,3,0,0,0,0,0,0,4,4,0,0,0,4,3,3,3,3,3],
    [3,3,0,0,0,0,0,0,0,5,7,0,0,0,4,3,3,3,3,3],
    [3,0,0,0,0,4,0,0,0,0,0,0,0,0,4,3,3,3,3,3],
    [0,0,0,0,0,4,6,6,0,0,0,0,0,7,4,0,3,3,3,3],
    [0,0,0,0,0,4,4,4,4,0,0,4,4,4,4,0,1,1,3,3],
    [0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,3],
    [1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
    [1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1],
    [1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1],
    [1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1]
    ]
    ,"img-small":"01-small.png"
    ,"img-big":"01-big.png"}

    好了,最后,效果如下。

    这张图不小心截错了,等后面抽出时间再截一张

    当遇到敌军的时候,移动力直接归零,比如下面的效果,左边是友军张飞,右边是敌军关羽,所以张飞左侧是可以到达的,而关羽的右侧是不可到达的,这就是战场上的人物遮挡,在战场上通过适当人物站位,可以有效的阻止敌军的攻击,和保护我军防御较弱的部队。


    利用A*算法来移动部队

    在A*寻路类LStarQuery中,是否可通过的判断是通过该节点坐标是0还是1来判断的,而战棋游戏中就不一样了,前面已经确定了可移动的范围,那么该范围内就是它可通过的路径,所以,在它的子类LSouSouSQuery中,要稍微修改一下。

    首先是节点类

    function LSouSouNode(_x,_y,_v){
    	var self = this;
    	base(self,LNode,[_x,_y,_v]);
    	self.isRoad = false;
    }
    LSouSouNode.prototype.init = function(){
    	var self = this;
    	arguments.callee[SUPER]["init"].call(self);
    	self.isRoad = false;
    };
    LSouSouNode.prototype.toString = function(){
    	return "["+this.x+","+this.y+","+this.isRoad+"]";
    };
    


    这样LSouSouNode继承自LNode类,并添加了新属性isRoad,当这个属性为true的时候,表示该位置可通过。

    原来的是否可通过的判断,也要相应的修改一下,如下

    /*判断是否可通过*/
    LSouSouSQuery.prototype.isWay = function(checkPoint,thisPoint){
    	if (this._map[checkPoint.y][checkPoint.x].isRoad) return true;
    	return false;
    };


    每次搜索前的地图初始化部分,修改如下

    LSouSouSQuery.prototype.setStart = function(){
    	var self=this,node;
    	arguments.callee[SUPER]["setStart"].call(self);
    	if(!LSouSouObject.sMap.roadList)return;
    	for(var i=0;i<LSouSouObject.sMap.roadList.length;i++){
    		node = LSouSouObject.sMap.roadList[i];
                    self._map[node.y][node.x].isRoad = true;
    	}
    };


    就是提前设定好各节点的isRoad的值,这样一来,在LSouSouSMapClick中,首先判断是否点中了移动路径的范围,代码如下。

    LSouSouSMapClick.prototype.clickRoad = function(mx,my){
    	var intX = ((mx - LSouSouObject.sMap.backLayer.x)/LSouSouObject.sMap.nodeLength) >>> 0;
    	var intY = ((my - LSouSouObject.sMap.backLayer.y)/LSouSouObject.sMap.nodeLength) >>> 0;
    	var isRoad = false,node,_characterS,i,j;
    	for(i=0;i<LSouSouObject.sMap.roadList.length;i++){
    		node = LSouSouObject.sMap.roadList[i];
    		for(j=0;j<LSouSouObject.sMap.ourlist.length;j++){
    			_characterS = LSouSouObject.sMap.ourlist[j];
    			if(_characterS.visible && _characterS.member.getIndex() != LSouSouObject.charaSNow.member.getIndex() && _characterS.locationX() == intX && _characterS.locationY() == intY)return;
    		}
    		for(j=0;j<LSouSouObject.sMap.friendlist.length;j++){
    			_characterS = LSouSouObject.sMap.friendlist[j];
    			if(_characterS.locationX() == intX && _characterS.locationY() == intY)return;
    		}
    		for(j=0;j<LSouSouObject.sMap.enemylist.length;j++){
    			_characterS = LSouSouObject.sMap.enemylist[j];
    			if(_characterS.locationX() == intX && _characterS.locationY() == intY)return;
    		}
    		if(mx >= node.x*LSouSouObject.sMap.nodeLength + LSouSouObject.sMap.backLayer.x && 
    			mx < node.x*LSouSouObject.sMap.nodeLength + LSouSouObject.sMap.backLayer.x + LSouSouObject.sMap.nodeLength && 
    			my >= node.y*LSouSouObject.sMap.nodeLength + LSouSouObject.sMap.backLayer.y && 
    			my < node.y*LSouSouObject.sMap.nodeLength + LSouSouObject.sMap.backLayer.y + LSouSouObject.sMap.nodeLength){
    			isRoad = true;
    			break;
    		}
    	}
    	if(!isRoad)return;
    	LSouSouObject.sMap.moveToCoordinate(intX,intY);
    };


    因为,不可能将人物移动到另一个人物之上,所以有人的地方要排除,最后,点击了路径之后,调用LSouSouObject.sMap.moveToCoordinate函数,如下。

    LSouSouSMap.prototype.moveToCoordinate = function(intX,intY){
    	var self = this;
    	var toPoint = new LPoint(intX,intY);
    	LSouSouObject.charaSNow.path = LSouSouObject.sQuery.queryPath(new LPoint(LSouSouObject.charaSNow.locationX(),LSouSouObject.charaSNow.locationY()),toPoint);
    	trace("LSouSouObject.charaSNow.path="+LSouSouObject.charaSNow.path);
    	if(LSouSouObject.charaSNow.path){
    		self.roadList = null;
    		LSouSouObject.sMap.roadLayer.graphics.clear();
    		LSouSouObject.charaSNow.addEventListener(LSouSouEvent.CHARACTER_MOVE_COMPLETE,self.onShowAttackMenu);
    	}
    };
    LSouSouSMap.prototype.onShowAttackMenu = function(){
    	var self = LSouSouObject.sMap;
    	LSouSouObject.charaSNow.removeEventListener(LSouSouEvent.CHARACTER_MOVE_COMPLETE,self.onShowAttackMenu);
    	trace("移动结束");
    };

    上面代码,如果搜索到了路径,则将路径赋值给当前正在控制的军队LSouSouObject.charaSNow,然后最后就是LSouSouCharacterS类的修改了。
    在LSouSouCharacterS类中判断路径path是否有值,有的话,根据path中的坐标节点,一个一个的移动,直到移动到最后一个节点,然后移动结束。

    LSouSouCharacterS.prototype.move = function(){
    	var self = this;
    	if(!self.path)return;
    	if(self.x == self.tagerCoordinate.x && self.y == self.tagerCoordinate.y){
    		if(self.path.length == 0){
    			self.tagerCoordinate.x = self.locationX();
    			self.tagerCoordinate.y = self.locationY();
    			self.path = null;
    			if(self.onMoveComplete)self.onMoveComplete();
    			return;
    		}else{
    			self.tagerCoordinate.x = self.path[0].x*LSouSouObject.sMap.nodeLength;
    			self.tagerCoordinate.y = self.path[0].y*LSouSouObject.sMap.nodeLength;
    			self.path.shift();
    		}
    	}
    	if(self.x > self.tagerCoordinate.x){
    		self.x -= LStaticSouSouCharacterS.MOVESETP;
    		self.action = LStaticSouSouCharacterS.MOVE_LEFT;
    	}else if(self.y < self.tagerCoordinate.y){
    		self.y += LStaticSouSouCharacterS.MOVESETP;
    		self.action = LStaticSouSouCharacterS.MOVE_DOWN;
    	}else if(self.y > self.tagerCoordinate.y){
    		self.y -= LStaticSouSouCharacterS.MOVESETP;
    		self.action = LStaticSouSouCharacterS.MOVE_UP;
    	}else{
    		self.x += LStaticSouSouCharacterS.MOVESETP;
    		self.action = LStaticSouSouCharacterS.MOVE_RIGHT;
    	}
    };

    然后,在LSouSouCharacterS的时间轴函数onframe中调用move函数就可以了,下面的预览图,刘备正在移动中。

    测试连接如下

    http://lufylegend.com/demo/test/lsharp/10/game/index.html

    以上,本章就先讲这么多了,下一章可能会讲一讲攻击?

    本章为止的源码如下,不包含lufylegend.js引擎源码,请自己到官网下载

    http://lufylegend.com/demo/test/lsharp/10/10.rar

    ※源码运行说明:需要服务器支持,详细请看本系列文章《序》和《第一章》

    《游戏脚本的设计与开发》系列文章目录

    http://blog.csdn.net/lufy_legend/article/details/8888787

    本章就讲到这里,欢迎继续关注我的博客

     

    转载请注明:转自lufy_legend的博客http://blog.csdn.net/lufy_legend
  • 相关阅读:
    arcgis增大缩放级别
    arcgis地图窗口操作
    arcgis访问百度地图
    内存溢出和内存泄漏的区别、产生原因以及解决方案
    多线程之sleep和wait的区别
    java技术-重点方向
    技术思维VS管理思维
    几个问题
    写完百行代码,一次运行全部案例通过 是什么体验?
    分布式开放消息系统(RocketMQ)的原理与实践
  • 原文地址:https://www.cnblogs.com/jiangu66/p/3206250.html
Copyright © 2020-2023  润新知