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”来实现。
此外,可以看到不管是4x4规则的或者5x5,6x6的,整个可滑动区域都是一个正方形,方块平均分布,因此可以自定义一个View,继承于GridLayout,为之添加多个Card 。
GameView 的重点在于方块的滑动判断以及实现滑动效果。
SoundPool 的使用方法在Android5.0之后发生了改变,所以需要在代码中判断当前系统版本,从而使用不同的初始化方法。
要注意的是,在GameView的构造函数中,需要读取GameView的一个自定义属性“Row”,如果没有指定则默认为5。该属性的定义在values文件夹的attrs.xml文件中。
这样,在布局文件中使用GameView时,先加上属性声明
然后就可以为GameView设置显示行数了
整个游戏界面是由GameActivity呈现的,该Activity通过Bundle 携带的数据使用不同的布局文件。
当中,可以通过mHandler实现“再按一次退出程序的效果”,这个效果需要靠boolean类型的isExit 来控制。
即如果用户点击了一次返回键后,mHandler就会在两秒后发送一条消息改变isExit 的值,如果在这两秒内用户没有再次点击返回键,则就又需要连续点击两次返回键才能退出。
GameActivity中使用到了一个自定义接口ScoreChangeListen
因为显示当前分数以及历史最高分的是两个TextView,GameView无法直接控制,所以就使用回调函数来间接控制TextView的值。
MainActivity的布局也较为简单,一共是六个ImageView,设定点击不同的ImageView执行特定的函数
ExplainActivity和SettingsActivity两个Activity较为简单这里就不再赘述
代码下载地址:Android游戏——2048的设计
访问密码:286a
如果链接失效,可以留言,我会补发的~
当初设计的时候还不觉得什么,最近在整理代码时却觉得当时代码设计得很是糟糕,代码混乱,界面也不好看。于是就趁着假期重写了一遍,游戏运行界面如下
实现的功能有:
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
如果链接失效,可以留言,我会补发的~
相关文章推荐
- Android Studio 下获取debug sha1和md5
- Android RecyclerView艺术般的控件使用完全解析
- Android权威编程指南学习笔记2
- android中进程优先级
- Android Textview动态改变drawable
- Android 游戏编程
- Edittext密码与显示状态切换
- android studio导入项目下载gradle-x.x.x-all.zip 放那个文件夹目录
- 完善继承HorizontalScrollView做的SlidingMenu
- ANDROID应用性能调优的技术点
- 关于APK瘦身值得分享的一些经验
- android ColorMatrix常用图像颜色矩阵处理效果
- Plugin is too old, please update to a more recent version错误
- 关于touch事件分发
- Android资源代码 源码 整理 Github开源项目下载地址
- Android view 详解(一)LayoutInflater
- 多段合并播放器方案(移动版)
- Android基于Pull方式解析xml的方法详解
- android事件分发机制解析(配流程图)
- 给 Android 开发者的 RxJava 详解