您的位置:首页 > 移动开发 > Android开发

Android游戏——2048的设计(2)

2016-07-20 12:06 405 查看
在去年的时候曾经写了一个Android小游戏——2048,也在应用商店上线了,地址:Android游戏——2048的设计

当初设计的时候还不觉得什么,最近在整理代码时却觉得当时代码设计得很是糟糕,代码混乱,界面也不好看。于是就趁着假期重写了一遍,游戏运行界面如下



实现的功能有:

1.有4x4,5x5,6x6三种规则

2.记录历史最高分

3.使用纯色块

4.保存游戏

5.开启音效

6.更换背景图

开发工具用的是Android Studio



游戏的思路并不复杂,甚至可以说是挺简单的。

首先要自定义一个View,作为可滑动的方块(其实滑动效果是通过改变数字与颜色来模拟实现的),这个View要继承于FrameLayout

每一种不同数值的方块有不同的颜色,通过设置“setBackgroundColor”来实现。

public class Card extends FrameLayout {

private TextView label;

private int num = 0;

//用于判断是否纯色块
public boolean flag;

public Card(Context context) {
super(context);
label = new TextView(context);
label.setGravity(Gravity.CENTER);
label.setTextSize(24);
label.setBackgroundColor(Color.parseColor("#77E8E2D8"));
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
lp.setMargins(5, 5, 0, 0);
addView(label, lp);
}

public void setNum(int num) {
this.num = num;
if (num == 0) {
label.setText("");
label.setBackgroundColor(Color.parseColor("#77E8E2D8"));
}else{
if(!flag){
label.setText(num + "");
}
changeCardColor();
}
}

public int getNum() {
return num;
}

public void changeCardColor() {
switch (num) {
case 2:
label.setBackgroundColor(Color.parseColor("#5DB8E8"));
break;
case 4:
label.setBackgroundColor(Color.parseColor("#A52812"));
break;
case 8:
label.setBackgroundColor(Color.parseColor("#0E7171"));
break;
case 16:
label.setBackgroundColor(Color.parseColor("#C0BB39"));
break;
case 32:
label.setBackgroundColor(Color.parseColor("#623889"));
break;
case 64:
label.setBackgroundColor(Color.parseColor("#5C7235"));
break;
case 128:
label.setBackgroundColor(Color.parseColor("#826FA3"));
break;
case 256:
label.setBackgroundColor(Color.parseColor("#355659"));
break;
case 512:
label.setBackgroundColor(Color.parseColor("#BB719B"));
break;
case 1024:
label.setBackgroundColor(Color.parseColor("#9B8B53"));
break;
case 2048:
label.setBackgroundColor(Color.parseColor("#196A5D"));
break;
default:
label.setBackgroundColor(Color.parseColor("#8A7760"));
}
}

public boolean equals(Card c) {
return this.getNum() == c.getNum();
}
}


此外,可以看到不管是4x4规则的或者5x5,6x6的,整个可滑动区域都是一个正方形,方块平均分布,因此可以自定义一个View,继承于GridLayout,为之添加多个Card 。

GameView 的重点在于方块的滑动判断以及实现滑动效果。

SoundPool 的使用方法在Android5.0之后发生了改变,所以需要在代码中判断当前系统版本,从而使用不同的初始化方法。

public class GameView extends GridLayout {

// 存储所有方块
private Card[][] Cards;

// 当前游戏的行数与列数
private int Row;

// 游戏记录
private SharedPreferences gameRecord;

// 存储游戏音效开关记录
private SharedPreferences GameSettings;

private SharedPreferences.Editor grEditor;

public ScoreChangeListen scoreChangeListen=null;

private Context context;

//当前得分
private int Score;

public SoundPool soundPool;

// private HashMap<Integer, Integer> soundID;

private int soundID;;

private boolean soundSwitch;

private class Point {
public Point(int x, int y) {
this.x = x;
this.y = y;
}
int x;
int y;
}

public GameView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.ViewSet);
Row = mTypedArray.getInt(R.styleable.ViewSet_Row, 5);
mTypedArray.recycle();
super.setColumnCount(Row);
init();
}

public GameView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.ViewSet);
Row = mTypedArray.getInt(R.styleable.ViewSet_Row, 5);
mTypedArray.recycle();
super.setColumnCount(Row);
init();
}

public GameView(Context context) {
super(context);
this.context = context;
init();
}

// 初始化
private void init() {
gameRecord = context.getSharedPreferences("GameRecord", Context.MODE_PRIVATE);
GameSettings = context.getSharedPreferences("GameSettings", Context.MODE_PRIVATE);

boolean flag=GameSettings.getBoolean("SolidColorSwitch",false);
soundSwitch=GameSettings.getBoolean("SoundSwitch",false);

//SoundPool的构建方法在5.0系统之后发生了变化
if (Build.VERSION.SDK_INT < 21) {
soundPool = new SoundPool(1,AudioManager.STREAM_MUSIC,0);
}else{
SoundPool.Builder builder = new SoundPool.Builder();
builder.setMaxStreams(1);
AudioAttributes.Builder attrBuilder = new AudioAttributes.Builder();
attrBuilder.setLegacyStreamType(AudioManager.STREAM_MUSIC);
builder.setAudioAttributes(attrBuilder.build());
soundPool = builder.build();
}
soundID=soundPool.load(context,R.raw.sound,1);
grEditor = gameRecord.edit();
Cards = new Card[Row][Row];
for (int y = 0; y < Row; y++) {
for (int x = 0; x < Row; x++) {
Cards[x][y] = new Card(context);
Cards[x][y].flag=flag;
}
}
// 添加两个初始方块
randomCard();
randomCard();
}

protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 计算方块的边长
int cardWidth = (Math.min(w, h) - 5) / Row;
// 添加方块
addCard(cardWidth);
}

// 计算分数
private void countScore(int num) {
Score = Score + num;
if(scoreChangeListen!=null){
scoreChangeListen.OnNowScoreChange(Score);
if(soundSwitch){
soundPool.play(soundID, 1, 1, 0, 0, 1);
}
}
}

// 添加方块
private void addCard(int cardWidth) {
for (int y = 0; y < Row; y++) {
for (int x = 0; x < Row; x++) {
addView(Cards[x][y], cardWidth, cardWidth);
}
}
}

// 生成伪随机方块
private void randomCard() {
List<Point> points = new ArrayList<>();
for (int x = 0; x < Row; x++) {
for (int y = 0; y < Row; y++) {
// 如果还有空白方块
if (Cards[x][y].getNum() == 0) {
points.add(new Point(x, y));
}
}
}
if (points.size() == 0) {
return;
}
int index = points.size() / 2;
Cards[points.get(index).x][points.get(index).y].setNum(2);
}

// 左移
private void moveLeftCard() {
allMoveLeft();
for (int y = 0; y < Row; y++) {
for (int x = 0; x < Row - 1; x++) {
if (Cards[x][y].getNum() != 0) {
if (Cards[x][y].equals(Cards[x + 1][y])) {
int num = Cards[x][y].getNum();
Cards[x][y].setNum(2 * num);
Cards[x + 1][y].setNum(0);
countScore(num);
allMoveLeft();
}
}
}
}
randomCard();
}

// 右移
private void moveRightCard() {
allMoveRight();
for (int y = 0; y < Row; y++) {
for (int x = Row - 1; x > 0; x--) {
if (Cards[x][y].getNum() != 0) {
if (Cards[x][y].equals(Cards[x - 1][y])) {
int num = Cards[x][y].getNum();
Cards[x][y].setNum(2 * num);
Cards[x - 1][y].setNum(0);
countScore(num);
allMoveRight();
}
}
}
}
randomCard();
}

// 上移
private void moveUpCard() {
allMoveUp();
for (int x = 0; x < Row; x++) {
for (int y = 0; y < Row - 1; y++) {
if (Cards[x][y].getNum() != 0) {
if (Cards[x][y].equals(Cards[x][y + 1])) {
int num = Cards[x][y].getNum();
Cards[x][y].setNum(2 * num);
Cards[x][y + 1].setNum(0);
countScore(num);
allMoveUp();
}
}
}
}
randomCard();
}

// 下移
private void moveDownCard() {
allMoveDown();
for (int x = 0; x < Row; x++) {
for (int y = Row - 1; y > 0; y--) {
if (Cards[x][y].getNum() != 0) {
if (Cards[x][y].equals(Cards[x][y - 1])) {
int num = Cards[x][y].getNum();
Cards[x][y].setNum(2 * num);
Cards[x][y - 1].setNum(0);
countScore(num);
allMoveDown();
}
}
}
}
randomCard();
}

// 全部左移
private void allMoveLeft() {
for (int y = 0; y < Row; y++) {
int i = 0;
for (int x = 0; x < Row; x++) {
if (Cards[x][y].getNum() != 0) {
int num = Cards[x][y].getNum();
Cards[x][y].setNum(0);
Cards[i++][y].setNum(num);
}
}
}
}

// 全部右移
private void allMoveRight() {
for (int y = 0; y < Row; y++) {
int i = Row - 1;
for (int x = Row - 1; x > -1; x--) {
if (Cards[x][y].getNum() != 0) {
int num = Cards[x][y].getNum();
Cards[x][y].setNum(0);
Cards[i--][y].setNum(num);
}
}
}
}

// 全部上移
private void allMoveUp() {
for (int x = 0; x < Row; x++) {
int i = 0;
for (int y = 0; y < Row; y++) {
if (Cards[x][y].getNum() != 0) {
int num = Cards[x][y].getNum();
Cards[x][y].setNum(0);
Cards[x][i++].setNum(num);
}
}
}
}

// 全部下移
private void allMoveDown() {
for (int x = 0; x < Row; x++) {
int i = Row - 1;
for (int y = Row - 1; y > -1; y--) {
if (Cards[x][y].getNum() != 0) {
int num = Cards[x][y].getNum();
Cards[x][y].setNum(0);
Cards[x][i--].setNum(num);
}
}
}
}

// 触屏事件监听
float X;
float Y;
float OffsetX;
float OffsetY;
int HintCount = 0;
public boolean isHalfway = true;
public boolean onTouchEvent(MotionEvent event) {
// 为了避免当游戏结束时消息多次提示
if (HintCount == 1) {
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
X = event.getX();
Y = event.getY();
break;
case MotionEvent.ACTION_UP:
OffsetX = event.getX() - X;
OffsetY = event.getY() - Y;
if (Math.abs(OffsetX) > (Math.abs(OffsetY))) {
if (OffsetX < -5) {
moveLeftCard();
} else if (OffsetX > 5) {
moveRightCard();
}
} else {
if (OffsetY < -5) {
moveUpCard();
} else if (OffsetY > 5) {
moveDownCard();
}
}
HintMessage();
break;
}
return true;
}

// 判断游戏是否结束
private boolean isOver() {
for (int y = 0; y < Row; y++) {
for (int x = 0; x < Row; x++) {
if ((Cards[x][y].getNum() == 0) || (x - 1 >= 0 && Cards[x - 1][y].equals(Cards[x][y]))
|| (x + 1 <= Row - 1 && Cards[x + 1][y].equals(Cards[x][y]))
|| (y - 1 >= 0 && Cards[x][y - 1].equals(Cards[x][y]))
|| (y + 1 <= Row - 1 && Cards[x][y + 1].equals(Cards[x][y]))) {
return false;
}
}
}
return true;
}

// 当游戏结束时提示信息
private void HintMessage() {
if (isOver()) {
Toast.makeText(getContext(), "游戏结束啦", Toast.LENGTH_SHORT).show();
HintCount=1;
}
}

//重新开始
public void restart(){
for (int y = 0; y < Row; y++) {
for (int x = 0; x < Row; x++) {
Cards[x][y].setNum(0);
}
}
Score=0;
HintCount=0;
// 添加两个初始方块
randomCard();
randomCard();
}

//保存游戏
public void saveGame(){
grEditor.clear();
grEditor.putInt("Row", Row);
grEditor.putInt("Score",Score);
int k = 0;
for (int i = 0; i < Row; i++) {
for (int j = 0; j < Row; j++) {
k++;
String str = k + "";
grEditor.putInt(str, Cards[i][j].getNum());
}
}
if( grEditor.commit()){
Toast.makeText(context, "保存成功", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(context, "保存失败,请重试", Toast.LENGTH_SHORT).show();
}
}

// 恢复游戏
public void recoverGame() {
int k = 0;
for (int i = 0; i < Row; i++) {
for (int j = 0; j < Row; j++) {
k++;
String str = k + "";
int num = gameRecord.getInt(str, 0);
Cards[i][j].setNum(num);
}
}
Score=gameRecord.getInt("Score",0);
scoreChangeListen.OnNowScoreChange(Score);
}

}


