最近觉得在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、