让怪物动起来容易,可是想让他不会向路痴一样乱跑,而是聪明地动起来,还是有一点难度的。本节中通过一个演示程序,对游戏中能够让怪物聪明地运动的各种算法进行详细介绍。
1.路径搜索示例基本框架的搭建
在正式介绍搜索算法之前,需要将示例的框架搭建出来,这样在介绍各个搜索算法时才能够看到算法的运行效果。
GameView类:
package wyf.ytl;
import java.util.ArrayList;
import java.util.HashMap;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Spinner;
import android.widget.TextView;
public class GameView extends View{
private static final int VIEW_WIDTH = 320;//此View的宽度
private static final int VIEW_HEIGHT = 340;//此View的高度
Game game;
Spinner mySpinner;
TextView CDTextView;
int span = 13;
Bitmap source = BitmapFactory.decodeResource(getResources(), R.drawable.source);
Bitmap target = BitmapFactory.decodeResource(getResources(), R.drawable.target) ;
Paint paint = new Paint();
private Handler myHandler = new Handler(){//用来更新UI线程的控件
public void handleMessage(Message msg) {
if(msg.what == 1){//改变长度的TextView的值
CDTextView.setText("路径长度:" + (Integer)msg.obj);
}
}
};
public GameView(Context context, AttributeSet attrs) {//构造器
super(context, attrs);
}
protected void onDraw(Canvas canvas) {//重写的绘制方法
try{
onMyDraw(canvas);//调用自己的绘制方法
}
catch(Exception e){}
}
protected void onMyDraw(Canvas canvas){//自己的绘制方法
super.onDraw(canvas);
canvas.drawColor(Color.GRAY);//背景色
//省略绘制方法,随后完善
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){//重新的方法,返回的是此View的大小
setMeasuredDimension(VIEW_WIDTH,VIEW_HEIGHT);
}
}
res/layout下的布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<com.ga.GameView
android:id="@+id/GameView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
<AbsoluteLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
> <!-- 坐标布局 -->
<Spinner
android:id="@+id/mySpinner"
android:layout_width="150px"
android:layout_height="wrap_content"
android:layout_x="5px"
android:layout_y="0px"
/> <!-- 在指定位置添加一个下拉列表Spinner -->
<Spinner
android:id="@+id/target"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_x="155px"
android:layout_y="0px"
/>
<Button
android:id="@+id/go"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始"
android:layout_x="260px"
android:layout_y="0px"
/>
<TextView
android:id="@+id/bushu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="使用步数:"
android:layout_x="20px"
android:layout_y="50px"
/>
<TextView
android:id="@+id/changdu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="路径长度:"
android:layout_x="150px"
android:layout_y="50px"
/>
</AbsoluteLayout>
</LinearLayout>
注释掉报错部分,此时运行项目效果如图:
2.路径搜索示例的控制面板
基本框架搭建完成,但是控件内还没有内容,接下来继续对框架进行完善。
地图类MapList:
package wyf.ytl;
public class MapList {
static int[] source = { 2, 2 }; // 出发点坐标
static int[][] target = { { 13, 2 }, { 4, 22 }, { 0, 11 }, { 9, 10 },
{ 21, 22 } }; // 目标点坐标数组
static int[][][] map = new int[][][]// 地图数组
{ {
{ 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 },
{ 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0 },
{ 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 },
{ 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 },
{ 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0 },
{ 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0 },
{ 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
{ 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } };
}
- 二维数组target为目标点的位置数组,这样可以添加多个目标点,运行时通过选择不同的目标点来测试同一种搜索算法在不同情况下的运行效果。
- map为地图数组,使用三维数组表示的,以后如需添加新地图,只需在三维数组中再添加一个二维数组即可。
算法框架Game类:
package wyf.ytl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Stack;
import android.os.Handler;//引入相关类
import android.os.Message;//引入相关类
import android.widget.Button;//引入相关类
import android.widget.TextView;//引入相关类
public class Game {//算法类
int algorithmId=0;//算法代号 0--深度优先
int mapId = 0;//地图编号
int[][] map = MapList.map[mapId];
int[] source = MapList.source;//出发点
int[] target = MapList.target[0];//目标点
GameView gameView;//gameView的引用
Button goButton;//goButton的引用
TextView BSTextView;//BSTextView的引用
ArrayList<int[][]> searchProcess=new ArrayList<int[][]>();//搜索过程
Stack<int[][]> stack=new Stack<int[][]>();//深度优先所用栈
HashMap<String,int[][]> hm=new HashMap<String,int[][]>();//结果路径记录
LinkedList<int[][]> queue=new LinkedList<int[][]>();//广度优先所用队列
//A*用优先级队列
PriorityQueue<int[][]> astarQueue=new PriorityQueue<int[][]>(100,new AStarComparator(this));
//记录到每个点的最短路径 for Dijkstra
HashMap<String,ArrayList<int[][]>> hmPath=new HashMap<String,ArrayList<int[][]>>();
//记录路径长度 for Dijkstra
int[][] length=new int[MapList.map[mapId].length][MapList.map[mapId][0].length];
int[][] visited=new int[MapList.map[0].length][MapList.map[0][0].length];//0 未去过 1 去过
int[][] sequence={
{0,1},{0,-1},
{-1,0},{1,0},
{-1,1},{-1,-1},
{1,-1},{1,1}
};
boolean pathFlag=false;//true 找到了路径
int timeSpan=10;//时间间隔
private Handler myHandler = new Handler(){//用来更新UI线程的控件
public void handleMessage(Message msg){
if(msg.what == 1){//改变按钮状态
goButton.setEnabled(true);
}
else if(msg.what == 2){//改变步数的TextView的值
BSTextView.setText("使用步数:" + (Integer)msg.obj);
}
}
};
public void clearState(){//清空所有状态与列表
pathFlag=false;
searchProcess.clear();
stack.clear();
queue.clear();
astarQueue.clear();
hm.clear();
visited=new int[MapList.map[mapId].length][MapList.map[mapId][0].length];
hmPath.clear();
for(int i=0;i<length.length;i++){
for(int j=0;j<length[0].length;j++){
length[i][j]=9999;//初始路径长度为最大距离都不可能的那么大
}
}
}
public void runAlgorithm(){//运行算法
clearState();
switch(algorithmId){
//此处代码省略,稍后补全
}
}
}
- sequence数组表示相对于当前点的8个方向的点。
Activity类:
package wyf.ytl;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.TextView;
public class MainActivity extends Activity {
private static final String[] mySpinner_str = {//搜索下拉列表的内容
"深度优先","广度优先","广度优先A*","Dijkstra","Dijkstra A*"
};
Spinner mySpinner;//搜索下拉列表框
Spinner targetSpinner;//目标下拉列表框
Button goButton;//开始按钮
GameView gameView;//自己实现的地图View
TextView BSTextView;//使用步数的文本
TextView CDTextView;//路径长度的文本
Game game;
private ArrayAdapter<String> adapter;//搜索下拉列表的模型
private ArrayAdapter<String> adapter2;//目标下拉列表的模型
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);//设置当前显示的用户界面
mySpinner = (Spinner)findViewById(R.id.mySpinner);//得到搜索下拉列表的引用
targetSpinner = (Spinner)findViewById(R.id.target);//得到目标下拉列表的引用
gameView = (GameView) findViewById(R.id.gameView);//得到GameView的引用
BSTextView = (TextView)findViewById(R.id.bushu);//得到使用步数的文本控件的引用
CDTextView = (TextView)findViewById(R.id.changdu);//得到路径长度的文本控件的引用
goButton = (Button) findViewById(R.id.go);//得到开始按钮的引用
game = new Game();//初始化算法类
//创建搜索下拉列表的模型
adapter = new ArrayAdapter<String>(this,android.R.layout.simple_spinner_item, mySpinner_str);
String[] target_str = new String[MapList.target.length];//根据目标点的个数创建一个数组
for(int i=0; i<MapList.target.length; i++){
target_str[i] = "目标"+i;
}
//创建目标下拉列表的模型
adapter2 = new ArrayAdapter<String>(this,android.R.layout.simple_spinner_item, target_str);
mySpinner.setAdapter(adapter);//设置模型
targetSpinner.setAdapter(adapter2);//设置模型
goButton.setOnClickListener(//按钮监听器
new Button.OnClickListener(){
public void onClick(View v) {
game.runAlgorithm();//调用运算方法
goButton.setEnabled(false);
}
}
);
targetSpinner.setOnItemSelectedListener(//目标选择的下拉列表监听
new Spinner.OnItemSelectedListener(){
public void onItemSelected(AdapterView<?> a, View v,int arg2, long arg3){
game.target = MapList.target[arg2];
game.clearState();//将game的状态清空
gameView.postInvalidate();//重写绘制gameView
}
public void onNothingSelected(AdapterView<?> arg0){
}
}
);
mySpinner.setOnItemSelectedListener(//算法选择的下拉列表监听
new Spinner.OnItemSelectedListener(){
public void onItemSelected(AdapterView<?> ada, View v,int arg2, long arg3){
game.clearState();//将game的状态清空
game.algorithmId = (int) ada.getSelectedItemId();//得到选择的算法ID
gameView.postInvalidate();//重写绘制gameView
}
public void onNothingSelected(AdapterView<?> arg0) {
}
}
);
this.initIoc();//调用依赖注入方法
}
public void initIoc()
{//依赖注入
gameView.game = this.game;
gameView.mySpinner = this.mySpinner;
gameView.CDTextView = this.CDTextView;
game.gameView = this.gameView;
game.goButton = this.goButton;
game.BSTextView = this.BSTextView;
}
}
注释掉报错部分,此时运行项目效果如图:
3.继续完善GameView
GameView类onMyDraw方法补充如下:
protected void onMyDraw(Canvas canvas){//自己的绘制方法
super.onDraw(canvas);
canvas.drawColor(Color.GRAY);//背景色
paint.setColor(Color.BLACK);//设置颜色
paint.setStyle(Style.STROKE);//设置风格
canvas.drawRect(5, 5, 313, 327, paint);//绘制矩形
int[][] map = game.map;
int row = map.length;
int col = map[0].length;
for(int i=0; i<row; i++){//绘制地图块
for(int j=0; j<col; j++){
if(map[i][j] == 0){//白色
paint.setColor(Color.WHITE);
paint.setStyle(Style.FILL);
canvas.drawRect(6+j*(span+1), 6+i*(span+1), 6+j*(span+1)+span, 6+i*(span+1)+span, paint);
}
else if(map[i][j] == 1){//黑色
paint.setColor(Color.BLACK);
paint.setStyle(Style.FILL);
canvas.drawRect(6+j*(span+1), 6+i*(span+1), 6+j*(span+1)+span, 6+i*(span+1)+span, paint);
}
}
}
ArrayList<int[][]> searchProcess=game.searchProcess;
for(int k=0;k<searchProcess.size();k++){//绘制寻找过程
int[][] edge=searchProcess.get(k);
paint.setColor(Color.BLACK);
paint.setStrokeWidth(1);
canvas.drawLine(
edge[0][0]*(span+1)+span/2+6,edge[0][1]*(span+1)+span/2+6,
edge[1][0]*(span+1)+span/2+6,edge[1][1]*(span+1)+span/2+6,
paint
);
}
//绘制结果路径
if(
mySpinner.getSelectedItemId()==0||
mySpinner.getSelectedItemId()==1||
mySpinner.getSelectedItemId()==2
){//"深度优先","广度优先","广度优先 A*" 绘制
if(game.pathFlag){
HashMap<String,int[][]> hm=game.hm;
int[] temp=game.target;
int count=0;//路径长度计数器
while(true){
int[][] tempA=hm.get(temp[0]+":"+temp[1]);
paint.setColor(Color.BLACK);
paint.setStyle(Style.STROKE);//加粗
paint.setStrokeWidth(2);//设置画笔粗度为2px
canvas.drawLine(
tempA[0][0]*(span+1)+span/2+6,tempA[0][1]*(span+1)+span/2+6,
tempA[1][0]*(span+1)+span/2+6,tempA[1][1]*(span+1)+span/2+6,
paint
);
count++;
if(tempA[1][0]==game.source[0]&&tempA[1][1]==game.source[1]){//判断有否到出发点
break;
}
temp=tempA[1];
}
Message msg1 = myHandler.obtainMessage(1, count);//改变TextView文字
myHandler.sendMessage(msg1);
}
}
else if(
mySpinner.getSelectedItemId()==3||
mySpinner.getSelectedItemId()==4
){//"Dijkstra"绘制
if(game.pathFlag){
HashMap<String,ArrayList<int[][]>> hmPath=game.hmPath;
ArrayList<int[][]> alPath=hmPath.get(game.target[0]+":"+game.target[1]);
for(int[][] tempA:alPath){
paint.setColor(Color.BLACK);
paint.setStyle(Style.STROKE);//加粗
paint.setStrokeWidth(2);//设置画笔粗度为2px
canvas.drawLine(
tempA[0][0]*(span+1)+span/2+6,tempA[0][1]*(span+1)+span/2+6,
tempA[1][0]*(span+1)+span/2+6,tempA[1][1]*(span+1)+span/2+6,
paint
);
}
Message msg1 = myHandler.obtainMessage(1, alPath.size());//改变TextView文字
myHandler.sendMessage(msg1);
}
}
//绘制出发点
canvas.drawBitmap(source, 6+game.source[0]*(span+1), 6+game.source[1]*(span+1), paint);
//绘制目标点
canvas.drawBitmap(target, 6+game.target[0]*(span+1), 6+game.target[1]*(span+1), paint);
}
此时运行项目效果如图:
在前面代码Game类中出现AStarComparator类是A*算法所使用的比较器,下面来创建他的代码。
AStarComparator类:
package wyf.ytl;
import java.util.Comparator;
public class AStarComparator implements Comparator<int[][]> {
Game game;
public AStarComparator(Game game) {
this.game=game;
}
@Override
public int compare(int[][] o1, int[][] o2) {
int[] t1=o1[1];
int[] t2=o2[2];
int[] target=game.target; //得到目标点
//直线物理距离(免开方)
int a=(t1[0]-target[0])*(t1[0]-target[0])+(t1[1]-target[1])*(t1[1]-target[1]);
int b=(t2[0]-target[0])*(t2[0]-target[0])+(t2[1]-target[1])*(t2[1]-target[1]);
//门特卡罗距离
//int a=game.visited[o2[0][1]][o2[0][0]]+Math.abs(t1[0]-target[0])+Math.abs(t1[1]-target[1]);
//int b=game.visited[o2[0][1]][o2[0][0]]+Math.abs(t2[0]-target[0])+Math.abs(t2[1]-target[1]);
return a-b;
}
@Override
public boolean equals(Object o) {
return false;
}
}
- 两点间距离的计算方法有多种,列出的门特卡罗距离就是一种,但本例中使用的是直线物理距离,所以将门特卡罗距离注释。
4.深度优先路径搜索DFS
深度优先搜索DFS在搜索过程中不考虑各个边的开销,只考虑路径的选择。其思路是站在一个连通图的节点上,然后尽可能地沿着一条边深入,当遇到死胡同时进行回溯,然后继续搜索,直到找到目标为止。
在Game类添加DFS方法如下:
public void DFS() {
new Thread() {
@Override
public void run() {
boolean flag=true;
int[][] start={{source[0],source[1]},{source[0],source[1]}};
stack.push(start);
int count=0; //步数计数器
while(flag) {
int[][] currentEdge=stack.pop(); //从栈顶取出边
int[] tempTarget=currentEdge[1]; //取出此边的目的点
//判断目的点是否去过,若去过则直接进入下次循环
if(visited[tempTarget[1]][tempTarget[0]]==1) {
continue;
}
count++;
visited[tempTarget[1]][tempTarget[0]]=1;
searchProcess.add(currentEdge); //将临时目的点加入搜索过程中
hm.put(tempTarget[0]+":"+tempTarget[1],new int[][]{currentEdge[1],currentEdge[0]});
gameView.postInvalidate();
try {
Thread.sleep(timeSpan);
} catch (InterruptedException e) {
e.printStackTrace();
}
//判断是否找到目的点
if(tempTarget[0]==target[0]&&tempTarget[1]==target[1]) {
break;
}
//将所有可能的边入栈
int currCol=tempTarget[0];
int currRow=tempTarget[1];
for(int[] rc:sequence) {
int i=rc[1];
int j=rc[0];
if(i==0&&j==0) {continue;}
if(currRow+i>=0&&currRow+i<MapList.map[mapId].length&&currCol+j>=0
&&currCol+j<MapList.map[mapId][0].length
&&map[currRow+i][currCol+j]!=1
) {
int[][] tempEdge={{tempTarget[0],tempTarget[1]},{currCol+j,currRow+i}};
stack.push(tempEdge);
}
}
}
pathFlag=true;
gameView.postInvalidate();
Message msg1=myHandler.obtainMessage(1);
myHandler.sendMessage(msg1);
Message msg2=myHandler.obtainMessage(2,count);
myHandler.sendMessage(msg2);
}
}.start();
}
- 算法的开始之前将起始点到起始点作为第一条边入栈。
之后在Game类的runAlgorichm方法的switch代码块中添加几行代码来调用算法。
case 0:
DFS();
break;
此时运行项目,按开始按钮后目标0效果如图:
目标1效果如图:
- 其中细线为搜索过程,粗线为搜索结果。
5.广度优先路径搜索BFS
广度优先搜索是在游戏中使用较多的一种搜索算法,其基本思路是站在一个节点上,先将所有连接到该节点的节点访问到,然后再继续访问下一层,直到找到目标为止。
广度优先在编程方面与深度优先基本相同,只是深度优先使用的是栈储存待访问的边,而广度优先使用的是队列。
在Game类添加BFS方法如下:
public void BFS(){//广度优先算法
new Thread(){
public void run(){
int count=0;//步数计数器
boolean flag=true;
int[][] start={//开始状态
{source[0],source[1]},
{source[0],source[1]}
};
queue.offer(start);
while(flag){
int[][] currentEdge=queue.poll();//从队首取出边
int[] tempTarget=currentEdge[1];//取出此边的目的点
//判断目的点是否去过,若去过则直接进入下次循环
if(visited[tempTarget[1]][tempTarget[0]]==1){
continue;
}
count++;
visited[tempTarget[1]][tempTarget[0]]=1;//标识目的点为访问过
searchProcess.add(currentEdge);//将临时目的点加入搜索过程中
//记录此临时目的点的父节点
hm.put(tempTarget[0]+":"+tempTarget[1],
new int[][]{currentEdge[1],currentEdge[0]});
gameView.postInvalidate();
try{Thread.sleep(timeSpan);}catch(Exception e){e.printStackTrace();}
//判断有否找到目的点
if(tempTarget[0]==target[0]&&tempTarget[1]==target[1]){
break;
}
//将所有可能的边入队列
int currCol=tempTarget[0];
int currRow=tempTarget[1];
for(int[] rc:sequence){
int i=rc[1];
int j=rc[0];
if(i==0&&j==0){continue;}
if(currRow+i>=0&&currRow+i<MapList.map[mapId].length
&&currCol+j>=0&&currCol+j<MapList.map[mapId][0].length&&
map[currRow+i][currCol+j]!=1){
int[][] tempEdge={
{tempTarget[0],tempTarget[1]},
{currCol+j,currRow+i}
};
queue.offer(tempEdge);
}
}
}
pathFlag=true;
gameView.postInvalidate();
Message msg1 = myHandler.obtainMessage(1);
myHandler.sendMessage(msg1);//设置按钮的可用性
Message msg2 = myHandler.obtainMessage(2, count);
myHandler.sendMessage(msg2);//改变TextView文字
}
}.start();
}
之后在Game类的runAlgorichm方法的switch代码块中添加几行代码来调用算法。
case 1:
BFS();
break;
此时运行项目,按开始按钮后目标0效果如图:
目标3效果如图:
6.路径搜索算法——Dijkstra
Dijkstras算法时典型的最短路径算法,一般用于求出从一点触发到达另一点的最优路径,但是,因为其遍历运算的节点较多,所以运行效率较低。
在Game类添加Dijkstra方法如下:
public void Dijkstra(){//Dijkstra算法
new Thread(){
public void run(){
int count=0;//步数计数器
boolean flag=true;//搜索循环控制
int[] start={source[0],source[1]};//开始点col,row
visited[source[1]][source[0]]=1;
for(int[] rowcol:sequence){ //计算此点所有可以到达点的路径及长度
int trow=start[1]+rowcol[1];
int tcol=start[0]+rowcol[0];
if(trow<0||trow>18||tcol<0||tcol>18)continue;
if(map[trow][tcol]!=0)continue;
//记录路径长度
length[trow][tcol]=1;
//计算路径
String key=tcol+":"+trow;
ArrayList<int[][]> al=new ArrayList<int[][]>();
al.add(new int[][]{{start[0],start[1]},{tcol,trow}});
hmPath.put(key,al);
//将去过的点记录
searchProcess.add(new int[][]{{start[0],start[1]},{tcol,trow}});
count++;
}
gameView.postInvalidate();
outer:while(flag){
//找到当前扩展点K 要求扩展点K为从开始点到此点目前路径最短,且此点未考察过
int[] k=new int[2];
int minLen=9999;
for(int i=0;i<visited.length;i++){
for(int j=0;j<visited[0].length;j++){
if(visited[i][j]==0){
if(minLen>length[i][j]){
minLen=length[i][j];
k[0]=j;//col
k[1]=i;//row
}
}
}
}
visited[k[1]][k[0]]=1;//设置去过的点
gameView.postInvalidate();//重绘
int dk=length[k[1]][k[0]];//取出开始点到K的路径长度
ArrayList<int[][]> al=hmPath.get(k[0]+":"+k[1]);//取出开始点到K的路径
//循环计算所有K点能直接到的点到开始点的路径长度
for(int[] rowcol:sequence){
int trow=k[1]+rowcol[1];//计算出新的要计算的点的坐标
int tcol=k[0]+rowcol[0];
//若要计算的点超出地图边界或地图上此位置为障碍物则舍弃考察此点
if(trow<0||trow>MapList.map[mapId].length-1||tcol<0||tcol>MapList.map[mapId][0].length-1)continue;
if(map[trow][tcol]!=0)continue;
int dj=length[trow][tcol];//取出开始点到此点的路径长度
int dkPluskj=dk+1;//计算经K点到此点的路径长度
//若经K点到此点的路径长度比原来的小则修改到此点的路径
if(dj>dkPluskj){
String key=tcol+":"+trow;
//克隆开始点到K的路径
ArrayList<int[][]> tempal=(ArrayList<int[][]>)al.clone();
//将路径中加上一步从K到此点
tempal.add(new int[][]{{k[0],k[1]},{tcol,trow}});
//将此路径设置为从开始点到此点的路径
hmPath.put(key,tempal);
//修改到从开始点到此点的路径长度
length[trow][tcol]=dkPluskj;
//若此点从未计算过路径长度则将此点加入考察过程记录
if(dj==9999){//将去过的点记录
searchProcess.add(new int[][]{{k[0],k[1]},{tcol,trow}});
count++;
}
}
//看是否找到目的点
if(tcol==target[0]&&trow==target[1]){
pathFlag=true;
Message msg1 = myHandler.obtainMessage(1);
myHandler.sendMessage(msg1);//设置按钮的可用性
Message msg2 = myHandler.obtainMessage(2, count);
myHandler.sendMessage(msg2);//改变TextView文字
break outer;
}
}
try{Thread.sleep(timeSpan);}catch(Exception e){e.printStackTrace();}
}
}
}.start();
}
之后在Game类的runAlgorichm方法的switch代码块中添加几行代码来调用算法。
case 2:
Dijkstra();
break;
此时运行项目,按开始按钮后效果如图:
7.用A*算法优化搜索
A*算法时一种启发式搜索,所谓启发式搜索,就是利用一个启发因子评估每次寻找的路线的优劣,再决定往哪个节点走。实际上,A*只是一种思想,可以运用这种思想对之前实现的搜索算法进行优化。
之前开发出启发因子AStarComparator类,为优先级队列设置比较器AStarComparator即可。
7.1用A*算法优化广度优先算法
在Game类添加BFSAStar方法如下:
public void BFSAStar(){//广度优先 A*算法
new Thread(){
public void run(){
int count=0;//步数计数器
boolean flag=true;
int[][] start={//开始状态
{source[0],source[1]},
{source[0],source[1]}
};
astarQueue.offer(start);
while(flag){
int[][] currentEdge=astarQueue.poll();//从队首取出边
int[] tempTarget=currentEdge[1];//取出此边的目的点
//判断目的点是否去过,若去过则直接进入下次循环
if(visited[tempTarget[1]][tempTarget[0]]==1){
continue;
}
count++;
visited[tempTarget[1]][tempTarget[0]]=1;//标识目的点为访问过
searchProcess.add(currentEdge);//将临时目的点加入搜索过程中
//记录此临时目的点的父节点
hm.put(tempTarget[0]+":"+tempTarget[1],
new int[][]{currentEdge[1],currentEdge[0]});
gameView.postInvalidate();
try{Thread.sleep(timeSpan);}catch(Exception e){e.printStackTrace();}
//判断有否找到目的点
if(tempTarget[0]==target[0]&&tempTarget[1]==target[1]){
break;
}
//将所有可能的边入队列
int currCol=tempTarget[0];
int currRow=tempTarget[1];
for(int[] rc:sequence){
int i=rc[1];
int j=rc[0];
if(i==0&&j==0){continue;}
if(currRow+i>=0&&currRow+i<MapList.map[mapId].length
&&currCol+j>=0&&currCol+j<MapList.map[mapId][0].length&&
map[currRow+i][currCol+j]!=1){
int[][] tempEdge={
{tempTarget[0],tempTarget[1]},
{currCol+j,currRow+i}
};
astarQueue.offer(tempEdge);
}
}
}
pathFlag=true;
gameView.postInvalidate();
Message msg1 = myHandler.obtainMessage(1);
myHandler.sendMessage(msg1);//设置按钮的可用性
Message msg2 = myHandler.obtainMessage(2, count);
myHandler.sendMessage(msg2);//改变TextView文字
}
}.start();
}
- 运用A*算法对广度优先算法优化很简单,只需要将广度优先中使用的广度优先队列改为A*优先级队列即可。
之后在Game类的runAlgorichm方法的switch代码块中添加几行代码来调用算法。
case 2:
BFSAStar();
break;
此时运行项目,效果如图:
- 用A*优化的广度优先搜索算法得到的路径基本上是最优路径,但是并不能保证一定是最优路径,但执行效率较高,所以在游戏中应用较多。
7.1用A*算法优化Dijkstra算法
在Game类添加DijkstraAStar方法如下:
public void DijkstraAStar(){//Dijkstra A*算法
new Thread(){
public void run(){
int count=0;//步数计数器
boolean flag=true;//搜索循环控制
int[] start={source[0],source[1]};//开始点col,row
visited[source[1]][source[0]]=1;
for(int[] rowcol:sequence){ //计算此点所有可以到达点的路径及长度
int trow=start[1]+rowcol[1];
int tcol=start[0]+rowcol[0];
if(trow<0||trow>18||tcol<0||tcol>18)continue;
if(map[trow][tcol]!=0)continue;
//记录路径长度
length[trow][tcol]=1;
//计算路径
String key=tcol+":"+trow;
ArrayList<int[][]> al=new ArrayList<int[][]>();
al.add(new int[][]{{start[0],start[1]},{tcol,trow}});
hmPath.put(key,al);
//将去过的点记录
searchProcess.add(new int[][]{{start[0],start[1]},{tcol,trow}});
count++;
}
gameView.postInvalidate();
outer:while(flag){
//找到当前扩展点K 要求扩展点K为从开始点到此点目前路径最短,且此点未考察过
int[] k=new int[2];
int minLen=9999;
boolean iniFlag=true;
for(int i=0;i<visited.length;i++){
for(int j=0;j<visited[0].length;j++){
if(visited[i][j]==0){
//与普通Dijkstra算法的区别部分=========begin=================================
if(length[i][j]!=9999){
if(iniFlag){//第一个找到的可能点
minLen=length[i][j]+
(int)Math.sqrt((j-target[0])*(j-target[0])+(i-target[1])*(i-target[1]));
k[0]=j;//col
k[1]=i;//row
iniFlag=!iniFlag;
}
else{
int tempLen=length[i][j]+
(int)Math.sqrt((j-target[0])*(j-target[0])+(i-target[1])*(i-target[1]));
if(minLen>tempLen){
minLen=tempLen;
k[0]=j;//col
k[1]=i;//row
}
}
}
//与普通Dijkstra算法的区别部分==========end==================================
}
}
}
visited[k[1]][k[0]]=1;//设置去过的点
gameView.postInvalidate();//重绘
int dk=length[k[1]][k[0]];//取出开始点到K的路径长度
ArrayList<int[][]> al=hmPath.get(k[0]+":"+k[1]);//取出开始点到K的路径
//循环计算所有K点能直接到的点到开始点的路径长度
for(int[] rowcol:sequence){
int trow=k[1]+rowcol[1];//计算出新的要计算的点的坐标
int tcol=k[0]+rowcol[0];
//若要计算的点超出地图边界或地图上此位置为障碍物则舍弃考察此点
if(trow<0||trow>MapList.map[mapId].length-1||tcol<0||tcol>MapList.map[mapId][0].length-1)continue;
if(map[trow][tcol]!=0)continue;
int dj=length[trow][tcol];//取出开始点到此点的路径长度
int dkPluskj=dk+1;//计算经K点到此点的路径长度
//若经K点到此点的路径长度比原来的小则修改到此点的路径
if(dj>dkPluskj){
String key=tcol+":"+trow;
//克隆开始点到K的路径
ArrayList<int[][]> tempal=(ArrayList<int[][]>)al.clone();
//将路径中加上一步从K到此点
tempal.add(new int[][]{{k[0],k[1]},{tcol,trow}});
//将此路径设置为从开始点到此点的路径
hmPath.put(key,tempal);
//修改到从开始点到此点的路径长度
length[trow][tcol]=dkPluskj;
//若此点从未计算过路径长度则将此点加入考察过程记录
if(dj==9999){//将去过的点记录
searchProcess.add(new int[][]{{k[0],k[1]},{tcol,trow}});
count++;
}
}
//看是否找到目的点
if(tcol==target[0]&&trow==target[1]){
pathFlag=true;
Message msg1 = myHandler.obtainMessage(1);
myHandler.sendMessage(msg1);//设置按钮的可用性
Message msg2 = myHandler.obtainMessage(2, count);
myHandler.sendMessage(msg2);//改变TextView文字
break outer;
}
}
try{Thread.sleep(timeSpan);}catch(Exception e){e.printStackTrace();}
}
}
}.start();
}
之后在Game类的runAlgorichm方法的switch代码块中添加几行代码来调用算法。
case 4:
DijkstraAStar();
break;
此时运行项目,效果如图:
- 用A*优化的Dijkstra搜索算法得到的路径一定是最优路径,在真正的游戏开发中,应该该算法的次数很多。
8.四向走法
如果游戏中是四向走法而不是八向走法,只需将Game类中的成员变量sequence去掉四个斜方向。
即由
int[][] sequence={
{0,1},{0,-1},
{-1,0},{1,0},
{-1,1},{-1,-1},
{1,-1},{1,1}
};
改为
int[][] sequence={
{0,1},{0,-1},
{-1,0},{1,0},
};
此时两种优化算法运行效果如图: