android 游戏:俄罗斯方块的小结和开发过程
最近觉得在android上开发游戏是一件很有趣的事情,所以查找了一些资料,自己做出了一个简单俄罗斯方块游戏,现将其总结如下:
1、基础数据模型
一个俄罗斯方块都是以一个4*4的二维数组来存储的,在我的demo游戏中,一共有7种方块类型: I S Z J O L T七种类型。见下图:
每种类型都有四种旋转状态, 如何在一个4*4的二位数组表示呢?可以用0和1来表示,1表示该小单元格需要显示,0表示该小单元格不需要显示。
设置屏幕为320*480,每个小单元格是20像素,这样宽就是COLS=16,高就是ROWS=24。
2、涉及到的几个JavaBean
2.1 Shape:把每种图形抽象出来为一个对象,具有的属性是
int的 left(x方向)和top(y方向)、
int的 status 旋转状态,可选择的值是0,1,2,3 默认是0
int[][] body 存储四种旋转状态的数据
int LEFT = 1, RIGHT = 2, DOWN = 3, ROTATE = 4
Shape具有的方法是moveLeft() moveRight() moveDown()和rotate()四种基本方法,对应模拟器上的四个方向键。如果模拟器的方向键不能使用,自己在百度或google上看看是怎么解决的,这里不记录。
同时Shape还应该具有drawMe()画出值为1的单元格、checkBound()检查是否出了边界 和 checkValue(x, y)检查对应的小单元格的值是否为1或0, 1显示0不显示。
2.2 Ground:地面类 就是各个小单元格堆放起来的那个地面,我们也把它抽象成为一个JavaBean,经过分析,它也应该具有如下的属性和方法。
int[][] body 这个body就是面板的最大面积COLS * ROWS =16*24
它应该有一个drawMe()的方法,显示他目前已经接纳且未消行的小单元格,另外它应该有消行和统计分数的方法removeRows(),检查某个小单元格是否为1的方法checkValue(),接纳Shape的方法addShape()。
2.3 定义一个GameView,用来显示Shape和Ground的图形变化,需要重写的方法有
onDraw():主要是画Shape和Ground
onKeyDown():处理方向键按下的事件
在它的构造函数里面需要实现小单元格的静态资源的获取,
调用setFocusable(true); 以显示图形
在正式运行时需要执行定时器,以3秒钟不停刷新页面的数据。
2.4 MainActivity 将系统的标题栏去掉,调用
//以下去掉标题和全屏
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
然后调用GameView。
===================================================================
按照以上的步骤,我开始逐步编程
1、ShapeFactory的代码
import java.util.Random; public class ShapeFactory { public static Shape getShape(){ Shape shape = new Shape(); int type = new Random().nextInt(shapes.length); shape.setBody(shapes[type]); shape.setStatus(0); return shape; } // 方块的形状 第一组代表方块类型有S、Z、L、J、I、O、T 7种 // 第二组为图型的型状态 //每三组图型数据,根据这些数来描绘在view中 private final static int shapes[][][] = new int[][][] { // i { { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0 }, { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0 } }, // s { { 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }, { 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 } }, // z { { 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }, { 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 } }, // J { { 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }, { 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, // o(田字) { { 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, // l(竖右勾) { { 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0 }, { 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }, { 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, // T { { 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }, { 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0 } } }; }
配置文件
public class Config { public static final int CELL_SIZE = 20; //320*480 宽 *高 public static final int ROWS = 24;//行--高 public static final int COLS = 16;//列 --宽 }
2、Shape的代码
import android.graphics.Canvas; import android.graphics.drawable.Drawable; public class Shape { private int left; private int top; private int[][] body; private int status; public static final int LEFT = 1, RIGHT = 2, DOWN = 3, ROTATE = 4; public void moveLeft(){ left--; } public void moveRight(){ left++; } //move down //must controller multiple thread(1-refresh schedule, 2-event) public synchronized void moveDown(Ground ground) { if (checkBound(Shape.DOWN, ground)) top ++; } //旋转其实就是转到下一个图形中,通过改变status的值即可 public void rotate(){ status = (status+1)%body.length; } /**绘制Shape需要传入画布Canvas和图片资源**/ public void drawMe(Canvas canvas, Drawable d){ for(int x=0; x<4; x++){ for(int y=0; y<4; y++){ if(checkValue(x,y)){//如果单元格的值不为0则为1,需要绘制单元格 //绘制之前需要先设置边界,设置边界有两种方法,这里选择四个参数的 //这四个参数需要计算出其绝对坐标值 int tempx = (x+left)*Config.CELL_SIZE; int tempy = (y+top)*Config.CELL_SIZE; int right = tempx+Config.CELL_SIZE; int bottom = tempy+Config.CELL_SIZE; d.setBounds(tempx, tempy, right, bottom); d.draw(canvas); } } } } //检查Shape是否到达边界,这里需要考虑向左 向右和向下三种情况 public boolean checkBound(int action, Ground ground){ int temptop = top; int templeft = left; switch(action){ case LEFT: templeft--; break; case RIGHT: templeft++; break; case DOWN: temptop++; break; case ROTATE: break; } for(int x=0; x<4; x++){ for(int y=0; y<4; y++){ if(checkValue(x,y)){//如果单元格的值为1,需要判断它是否到达边界 if(templeft<0 || templeft >Config.COLS //X方向的边界判断 || temptop > Config.ROWS //Y方向的边界判断 || ground.checkValue(templeft,temptop,x,y) ){ return false; }//end if } } }//end for return true; } //判断x,y点的值是否为1,1返回true 0返回false public boolean checkValue(int x, int y){ return body[status][4*y+x]==1; } public int[][] getBody() { return body; } public void setBody(int[][] body) { this.body = body; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; }
3、 Ground的代码
import android.graphics.Canvas; import android.graphics.drawable.Drawable; public class Ground { public boolean checkValue(int left, int top, int x, int y) { if (body[left + x][top + y] == 1) { return true; } return false; } public int[][] body = new int[Config.COLS][Config.ROWS]; public void drawMe(Canvas canvas, Drawable d) { for (int x = 0; x < Config.COLS; x++) {//列 --> X方向 for (int y = 0; y < Config.ROWS; y++) {//行--> Y方向 if (body[x][y] == 1) { int tempX = x * Config.CELL_SIZE; int tempY = y * Config.CELL_SIZE; int right = tempX + Config.CELL_SIZE; int bottom = tempY + Config.CELL_SIZE; d.setBounds(tempX, tempY, right, bottom); d.draw(canvas); } } } } /** * remember drawed block * @param type block type(seven type) * @param status block status * @param left x coordinate * @param top y coordinate */ public void add(int left, int top, Shape shape) { for (int x = 0; x < 4; x++) { for (int y = 0; y < 4; y++) { if (shape.checkValue(x, y)){ try{ body[left + x][top + y] = 1; } catch(Exception e) { e.printStackTrace(); }//end try-catch }//end if }//end for loop }//end for loop } /** * remove rows 当所堆积的小框框满一行时,就要进行消行。 * 消行的逻辑是:以Y方向为基准,从最底下一行的开始检查,当某一行全部满格时,则进行消行 * 消行其实就是把上一行的数据填充到下一行的位置上,以此进行循环操作 */ public void removeRow() {//消掉多少行 int c = 0; int removeRows = 0;//counter for (int y = Config.ROWS - 1; y > 0; y--) {//y coordinate for (int x = 0; x < Config.COLS; x++) {//x coordinate if (body[x][y] == 1){ c++; if (c == Config.COLS) {//should remove the row removeRows++; for (int j = y; j > 0; j--) {//reset all block in body y coordinate for (int z = 0; z < Config.COLS; z++) {//x coordinate body[z][j] = body[z][j - 1]; }//end for loop }//end for loop y++;//because the row has fall down one row. }//end if }//end if }//end for c = 0; }//end for countScore(removeRows); } /** * count score统计分数 * @param removeRows */ public void countScore(int removeRows) { } }
4、GameView
import java.util.Timer; import java.util.TimerTask; import android.content.Context; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.view.KeyEvent; import android.view.View; public class GameView extends View { private Shape shape; private Drawable d; Ground ground = new Ground(); public GameView(Context context) { super(context); d = context.getResources().getDrawable(R.drawable.brick); shape = ShapeFactory.getShape(); //new Thread(this).start(); setFocusable(true); schedule(); } /** * onDraw()方法只会在初始化后被调用一次,或者在使用postInvalidate()的时候会被调用 */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); shape.drawMe(canvas, d); ground.drawMe(canvas, d); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: shape.rotate(); break; case KeyEvent.KEYCODE_DPAD_DOWN: if(shape.checkBound(Shape.DOWN, ground)) shape.moveDown(ground); break; case KeyEvent.KEYCODE_DPAD_LEFT: if(shape.checkBound(Shape.LEFT,ground)) shape.moveLeft(); break; case KeyEvent.KEYCODE_DPAD_RIGHT: if(shape.checkBound(Shape.RIGHT, ground)) shape.moveRight(); break; default: break; } return super.onKeyDown(keyCode, event); } private void schedule(){ Timer nT1 = new Timer(); nT1.schedule(new TimerTask(){ //计划运行时间间隔 public void run(){ refresh(); //过3秒调用一下refresh() } },0,500); } public void refresh(){ //auto down if (shape.checkBound(Shape.DOWN, ground)){ shape.moveDown(ground); this.postInvalidate(); //使用postInvalidate();刷新 } else{//generator new block ground.add(shape.left, shape.top, shape); ground.removeRow(); shape = ShapeFactory.getShape(); } } /*@Override public void run() { while(true){ if(shape.checkBound(Shape.DOWN)) shape.moveDown(); postInvalidate(); try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } } }*/ }
6、MainActivity
import android.app.Activity; import android.os.Bundle; import android.view.Window; import android.view.WindowManager; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //setContentView(R.layout.activity_main); //以下去掉标题和全屏 requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); GameView gameView = new GameView(this); setContentView(gameView); } }
6、