要注意的是,在GameView的构造函数中,需要读取GameView的一个自定义属性“Row”,如果没有指定则默认为5。该属性的定义在values文件夹的attrs.xml文件中。

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ViewSet">
<attr name="Row" format="integer"/>
</declare-styleable>
</resources>


这样,在布局文件中使用GameView时,先加上属性声明

xmlns:my="http://schemas.android.com/apk/res-auto"


然后就可以为GameView设置显示行数了

整个游戏界面是由GameActivity呈现的,该Activity通过Bundle 携带的数据使用不同的布局文件。

public class GameActivity extends AppCompatActivity {

private GameView gameView;

private TextView text_nowScore;

private TextView text_highestScore;

private TextView text_restart;

private TextView text_saveGame;

private ScoreChangeListen scoreChangeListen;

// 游戏设置
private SharedPreferences gameSettings;

private SharedPreferences.Editor gsEditor;

// 历史最高分
private int highestScore;

// 用于实现“在点击一次返回键退出程序”的效果
private boolean isExit = false;

private boolean flag;

private int temp;

private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
super.handleMessage(msg);
isExit = false;
}
};

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().hide();
Intent intent = getIntent();
Bundle bundle = intent.getExtras();
int row = bundle.getInt("Row", 4);
if (row == 4) {
setContentView(R.layout.activity_four);
} else if (row == 5) {
setContentView(R.layout.activity_five);
} else {
setContentView(R.layout.activity_six);
}
init();
//判断是否需要恢复游戏记录
if (bundle.getBoolean("RecoverGame", false)) {
gameView.recoverGame();
}
}

// 初始化
public void init() {
gameView = (GameView) findViewById(R.id.gameView_five);
text_nowScore = (TextView) findViewById(R.id.nowScore);
text_highestScore = (TextView) findViewById(R.id.highestScore);
text_restart = (TextView) findViewById(R.id.restart);
text_saveGame = (TextView) findViewById(R.id.save_game);
gameSettings = getSharedPreferences("GameSettings", Context.MODE_PRIVATE);
gsEditor = gameSettings.edit();

highestScore = gameSettings.getInt("HighestScore", 0);
text_nowScore.setText("当前得分\n" + 0);
text_highestScore.setText("最高得分\n" + highestScore);
flag = true;

LinearLayout rootLayout = (LinearLayout) findViewById(R.id.rootLayout);
int themeIndex = gameSettings.getInt("ThemeIndex", 1);
switch (themeIndex) {
case 1:
rootLayout.setBackgroundResource(R.drawable.back1);
break;
case 2:
rootLayout.setBackgroundResource(R.drawable.back2);
break;
case 3:
rootLayout.setBackgroundResource(R.drawable.back3);
break;
case 4:
rootLayout.setBackgroundResource(R.drawable.back4);
break;
case 5:
rootLayout.setBackgroundResource(R.drawable.back5);
break;
case 6:
rootLayout.setBackgroundResource(R.drawable.back6);
break;
}

//重新开始游戏
text_restart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AlertDialog.Builder builder = new AlertDialog.Builder(GameActivity.this);
builder.setMessage("确认重新开始游戏吗?");
builder.setTitle("提示");
builder.setPositiveButton("确认", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
gameView.restart();
text_nowScore.setText("当前得分\n" + 0);
if (temp != 0) {
scoreChangeListen.OnHighestScoreChange(temp);
highestScore = temp;
flag = true;
}
}
});
builder.setNegativeButton("取消", null);
builder.create().show();
}
});

//保存游戏
text_saveGame.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AlertDialog.Builder builder = new AlertDialog.Builder(GameActivity.this);
builder.setMessage("确认保存游戏吗?");
builder.setTitle("提示");
builder.setPositiveButton("确认", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
gameView.saveGame();
}
});
builder.setNegativeButton("取消", null);
builder.create().show();
}
});

scoreChangeListen = new ScoreChangeListen() {
@Override
public void OnNowScoreChange(int Score) {
text_nowScore.setText("当前得分\n" + Score);
if (Score > highestScore) {
if (flag && highestScore != 0) {
Toast.makeText(GameActivity.this, "打破最高纪录啦,请继续保持", Toast.LENGTH_SHORT).show();
flag = false;
}
temp = Score;
text_highestScore.setText("最高得分\n" + temp);
}
}

@Override
public void OnHighestScoreChange(int Score) {
gsEditor.putInt("HighestScore", Score);
gsEditor.commit();
}
};
gameView.scoreChangeListen = scoreChangeListen;
}

