Android 之游戏开发流程

时间:2022-04-27
本文章向大家介绍Android 之游戏开发流程,主要内容包括一、概述、二、模块划分、三、详细说明、四、结束语、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

一、概述

刚开始接触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总感觉有些不够好(小白一个,不明所以)

虽然问题很多,但还算是一个好的开始,至少一天的工作没有百忙活

路漫漫其修远兮,同志仍需努力