刚开始接触Android平台,之前也没有游戏开发经验,因此对于如何开发一款游戏没有思路,而且也不知道如何对整个项目进行模块划分。在学习连连看的教程时,略作修改,实现一个非常简单的小游戏,这里记录下我的整个思路,以此向我的处女作致敬
游戏规则:点击开始按钮,游戏开始;然后从1开始依次点击界面上的数字,本游戏中设计的最大数为50,当所有的数字都被点击完毕后,游戏结束。
用于显示游戏主界面的GameView,作为整个游戏的交互界面
游戏逻辑处理模块,用于人机处理交互信息(如点击屏幕上的某一方块时,会出现什么情况)GameService
配置信息GameConf,初始化模块Board
1. 界面布局activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:background="@drawable/bg"
tools:context=".MainActivity" >
<hust.wzb.view.GameView
android:id="@+id/gameView"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_marginTop="380dp"
android:gravity="center"
android:orientation="horizontal" >
<Button
android:id="@+id/startButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_selector" />
<TextView
android:id="@+id/timeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp"
android:width="150dp" />
</LinearLayout>
</RelativeLayout>
在界面上,主要有三个空间,自定义的GameView,用于开始游戏的StartButton,记录时间的TimeTextView
2. 游戏的基本配置信息 GameConf
这里包含了游戏中用到的基本参数,如每个方块的宽高,第一个方块的坐标等
package hust.wzb;
import android.content.Context;
public class GameConf {
// 每个方块的图片高与宽
public static final int PIECE_WIDTH = 80;
public static final int PIECE_HEIGHT = 80;
// Integer[][] 数组第一维的长度
private int xSize;
// Integer[][] 数组第二维的长度
private int ySize;
// Board中第一张图片出现的x,y坐标
private int beginX;
private int beginY;
private Context context;
public GameConf(int xSize, int ySize, int beginX, int beginY,
Context context) {
this.xSize = xSize;
this.ySize = ySize;
this.beginX = beginX;
this.beginY = beginY;
this.context = context;
}
public int getXSize() {
return xSize;
}
public int getYSize(){
return ySize;
}
public int getbeginX(){
return beginX;
}
public int getbeginY(){
return beginY;
}
public Context getContext(){
return context;
}
}
3. 将每个方块封装为Piece类,其中包含了改方块的值,坐标,在二维数组中的索引等信息
package hust.wzb.view;
import hust.wzb.GameConf;
import android.graphics.Point;
public class Piece {
private int value;
private int beginX;
private int beginY;
private int indexX;
private int indexY;
public Piece(){
}
// 设置Piece在数组中的位置
public Piece(int indexX, int indexY, int value) {
this.indexX = indexX;
this.indexY = indexY;
this.value = value;
}
public int getValue(){
return this.value;
}
public void setValue(int value){
this.value = value;
}
public int getBeginX() {
return beginX;
}
public void setBeginX(int beginX) {
this.beginX = beginX;
}
public int getBeginY() {
return beginY;
}
public void setBeginY(int beginY) {
this.beginY = beginY;
}
public int getIndexX() {
return this.indexX;
}
public void setIndexX(int indexX) {
this.indexX = indexX;
}
public int getIndexY() {
return indexY;
}
public void setIndexY(int indexY) {
this.indexY = indexY;
}
// 获取该Piece的中心
public Point getCenter() {
int x = GameConf.PIECE_WIDTH / 2 + getBeginX();
int y = GameConf.PIECE_HEIGHT / 2 + getBeginY();
return new Point(x, y);
}
}
3. GameView详细介绍
a. 必要的变量
gameService; // 游戏的逻辑实现类
private GameService gameService; // 游戏逻辑实现类
private GameConf config; // 游戏配置环境
private int lastNumber; // 上一个选中的方块,用于后面的判断
private Paint paint; // 画笔
public GameView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
lastNumber = 0;
paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(3);
paint.setTextSize(23);
}
b. gameView 作为主要的控件,首先需要实现其重绘onDraw()方法,用于在上面绘制图形
首先是通过gameService获得剩余的Piece[][],然后将所有的Piece绘制在界面上
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (this.gameService == null) {
return;
}
Piece[][] pieces = gameService.getPieces();
if (pieces != null) {
for (int i = 0; i < pieces.length; i++) {
for (int j = 0; j < pieces[0].length; j++) {
Piece temp = pieces[i][j];
if (temp != null) {
canvas.drawText(temp.getValue() + "",
temp.getBeginX() + 25, temp.getBeginY() + 40,
paint);
}
}
}
// 7条横线
for (int i = 0, dy = 0; i <= config.getXSize(); i++) {
canvas.drawLine(
config.getbeginX(),
config.getbeginY() + dy,
config.getYSize() * GameConf.PIECE_WIDTH
+ config.getbeginX(), config.getbeginY() + dy,
paint);
dy += GameConf.PIECE_HEIGHT;
}
// 6条竖线
for (int j = 0, dx = 0; j <= config.getYSize(); j++) {
canvas.drawLine(config.getbeginX() + dx, config.getbeginY(),
config.getbeginX() + dx, config.getbeginY()
+ (1 + config.getYSize())
* GameConf.PIECE_HEIGHT, paint);
dx += GameConf.PIECE_WIDTH;
}
}
}
c. 开始游戏方法 startGame();
public void startGame() {
this.gameService.start();
this.postInvalidate();
}
4. 初始化数据GameBoard
初始化Piece[][]数组,这里利用到了Collections.shuffle(list)方法实现打乱排列的功能
package hust.wzb.service;
import hust.wzb.GameConf;
import hust.wzb.view.Piece;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class GameBoard {
public Piece[][] create(GameConf config){
Piece[][] pieces = new Piece[config.getXSize()][config.getYSize()];
List<Integer> nums = getRandomInt(30);
int pieceWidth = GameConf.PIECE_WIDTH;
int pieceHeight = GameConf.PIECE_HEIGHT;
for(int i = 0; i < config.getXSize(); i++)
for(int j = 0 ; j < config.getYSize(); j++){
Piece piece = new Piece(i, j, nums.get(i * config.getYSize() + j));
piece.setBeginX(config.getbeginX() + j * pieceWidth);
piece.setBeginY(config.getbeginY() + i * pieceHeight);
pieces[i][j] = piece;
}
return pieces;
}
public List<Integer> getRandomInt(int max){
List<Integer> nums = new ArrayList<Integer>();
for(int i = 1; i <= max; i ++){
nums.add(i);
}
Collections.shuffle(nums);
return nums;
}
}
5. GameService,逻辑处理
主要的处理是piece[][]数组的更新,以及游戏的启动(初始化数据)
然后有一个根据触屏点击的坐标来获取对应的方块对象方法,这里将判断的过程放在了MainActivity中了,刚想了下,最好的是放在GameService中内部处理,直接将坐标传过来,然后gameService进行处理,并更新二维数组
package hust.wzb.service;
import hust.wzb.GameConf;
import hust.wzb.view.Piece;
import java.util.List;
public class GameService {
private Piece[][] pieces;
private GameConf config;
// 用于后续添加的Piece集合
private List<Piece> add;
public GameService(GameConf config){
this.config = config;
}
public Piece[][] getPieces(){
return this.pieces;
}
public void start(){
// 开始游戏,进行数据的初始化
GameBoard board = new GameBoard();
this.pieces = board.create(config);
this.add = board.getRandomPiece(31, 50);
}
private int getIndex(int relative, int size){
int index = -1;
if(relative % size == 0){
index = relative / size - 1;
} else{
index = relative / size;
}
return index;
}
// 根据坐标查找对应的方块
public Piece findPiece(float touchX, float touchY){
int relativeX = (int)touchX - config.getbeginX();
int relativeY = (int)touchY - config.getbeginY();
if(relativeX < 0 || relativeY < 0){
return null;
}
int indexY = getIndex(relativeX, GameConf.PIECE_WIDTH);
int indexX = getIndex(relativeY, GameConf.PIECE_HEIGHT);
System.out.println("IndexX is : " + indexX);
System.out.println("IndexY is : " + indexY);
if(indexX < 0 || indexY < 0){
return null;
}
if(indexX >= config.getXSize() || indexY >= config.getYSize()){
return null;
}
return this.pieces[indexX][indexY];
}
// 更新piece[][],即当前界面上的某个方块被点击取消后,从add集合中选中一个加入到二维数组中
// 若add为空,设置该索引处为null
public void update(int x, int y){
int size = add.size();
if(size == 0){
return;
}
int pieceWidth = GameConf.PIECE_WIDTH;
int pieceHeight = GameConf.PIECE_HEIGHT;
Piece piece = add.get(size - 1);
piece.setIndexX(x);
piece.setIndexY(y);
piece.setBeginX(config.getbeginX() + y * pieceWidth);
piece.setBeginY(config.getbeginY() + x * pieceHeight);
pieces[x][y] = piece;
add.remove(size -1);
}
}
6. MainActivity游戏入口程序流程
作为控制游戏的中枢,流程如:
初始化——注册监听器——事件触发处理方法
a. 初始化
private GameConf config;
private GameService gameService;
private GameView gameView;
private Button startButton;
private TextView timeTextView;
private Timer timer;
private int currentTime = 0; // 所用的时间
private boolean isPlaying = false; // 游戏的状态
private int lastNumber = 0; // 表示上一次选中的数字
private void init() {
lastNumber = 0;
isPlaying = false;
config = new GameConf(6, 5, 20, 30, this);
gameView = (GameView) findViewById(R.id.gameView);
gameView.setGameConf(config);
timeTextView = (TextView) findViewById(R.id.timeText);
startButton = (Button) findViewById(R.id.startButton);
// 获取震动器
vibrator = (Vibrator)getSystemService(VIBRATOR_SERVICE);
gameService = new GameService(this.config);
gameView.setGameService(gameService);
startButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
startGame(0);
}
});
this.gameView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View arg0, MotionEvent arg1) {
// TODO Auto-generated method stub
// 设置手机震动100ms
//vibrator.vibrate(100);
if (arg1.getAction() == MotionEvent.ACTION_DOWN) {
gameViewTouchDown(arg1);
} else if (arg1.getAction() == MotionEvent.ACTION_UP) {
gameViewTouchUp(arg1);
}
return false;
}
});
successDialog = createDialog("Succeed", "Succeed, restart", R.drawable.success)
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// TODO Auto-generated method stub
startGame(0);
}
});
}
b. 点击方块处理
首先是根据坐标获得对应的方块,然后判断方块的value是否正好比之前的大1,是,则更新Piece[][]数组,然后通知gameView调用onDraw()方法重绘
private void gameViewTouchDown(MotionEvent e) {
Piece[][] pieces = gameService.getPieces();
float touchX = e.getX();
float touchY = e.getY();
Piece currentPiece = gameService.findPiece(touchX, touchY);
if (currentPiece == null
|| currentPiece.getValue() != this.lastNumber + 1) {
return;
}
if (currentPiece.getValue() == 50) {
// 胜利
this.successDialog.show();
stopTimer();
isPlaying = false;
return;
}
this.lastNumber++;
pieces[currentPiece.getIndexX()][currentPiece.getIndexY()] = null;
this.gameService.update(currentPiece.getIndexX(),
currentPiece.getIndexY());
this.gameView.postInvalidate();
}
c. 游戏开始
重置数据,设置定时器,调用gameView的startGame()方法,开始游戏
public void startGame(int currentTime) {
if (this.timer != null) {
stopTimer();
}
this.currentTime = currentTime;
this.lastNumber = 0;
this.isPlaying = true;
if (currentTime == 0) {
gameView.startGame();
}
this.timer = new Timer();
this.timer.schedule(new TimerTask() {
@Override
public void run() {
// TODO Auto-generated method stub
handler.sendEmptyMessage(0x123);
}
}, 0, 1000);
}
这里定时器的作用主要是用来计时,因此在前面的初始化中应该添加一个Handler
Handler handler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case 0x123:
timeTextView.setText("TIME: " + currentTime);
currentTime++;
break;
}
}
};
这个流程貌似没有叙述的非常清楚,以后有空整个流程图出来比较。
以上代码当然有很多的问题,比如最明显的不够美观,模块划分的不好(至少我个人是没有彻底搞明白如何进行模块划分),之前有看到一个博客建议使用SuerfaceView代替View的重绘(具体怎么着,也不很明白)
另外就是gameService总感觉有些不够好(小白一个,不明所以)
…
虽然问题很多,但还算是一个好的开始,至少一天的工作没有百忙活
路漫漫其修远兮,同志仍需努力