// 重写返回键监听事件
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
exit();
return false;
}
return super.onKeyDown(keyCode, event);
}

private void exit() {
if (!isExit) {
isExit = true;
if (gameView.isHalfway) {
Toast.makeText(this, "再按一次结束游戏,建议保存游戏", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "再按一次结束游戏", Toast.LENGTH_SHORT).show();
}
// 利用handler延迟发送更改状态信息
mHandler.sendEmptyMessageDelayed(0, 2000);
} else {
finish();
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if (temp != 0) {
scoreChangeListen.OnHighestScoreChange(temp);
}
gameView.soundPool.release();
}
}


当中,可以通过mHandler实现“再按一次退出程序的效果”,这个效果需要靠boolean类型的isExit 来控制。

即如果用户点击了一次返回键后,mHandler就会在两秒后发送一条消息改变isExit 的值,如果在这两秒内用户没有再次点击返回键,则就又需要连续点击两次返回键才能退出。

private void exit() {
if (!isExit) {
isExit = true;
if (gameView.isHalfway) {
Toast.makeText(this, "再按一次结束游戏,建议保存游戏", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "再按一次结束游戏", Toast.LENGTH_SHORT).show();
}
// 利用handler延迟发送更改状态信息
mHandler.sendEmptyMessageDelayed(0, 2000);
} else {
finish();
}
}


GameActivity中使用到了一个自定义接口ScoreChangeListen

/**
* Created by ZY on 2016/7/18.
*/
public interface ScoreChangeListen {

void OnNowScoreChange(int Score);

void OnHighestScoreChange(int Score);
}


因为显示当前分数以及历史最高分的是两个TextView,GameView无法直接控制,所以就使用回调函数来间接控制TextView的值。

MainActivity的布局也较为简单,一共是六个ImageView,设定点击不同的ImageView执行特定的函数

public class MainActivity extends AppCompatActivity {

// 游戏记录
private SharedPreferences gameRecord;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportActionBar().hide();
gameRecord = getSharedPreferences("GameRecord", Context.MODE_PRIVATE);
}

//软件说明
public void explain(View view){
Intent intent=new Intent(MainActivity.this,ExplainActivity.class);
startActivity(intent);
}

// 4乘4
public void fourRow(View view) {
Intent intent = new Intent(MainActivity.this, GameActivity.class);
Bundle bundle=new Bundle();
bundle.putInt("Row",4);
intent.putExtras(bundle);
startActivity(intent);
}

// 5乘5
public void fiveRow(View view) {
Intent intent = new Intent(MainActivity.this, GameActivity.class);
Bundle bundle=new Bundle();
bundle.putInt("Row",5);
intent.putExtras(bundle);
startActivity(intent);
}

// 6乘6
public void sixRow(View view) {
Intent intent = new Intent(MainActivity.this, GameActivity.class);
Bundle bundle=new Bundle();
bundle.putInt("Row",6);
intent.putExtras(bundle);
startActivity(intent);
}

//恢复游戏
public void recoverGame(View view){
if(gameRecord.contains("Row")){
int row=gameRecord.getInt("Row",4);
Bundle bundle=new Bundle();
Intent intent = new Intent(MainActivity.this, GameActivity.class);
if(row==4){
bundle.putInt("Row",4);
}else if(row==5){
bundle.putInt("Row",5);
}else{
bundle.putInt("Row",6);
}
bundle.putBoolean("RecoverGame",true);
intent.putExtras(bundle);
startActivity(intent);
}else{
Toast.makeText(MainActivity.this,"没有保存记录,来一局新游戏吧",Toast.LENGTH_SHORT).show();
}
}

//设置
public void settings(View view){
Intent intent = new Intent(MainActivity.this, SettingsActivity.class);
startActivity(intent);
}
}


ExplainActivity和SettingsActivity两个Activity较为简单这里就不再赘述

代码下载地址:Android游戏——2048的设计

访问密码:286a

如果链接失效,可以留言,我会补发的~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: