logo资料库

12编写推箱子游戏程序(第七步).docx

第1页 / 共13页
第2页 / 共13页
第3页 / 共13页
第4页 / 共13页
第5页 / 共13页
第6页 / 共13页
第7页 / 共13页
第8页 / 共13页
资料共13页,剩余部分请下载后查看
编写推箱子游戏程序(第七步)——绘制游戏局面
本文目标
实现思路和步骤
实现思路
实现步骤
存储游戏局面的方法
存储和使用游戏关卡的开局
存储游戏关卡的开局
据关卡号获取游戏关卡的开局
修改选择关卡功能的代码
存储和绘制游戏局面
存储游戏局面
绘制游戏局面
总结
作业
编写推箱子游戏程序(第七步)——绘制游戏局面1 本文目标 本文讲解如何绘制游戏局面。游戏局面的示例如图 1,图 2 所示。这两幅图中,红旗代 表箱子的目的地。在任一关卡,玩家把全部箱子推到各个标有红旗的单元格上,就过了这一 关。 图 1 游戏局面示例 1 图 2 游戏局面示例 2 实现绘制游戏局面这一任务,要解决两个子问题: 1. 如何在程序中存储游戏局面? 2. 如何读取游戏局面的存储数据,显示到手机屏幕上? 第 1 个问题是本文的关键。我们知道,不仅每一关的游戏局面不一样,而且由于搬运工 或箱子的移动,游戏局面是不断变化的。因此,我们要用一个“变量”来存储游戏局面。 通过本文,你将学习到: 1. 用数据类存储游戏局面的方法。 2. 静态数据成员的用法。 1本文遵循 Apache License 2.0 协议。你可以修改和再发布本文档,但须保留原著者和采用 Apache License 2.0 协议。
实现思路和步骤 实现思路 解决第一个子问题,即“如何在程序中存储游戏局面”,的思路是,采用矩阵来存储游 戏局面,用字符来表示单元格的内容(例如,字符 B (Box) 表示箱子;字符 W (Wall) 表示墙 体)。矩阵的元素与游戏局面上的单元格一一对应,如下一节的图 3 所示。矩阵的元素是字 符型的。 解决第二个子问题的思路是,依次读取矩阵的元素,根据元素的字符值在相应的单元格 内绘制图像。例如,如果矩阵元素值为’B’,则绘制箱子;如果矩阵元素值为’W’,则绘制墙 体。 我们约定,游戏局面固定为 12 行 12 列。这一约定是为了减少次要的细节,使我们聚焦 于核心内容。 实现步骤 我们要区分游戏开局和游戏局面这两个概念。  开局。推箱子游戏的每一关,都有一个开局。这是玩家第一次玩(或者重头玩)这 一关,最开始看到的游戏局面,也就是这一关的初始局面——搬运工和箱子都没有 移动过的局面。各个关卡的开局数据始终要存在。这是说,推箱子游戏程序运行期 间,开局数据要存在;程序下一次、下下次运行,开局数据还是要存在。这是因为, 即使以前玩家一个玩过关卡,在以后玩家都有可能重头玩这一关。还有一点,开局 是不会变化的。  局面。玩家在玩一个关卡期间,这个玩家在游戏界面所看到状态叫做局面。一个关 卡的开局是这一关的初始局面。搬运工或箱子移动后,都将导致状态变化,形成新 的局面。可见,局面是不断变化的。 下文中,首先详细讲解存储游戏局面的方法。在代码实现上,我们将讲解: 1. 如何存储游戏关卡的开局? 2. 如何从关卡号得到该关卡的开局? 3. 如何存储和绘制游戏关卡的局面?
存储游戏局面的方法 采用矩阵来存储游戏局面,矩阵的元素与游戏局面上的单元格一一对应,如图 3 所示。 图 3a 是矩阵,图 3b 是该矩阵表示的游戏局面。 W F W W W W W W W W W W W W W W W W W W W W W W W W B M W W W W 图 3a 存储游戏局面的矩阵(12x12) W W W W W W W W W W W W W W W W W W W 图 3b 矩阵对应的游戏局面 图 3 中,矩阵元素[0, 0]对应的是游戏局面第 1 行第 1 列的单元格。矩阵元素[1,2]对应的 是游戏局面第 2 行第 3 列的单元格。矩阵元素[11,11]对应的是游戏局面第 12 行第 12 列的单
元格。 单元格的内容可以是墙体、箱子、红旗、搬运工和空白(即没有墙体、箱子、红旗和搬 运工的情形)。我们在程序中用大写字符来表示它们:  墙体用’W’ (Wall),  红旗用’F’ (Flag),  搬运工用’M’ (Man),  空白用’ ‘(空格)。 这样,我们就知道图 3a 中的字符代表什么了,也知道图 3a 所示的矩阵正好表示了图 3b 这 个游戏局面。 事实上,单元格的内容还会出现以下情形:  搬运工踩在红旗上。用大写字符 R 表示。  箱子压在红旗上。用大写字符 X 表示。 存储和使用游戏关卡的开局 存储游戏关卡的开局 推箱子游戏的每一关都有一个开局。 代码实现上,我们用 GameLevels 类来实现存储开局数据的功能。GameLevels 类首先定 义了若干常量,如表 1 所示。注意,表 1 所示的代码仅仅是代码片段,需要和表 2、表 3、 表 4、表 6 合并在一起才构成 GameLevels 类的完整代码。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 表 1 GameLevels 类实现存储开局数据的功能 com.yescorp.moveboxgame.GameLevels.java(部分) public class GameLevels { public static final int DEFAULT_ROW_NUM= 12; public static final int DEFAULT_COLUMN_NUM= 12; //游戏区单元格放了什么 public static final char NOTHING= ' '; public static final char BOX= 'B'; public static final char FLAG= 'F'; public static final char MAN= 'M'; public static final char WALL= 'W'; public static final char MAN_FLAG= 'R'; public static final char BOX_FLAG= 'X'; …… //其他代码 //该单元格啥也没有 //该单元格放的是箱子 //红旗,表示箱子的目的地 //搬运工 //墙 //搬运工+ 红旗 //箱子+ 红旗 }; 对表 1,说明如下: 1. 第 2,3 行是定义了两个常量,对应于游戏局面固定为 12 行 12 列的约定。 2. 第 5~11 行定义了表示单元格内容的字符常量。这样做是为了增加可读性。
开局是游戏局面的初始状态。上面已经解释,应该采用字符矩阵来存储开局。代码实现 中,我们采用字符串数组来存储它,如表 2 所示。 表 2 存储第一关开局的代码片段 com.yescorp.moveboxgame.GameLevels.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static final String [] LEVEL_1 = { "WWWWWWWWWWWW", "W "W "W "W "W "W "W "W "W "W FW", W", W", WWWW W", B M W", W", W", W", W", W", "WWWWWWWWWWWW" }; 对表 2 的代码,说明如下:  第 1 行,static 修饰词使得数组 LEVEL_1 成为类级变量。这样,在程序运行期间, LEVEL_1 只 有 一 份 拷 贝 — — 发 挥 着 全 局 变 量 的 作 用 。 去 掉 static 的 话 , 每 个 GameLevels 类的实例都会有一份 LEVEL_1 的拷贝——这是没有必要的。  第 1 行,final 修饰词使得 LEVEL_1 的值始终保持不变。  第 1 行,String[]表明 LEVEL_1 是一个字符数组。前面约定游戏局面是 12 行 12 列, 那么意味着数组元素个数是 12,每个元素是字符串,字符串的长度是 12。  第 2~13 行表示的是图 3a 矩阵,也就是图 3b 的局面。 表 2 是第一关的开局数据。如何存储第二关呢?答案是定义另一个字符串数组常量,如 表 3 所示。依次类推,我们可以存储第三关,第四关,以及更多关卡的开局。 表 3 存储第二关的开局数据的代码片段 com.yescorp.moveboxgame.GameLevels.java public static final String [] LEVEL_2 = { " " " WWWWWWW " W FFB W " W W B W " W W W W " W BMW W " WFB W " WFWWWWW " WWW ", ", ", ", ", ", ", ", ", ", 1 2 3 4 5 6 7 8 9 10 11
" " 12 13 14 }; ", " 如表 2,表 3 所示,我们在程序中用硬编码的方式来存储开局。这一做法使得要改动开 局,就需要修改代码,需要重新编译程序。事实上,我们完全可以采用不用重新编译程序的 方法:用文件来存储开局,程序中读文件来加载开局。同学们可以自行尝试用文件存储开局 的方法。 据关卡号获取游戏关卡的开局 什么时候会使用开局数据?答案是,玩家选择关卡,进入游戏界面时,将使用所选关卡 的开局。本系列文章《编写推箱子游戏程序(三)》中讲到,玩家选择关卡后,将把所选的 关卡号传给显示游戏界面的活动(下称之为打游戏活动)。启动打游戏活动之际,要根据传 进来的关卡号得到开局数据,构成游戏关卡的初始状态。 问题来了,怎么根据传进来的关卡号得到开局数据呢?答案是,我们要有从关卡号映射 到开局的代码。也就是说,我们要有这样的代码:传入关卡号 1,返回 LEVEL_1 这个矩阵; 传入关卡 2,返回 LEVEL_2 这个矩阵。完成这一映射功能的代码如表 4 所示。 表 4 GameLevel.java 中,从关卡号映射到开局的代码片段 com.yescorp.moveboxgame.GameLevels.java 1 2 3 4 5 6 7 8 9 10 11 12 13 public static ArrayList OriginalLevels= new ArrayList<>(); //loadGameLevels()的作用是加载关卡列表 public static void loadGameLevels(){ if (OriginalLevels.isEmpty()) { //存储多个开局的列表 OriginalLevels.add(LEVEL_1); OriginalLevels.add(LEVEL_2); //把第 1 关开局添加到开局列表中 //把第 2 关开局添加到开局列表中 } } //getLevel()是根据关卡号 level 得到该关卡的开局(用 String[]实现的矩阵) public static String [] getLevel(int level){ //level 参数是关卡号 loadGameLevels(); returnOriginalLevels.get(level - 1); //加载关卡列表 //返回关卡号 level 对应的开局。level 从 1 开始编号,列表下标从 0 开始。 } 对表 4,说明如下: 1. 要从关卡号映射到开局,需要构建一个存储开局的列表(与数组类似)。表 4 中,用 ArrayList来实现这个列表,如第 1 行所示。OriginalLevels 是列表的名字。 2. 第 3~8 行的 loadGameLevels()方法的作用是往列表内填充每个关卡的开局。必须先填充 第 1 关的开局,接着第 2 关的开局,接着第 3 关(如有的话),….,顺序不能乱——这 样才保证列表的下标对应关卡号。第 4 行的 if 语句的作用是:即使 loadGameLevels()被 调用多次,也不会重复填充开局。 3. 第 5 行,OriginalLevels 是开局列表,add(LEVEL_1)是把第 1 关的开局添加到开局列表中。 4. 第 6 行,OriginalLevels 是开局列表,add(LEVEL_2)是把第 2 关的开局添加到开局列表中。
5. 根据第 3,4 点,你应该能知道如何添加第 3, 4, …的开局到开局列表中。 6. 第 10~13 行的 getLevel()方法的作用是根据输入的关卡号 level,得到该关卡的开局数据。 关卡号从 1 开始编号,列表下标从 0 开始编号。因此,第 12 行有 level-1 的写法。 7. 第 11 行,调用 loadGameLevels()来加载关卡列表。第 4 行保证:如果以前没有加载过, 那么就加载之;如果以前加载过,那么就不会再次加载。 8. 第 12 行,第 level 关对应的列表下标是 level-1。OriginalLevels.get(level-1)是获取第 level 关的开局。开局是用 String[]实现的二维矩阵。 9. 第 5 行的 add()和第 12 行的 get()方法是 ArrayList 类的成员方法。查阅 Android SDK 参考 文档可知它们的详细用法。 修改选择关卡功能的代码 本系列文章《编写推箱子游戏程序(三)》描述了实现选择关卡功能的代码。这部分代 码(位于 GameLevelActivity.java)有两处值得修改,见表 5。 1. 删除第 3 行。第 3 行设定的 4 个关卡是“编造”的——压根儿没有跟关卡的开局关联起 来。 2. 用第 12 行替换第 11 行。第 12 行调用了 GameLevels 类的 getLevelList()方法。getLevelList() 方法的作用是获取关卡名列表,代码如表 6 所示。 表 5 修改选择关卡功能的代码 com.yescorp.moveboxgame.GameLevelActivity.java public class GameLevelActivity extends AppCompatActivity { String[]levelList=newString[]{"第1关","第2关","第3关","第4关"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_game_level); GridView gv_levels = (GridView) findViewById(R.id.gv_levels); ArrayAdapterarrayAdapter=newArrayAdapter(this,R.layout.gv_levels_item_textview,levelList); ArrayAdapter arrayAdapter = new ArrayAdapter(this, R.layout.gv_levels_item_textview, GameLevels.getLevelList()); gv_levels.setAdapter(arrayAdapter); gv_levels.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView adapterView, View view, int i, long l) { Intent intent = new Intent(GameLevelActivity.this, GameActivity.class); intent.putExtra(GameActivity.KEY_SELECTED_LEVEL, i + 1); startActivity(intent); } }); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
24 } 表 6 是 GameLevels 类的 getLevelList()方法的代码,说明如下: 1. 这一方法将返回一个字符串列表,类型是 List,如第 1 行所示。返回值用作创建 ArrayAdapter 时要传入的第 3 个参数(见表 5 第 12 行)。 2. 第 2 行调用 loadGameLevels()。如果以前没有把关卡开局填充到开局数组中,这一步将 完成填充;如果以前填充过,那么这一步将什么也不做(见表 4 的第 3~8 行)。 3. 第 3 行,定义 levelList 来存储关卡名列表。方法执行到最后,将返回这个关卡名列表。 4. 第 4 行,得到关卡数目,用 levelNum 存储。OriginalLevels 是存储关卡开局的列表, OriginalLevels.size()是返回这个列表的元素个数。 5. 第 5~7 行,作用是生成包含“第 1 关”, “第 2 关”,…这样的关卡名的列表——关卡名 列表。 6. 第 6 行,作用是把关卡名添加到关卡名列表中。假如 i=1,那么就是把“第 1 关”这个 名称加入到关卡名列表中;假如 i=2,那么就是把“第 2 关”这个名称加入到关卡名列 表中。 7. 第 9 行,返回关卡名列表。 表 6 GameLevels 类的 getLevelList()方法 com.yescorp.moveboxgame.GameLevels.java(部分) 1 2 3 4 5 6 7 8 9 10 public static List getLevelList(){ //返关卡名列表(用 List实现这个列表) //加载关卡列表 loadGameLevels(); List levelList = new ArrayList<>(); int levelNum = OriginalLevels.size(); for (int i = 1; i <= levelNum; i++){ //创建关卡名列表对象 levelList,并分配存储空间 //得到关卡的数目 //对每一关 i (i=1, 2, …), levelList.add(new String("第" + i + "关")); //把关卡名(如第 1 关,第 2 关)加入到关卡名列表 } return levelList; //返回关卡名列表 } 注意,表1、表2、表3、表4、表6 合并在一起才构成GameLevels 类的完整代码。 到这一步,我们做到了: 1. 选择关卡界面显示的关卡名列表中,每一关都关联着一个游戏开局。 2. 玩家选择关卡后,将获得该关卡的开局数据。 下一步,我们将显示关卡的开局。 存储和绘制游戏局面 存储游戏局面 玩家在游戏界面上指挥搬运工走动后,游戏局面将发生变化。如何存储不断变化的游戏
分享到:
收藏