前言
如下图所示,这篇文章要完成的就是这个简单的示例,后续会继续添加上动画和声音。这里主要包含了游戏的一些简单元素和逻辑。
在我的多次尝试后发现想赢它还是挺难的……毕竟它的走法不是简简单单的Random而已。
代码已经上传至Github,建议大家直接Fork而不是Download,毕竟开源的意义在于彼此分享代码,而且这个太简单了,后续肯定还会继续更新的,所以……
游戏背景元素的定义
由于代码还会继续更新,所以博客中只是简单的介绍一下,而且我都写好了注释。
如大家所见的,背景中有许多的点,这里就定义一个Dot类即可。
关于Dot类,它显然应该有坐标X、Y,还有一个状态,那么这个状态具体有哪些呢?
1,点击后开启路障,这里设为红色
2,未点击时路障为关闭状态,这里设为灰色
3,神经猫所在的位置自然也应该是一个状态,毕竟这个位置玩家不能再去点击了
然后设置相应的构造函数。
package myapplication.nomasp.com.catchcrazycat;
/**
* Created by nomasp on 2015/09/25.
*/
public class Dot {
int x, y;
int status; // 灰色:猫可以走,红色:以及被设置为路障,橘色:猫的位置
public static final int STATUS_ON = 1; // 开启路障(红色)
public static final int STATUS_OFF = 0; // 关闭路障(灰色)
public static final int STATUS_IN = 9; // 神经猫的位置
public Dot(int x, int y) {
super();
this.x = x;
this.y = y;
status = STATUS_OFF;
}
public void setXY(int x, int y){
this.x=x;
this.y=y;
}
public void setX(int x) {
this.x = x;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
}
初始化游戏
游戏的背景等我们全都放在Playground类中。
在初始化游戏之前,我们首先需要定义一个Dot类型的二维数组用于保存这些游戏中的原点,我这里就称之为游戏元素好了。
然后还需要为神经猫实例化,它本质上也是Dot类型的。
private Dot matrix[][]; // 声明数组matrix来保存游戏元素
private Dot cat; // 猫
当然了,行与列,还有路障,这些都可以设置为static且final的,如果你希望在游戏中设置不同的难度对应不同的行列和路障后续可以再修改。
private static final int COL = 10; // 每列有10个元素
private static final int ROW = 10; // 每行有10个元素
private static final int BLOCKS = 15; // 默认添加的路障数量
下面就是初始化游戏的方法,最后的输出是测试使用的,为了方便我这种初学者的学习也就没有删掉。
private void initGame(){
for (int i = 0; i < ROW; i++){
for (int j = 0; j < COL; j++){
matrix[i][j].setStatus(Dot.STATUS_OFF);
}
}
cat = new Dot(ROW/2,COL/2); // 初始化猫的位置
getDot(ROW / 2, COL / 2).setStatus(Dot.STATUS_IN); // 初始化猫所在位置的状态
for(int i = 0; i < BLOCKS;){
int x = (int)((Math.random()*1000)%COL); // x为横坐标,数组中的列
int y = (int)((Math.random()*1000)%ROW); // y为纵坐标,数组中的行
if(getDot(x,y).getStatus() == Dot.STATUS_OFF){ // 如果当前Dot的状态为OFF
getDot(x,y).setStatus(Dot.STATUS_ON); // 则将其打开,并让i自增
i++;
//System.out.println("BLOCKS+"+i);
}
}
}
元素的宽度这里也可以一并声明好:
private static int WIDTH;
关于宽度的设置有两种方法:
方法一就是在MainActivity类中获取屏幕的分辨率。
public static int screenWidth = 0;
// 使用Playground类中提到的根据屏幕设置元素宽度的方法一
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
screenWidth = metrics.widthPixels;
然后在Playground类中进行设置。
private static final int WIDTH = (int)(MainActivity.screenWidth*0.8)/10;
方法二到下文中继续介绍。
重绘、回调以及触控
首先为Playground类扩展SurfaceView以及实现OnTouchListener。
下面就是重绘背景的方法了。记得为paint设置抗锯齿,后面根据不同状态的Dot设置不同的颜色,最后在drawOval方法类实例化一个RectF即可。
private void redraw(){
Canvas c = getHolder().lockCanvas(); // 先上锁
c.drawColor(Color.LTGRAY); // 为canvas设置为青色
Paint paint = new Paint(); // 开始绘制到屏幕上
paint.setFlags(Paint.ANTI_ALIAS_FLAG); // 抗锯齿
for (int i = 0; i < ROW; i++){
int offset = 0; // 设置偏移量
if(i%2 != 0){ // i为奇数表示是第2,4,6……行(索引为1,3,5……)
offset = WIDTH/2; // 偏移量为元素的宽度的一半
}
for (int j = 0; j < COL; j++){
Dot one = getDot(j,i);
switch (one.getStatus()){
case Dot.STATUS_OFF:
paint.setColor(0xFFEEEEEE);
break;
case Dot.STATUS_IN:
paint.setColor(0xFFFF0000);
break;
case Dot.STATUS_ON:
paint.setColor(0xFFFFAA00);
break;
default:
break;
}
c.drawOval(new RectF(one.getX()*WIDTH+offset,
one.getY()*WIDTH,
(one.getX()+1)*WIDTH+offset,
(one.getY()+1)*WIDTH),paint); // 大小由屏幕宽度决定
}
}
getHolder().unlockCanvasAndPost(c); // 解锁
}
然后就可以对它进行回调了。看,这里就是前文中所说的设置WIDTH的第二种方法,巧妙的利用了surfaceChanged中传入的width参数。
Callback callback = new Callback() { // 回调方法
@Override
public void surfaceCreated(SurfaceHolder holder) {
redraw(); // 调用redraw进行重绘
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
WIDTH = width/(COL+1);
redraw(); // 修改宽度后进行重绘
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
};
以下就开始写触控个功能了,直接重载onTouch方法即可。
注释已经写的非常详细了,和上面一样,测试代码为了学习方面也保留了。
@Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_UP){
//Toast.makeText(getContext(),event.getX()+":"+ event.getY(),
// Toast.LENGTH_SHORT).show();
int x,y; // 触摸的X、Y坐标
y = (int)(event.getY()/WIDTH);
if(y%2 == 0){ // 表示第1、3、5……行元素(索引为0、2、4……)
x = (int)(event.getX()/WIDTH);
}else{
x = (int)((event.getX()-WIDTH/2)/WIDTH); // 事件获取的X坐标减去左边空余的WIDTH/2部分,再除以WIDTH
}
// 以下代码对坐标进行保护,如果没有这些代码当点击位置超出坐标范围后也就是超出了数组索引
if(x+1 > COL || y+1 > ROW){ // 超出边界,当前点击无效
// 以下为测试代码
// System.out.println("------------------------");
// getNrighbour(cat,k).setStatus(Dot.STATUS_IN);
// k++;
// redraw();
initGame();
// 以下为测试代码
// System.out.println("------------------------");
// for(int i = 1;i < 7; i++){
// System.out.println(i+"@"+getDistance(cat,i));
} else if(getDot(x,y).getStatus() == Dot.STATUS_OFF){ // 当这个点可用时
getDot(x,y).setStatus(Dot.STATUS_ON); // 点击有效,开启路障状态
move();
}
redraw(); // 重绘
}
return true;
}
噢对了,前面用到的getDot方法还没有说,这是由于上面的X、Y是触控事件的X、Y坐标,但对应到数组中X就成了列,Y则成了行,而二维数组是行在前、列在后的,所以就为了方便起见就特意设置了一个方法。
private Dot getDot(int x, int y){ // 传入x,y返回matrix[y][x]
return matrix[y][x];
}
当然,你也可以直接写成matrix[y][x]。
下面这个其实是一开始就写的,只是其中用到的一些东西是逐个补充上去,所以这里放在了后面。
public Playground(Context context) {
super(context);
getHolder().addCallback(callback); // 为getHolder设置回调
matrix = new Dot[ROW][COL]; // 初始化
for(int i = 0; i < ROW; i++){
for(int j = 0; j < COL; j++){
matrix[i][j]=new Dot(j,i); // x表示行,y表示列,因此和数组的COL/ROW是错开的
}
}
setOnTouchListener(this); // 该类已经实现了OnTouchListener接口,因此只用传入this即可
initGame();
}
获取距离等一些功能性方法
一
首先,游戏需要判断神经猫等是否处于边缘部分,作用不言而喻。它自然需要的是一个Dot类型的参数。
private boolean isAtEdge(Dot d){
if(d.getX()*d.getY() == 0 || d.getX()+1 == COL || d.getY()+1 == ROW){ // 此时处于游戏边界
return true;
}
return false;
}
二
接下来这个就要好好理一理关系了,有了下面的图片示例就很好理解了,按顺时针计算就好。
private Dot getNeighbor(Dot d, int dir){ // 从水平相连的左侧点开始为1,依次顺时针计数
switch (dir){
case 1:
return getDot(d.getX()-1,d.getY());
case 2:
if(d.getY()%2 == 0){
return getDot(d.getX()-1,d.getY()-1);
}else{
return getDot(d.getX(),d.getY()-1);
}
case 3:
if(d.getY()%2 == 0){
return getDot(d.getX(),d.getY()-1);
}else{
return getDot(d.getX()+1,d.getY()-1);
}
case 4:
return getDot(d.getX()+1,d.getY());
case 5:
if(d.getY()%2 == 0){
return getDot(d.getX(),d.getY()+1);
}else{
return getDot(d.getX()+1,d.getY()+1);
}
case 6:
if(d.getY()%2 == 0){
return getDot(d.getX()-1, d.getY()+1);
}else{
return getDot(d.getX(),d.getY()+1);
}
default:
return null;
}
}
三
以下这个方法用于计算距离:
如果距离某个方向路障中间有2个灰色(OFF)则返回-2;
如果紧挨该方向的路障,则返回0;
如果该方向没有路障,则返回至边界中间的灰色元素个数,为正数。
private int getDistance(Dot d, int dir){
int distance = 0;
if(isAtEdge(d)){ // 如果该点已经在屏幕边缘,那么就不必继续判断直接返回distance即可
return 1;
}
Dot ori = d,next;
while (true) {
next = getNrighbour(ori,dir); // 将当前点d的某方向的邻居赋值给next
if(next.getStatus() == Dot.STATUS_ON){ // 碰到了路障,返回0或负数
return distance*-1;
}
if(isAtEdge(next)){ // 抵达了场景边缘,返回正数
distance++; // 说明下一个点也是可用的
return distance;
}
distance++; // 距离自增
ori = next; // 将next设置为当前参考点
}
}
胜负已分
由于这只是一个简单的Demo,所以胜负就用Toast通知好了,具体的后续继续完善,也欢迎大家贡献代码。
private void lose(){ // 游戏失败
Toast.makeText(getContext(),"Lose",Toast.LENGTH_SHORT).show();
}
private void win(){ // 游戏成功
Toast.makeText(getContext(),"Win",Toast.LENGTH_SHORT).show();
}
负责神经猫移动的方法
好吧,我承认移动这一部分正在写代码的时候顺序不在最后的,这是这里为重点就放在最后介绍了。
按功能分为两步,移动以及移动到,当然了move中肯定是要调用MoveTo的,只不过后者侧重于神经猫的动作,前者侧重于神经猫的走法。
private void MoveTo(Dot d){ // 移动神经猫到某个点
d.setStatus(Dot.STATUS_IN);
getDot(cat.getX(),cat.getY()).setStatus(Dot.STATUS_OFF);
cat.setXY(d.getX(),d.getY());
}
下面就来写move方法,为了方面学习,这里按步骤进行。
1,判断神经猫是否处于边界,是则游戏失败,不进行move,直接return。
if(isAtEdge(cat)){ // 判断猫是否在场景边界
lose(); // 游戏失败
return;
}
而后判断周围的6个元素是否可以走(简单的说就是:是否可用)。
for(int i = 1; i < 7; i++){
Dot n = getNeighbor(cat, i);
if(n.getStatus() == Dot.STATUS_OFF){ // 如果这邻居(点)可用
avaliable.add(n); // 将其添加到avaliable中
length.put(n,i); // i为方向
if(getDistance(n,i) > 0){ // 正数表示可以到达边界
positive.add(n);
}
}
}
上面实例化了一个n出来,当然你也可以直接写成:
if(getNeighbor(cat,i).getStatus() == Dot.STATUS_OFF)
如果它的邻居的状态是关闭的,那么也就意味着这一点是可用的,于是添加到avaliable中。在for循环之前就应该定义好它们:
Vector<Dot> avaliable = new Vector<>(); // 当前点周围6个点中的可用点
Vector<Dot> positive = new Vector<>(); // 当前点到边界的距离点
HashMap<Dot,Integer> length = new HashMap<Dot,Integer>();// 搭配positive,其用于记录方向
如果没有可用点,就意味着已经成功围住了神经猫;如果只有一个可用点,就直接移动过去就好了。
if(avaliable.size() == 0){ // 没有可用点
win(); // 成功围住神经猫
}else if(avaliable.size() == 1) { // 只有一个可用点
MoveTo(avaliable.get(0));
}
那么最关键的部分来了,如果有多条路可以走怎么办?
else{ // 既不是第一次点击,且有多条路可走
// 根据到边界edge的距离distance(包括路障等的计算)来决定走的方向
Dot best = null; // 最终决定要移动到的元素(点)
if(positive.size() != 0){ // 含有可到达边界的方向
System.out.println("向前进");
int min = 999; // 用于记录到达边界的最小值,初始值为一个较大值
for(int i = 0; i < positive.size(); i++){
int tempDis = getDistance(positive.get(i),length.get(positive.get(i)));
if(tempDis < min) {
min = tempDis;
best = positive.get(i);
}
}
}else{ // 所有方向都有路障
System.out.println("躲路障");
int max = 0; // 所有方向都有路障时,距离要么为负数要么为0
for(int i = 0; i < avaliable.size(); i++){
int tempDis = getDistance(avaliable.get(i),length.get(avaliable.get(i)));
if(tempDis < max) { // 因为tempDis是负数,所以用小于号
max = tempDis;
best = avaliable.get(i);
}
}
}
如果各个方向都没有路障,就选择路径最短的一个,如果有路障就要躲避这些路障。这里分别用min和max来记录最佳的路径,最后就将其赋值给best:
best = avaliable.get(i);
为了防止best为空时不知道怎么走,就为它设置一个随机路径好了。
if(best == null) { // 当best为空时,随机一个路径
int s = (int) ((Math.random() * 1000) % avaliable.size());
MoveTo(avaliable.get(s));
}else{
MoveTo(best); // 移动到最合适的一点
}
欢迎大家pull request
我都会上传到Github上,且都会有注释,为了帮助到更多人,欢迎大家Fork和Star,更欢迎大家pull request,地址为:传送门。