Mokepon代码导读

Mokepon代码导读

这个网站只讲一份代码:你交上去的 Project 2(commit 355252e)。它是一个用 JavaFX 写的小游戏 Mokepon,一共 9 个包、72 个类。我们从零开始,一点一点往深里走:先看游戏真实运行时的截图,再看画出这一块的是哪个类、哪几行代码,然后看代码跑出来的真实结果,最后说清楚为什么这样设计、考试会怎么问。

页面里每一张游戏图都是这份代码真的跑起来截下来的,不是画的。每一段代码都是从你的源文件里原样剪下来的,行号就是文件里的真实行号。

从第 1 章开始

先看一眼:游戏的样子,和画它的类

游戏一共有三个画面(screen)。每个画面是 ui 包里的一个类:选择界面 CSelectScreen、地图界面 COverworldScreen、战斗界面 CBattleScreen。下面四张图按一局游戏的顺序排:左上 → 右上 → 左下 → 右下。图上的红框加红色数字是标注,图下面编号相同的那一行是它的说明。

注意第 3 张:遇敌框不是第四个画面。它是一个 模态对话框(CModalDialog),盖在地图画面上面,后面的画面并没有换。点框里的 Fight! 以后,才换成战斗画面(第 4 张是进了战斗、又打了两回合以后的样子)。

每个画面里的东西,第 1 章会一块一块标出来,告诉你是哪个类、哪个字段(类里面用来存东西的变量)负责它。

先看全局:一张图

整个程序只有一个窗口,它是 JavaFX 的一个 Stage 对象。管这个窗口的是一个 CGameWindow 对象:它的字段 stage 存着这个窗口,类注释原话是 “Owns the window and decides which screen it shows”。CMain 一启动就 new 出这个 CGameWindow。窗口顶上是菜单栏,中间一次只放一个画面。下面这张图是整份代码最重要的结构,先只看方框和线上的字(手机上图太小的话,用两指放大):

画面:ui 包规则:world、battle 包游戏窗口:拥有 Stage,决定现在显示哪个界面。CGameWindowuicurrentScreen: GameScreen选择界面:从列表里挑一只 Mokepon,并查看它的属性。CSelectScreenui地图界面:地图、右上属性卷轴、方向按钮和下方状态栏。COverworldScreenui战斗界面:显示双方 Mokepon、技能按钮、道具按钮和日志。CBattleScreenui接口:地图上会发生的五件事(移动、遇敌、捡道具、打赢、到出口)。IOverworldListenerworld一局地图上的游戏:玩家、还剩下的物体和移动规则。COverworldworld一场战斗的规则:双方轮流出招、用道具、记日志、判输赢。CBattlebattleselectScreenoverworldScreennewworldworldimplementslistenersbattle
怎么看:带圆圈的方框 = 一个类(C = 普通类,I = 接口);右上角的虚线框 = GameWindow 的字段 currentScreen;空心菱形 + 箭头 = 菱形那头的类有一个字段,存着箭头那头的对象,线上写的是字段名;实心箭头 = new;虚线 + 空心三角 = implements(实现这个接口)。GameWindow 到 BattleScreen 没有字段,只有 startBattle() 里的 new

方框里圆圈字母的意思,下面 彩色的类名 那一节有一张表。现在一条一条读:

第一条说的“父类”,单独画出来是这样(这就是打印的 UML 里的画法):

游戏窗口:拥有 Stage,决定现在显示哪个界面。CGameWindowui抽象类:三个界面的共同部分:背景、对话框、按键、菜单开关。AGameScreenui选择界面:从列表里挑一只 Mokepon,并查看它的属性。CSelectScreenui地图界面:地图、右上属性卷轴、方向按钮和下方状态栏。COverworldScreenui战斗界面:显示双方 Mokepon、技能按钮、道具按钮和日志。CBattleScreenuicurrentScreen
实线 + 空心三角 = extends(继承):三个画面都是 GameScreen 的子类。GameWindow 的字段 currentScreen 的类型是父类 GameScreen,所以它可以指着三个画面里的任何一个

跟着一局游戏,看这张图怎么动起来

下面是一个分步动画(这个网站里叫 stepper),一共 11 步,从程序启动走到打赢一仗。每一步:左边是那一刻的真实截图;右边是刚才那张大图,这一步用到的方框加上红边,用到的线变成流动的红色虚线,没用到的变淡;最下面一两句话说这一步发生了什么。这里先不看代码,只看是哪几个对象在配合。每一步的代码,后面的章节会一行一行讲,学完可以回来再看一遍。

游戏刚打开:选择界面,还没选 Mokepon
12
  1. 1
    菜单栏:最顶上的白色窄条
  2. 2
    下面这一整块:CSelectScreen 的画面
真实运行截图 · 游戏刚打开:选择界面,还没选 Mokepon
点 Choose Mokepon 后进入地图,玩家站在起点 (10,18)
12
  1. 1
    卷轴:Impike 的数值,和选择界面上一样
  2. 2
    Fights 0,三种道具都是 0:新的一局
真实运行截图 · 点 Choose Mokepon 后进入地图,玩家站在起点 (10,18)
按右方向键走一格,现在在 (11,18)
1
  1. 1
    按一次 →:玩家从 (10,18) 走到 (11,18)
真实运行截图 · 按右方向键走一格,现在在 (11,18)
走到问号箱 (18,16):捡到 Poison,状态栏和卷轴里的数量都变了
12
  1. 1
    卷轴:Poison 变成 1
  2. 2
    状态栏:You picked up a Poison!
真实运行截图 · 走到问号箱 (18,16):捡到 Poison,状态栏和卷轴里的数量都变了
再次踩上感叹号:又遇到一只(敌人可能不同)
12
  1. 1
    遇敌框:这一次是 Galecrest
  2. 2
    玩家踩在 (17,17) 的感叹号上
真实运行截图 · 再次踩上感叹号:又遇到一只(敌人可能不同)
点 Fight! 进入战斗界面,战斗刚开始
12
  1. 1
    你这边:Impike
  2. 2
    敌人这边:Galecrest
真实运行截图 · 点 Fight! 进入战斗界面,战斗刚开始
用 Stun 命中:敌人被眩晕,3 回合内伤害减半
12
  1. 1
    点 Stun
  2. 2
    能量 33 → 29
真实运行截图 · 用 Stun 命中:敌人被眩晕,3 回合内伤害减半
敌人倒下:敌人卷轴变红,弹出 You Win 对话框
12
  1. 1
    You Win 框
  2. 2
    Back to it! 按钮
真实运行截图 · 敌人倒下:敌人卷轴变红,弹出 You Win 对话框
点 Back to it! 回到地图:感叹号消失,Fights 变成 1,状态栏提示
12
  1. 1
    回到原来的地图画面,玩家还在 (17,17)
  2. 2
    状态栏:You beat the enemy Galecrest!
真实运行截图 · 点 Back to it! 回到地图:感叹号消失,Fights 变成 1,状态栏提示
点 Back to it! 回到地图:感叹号消失,Fights 变成 1,状态栏提示
1
  1. 1
    Fights 变成 1
真实运行截图 · 点 Back to it! 回到地图:感叹号消失,Fights 变成 1,状态栏提示
画面:ui 包规则:world、battle 包游戏窗口:拥有 Stage,决定现在显示哪个界面。CGameWindowuicurrentScreen: GameScreen选择界面:从列表里挑一只 Mokepon,并查看它的属性。CSelectScreenui地图界面:地图、右上属性卷轴、方向按钮和下方状态栏。COverworldScreenui战斗界面:显示双方 Mokepon、技能按钮、道具按钮和日志。CBattleScreenui接口:地图上会发生的五件事(移动、遇敌、捡道具、打赢、到出口)。IOverworldListenerworld一局地图上的游戏:玩家、还剩下的物体和移动规则。COverworldworld一场战斗的规则:双方轮流出招、用道具、记日志、判输赢。CBattlebattleselectScreenoverworldScreennewworldworldimplementslistenersbattle
  1. 程序启动:CMain new 出一个 CGameWindow。CGameWindow 先造好最上面的菜单栏(标注 1),再 new 一个 CSelectScreen 存进字段 selectScreen,并让 currentScreen 指着它,窗口中间就显示出选择界面(标注 2)。
  2. 选好 Impike、点 Choose Mokepon:CGameWindow new 出这一局的规则 COverworld 和这一局的地图画面 COverworldScreen(两条 world 线指着同一个 COverworld),把地图画面登记成 COverworld 的监听者(listeners 线),再让 currentScreen 换成地图画面。右上角卷轴上是 Impike 的数值(标注 1);这是新的一局,Fights 和道具都从 0 开始(标注 2)。
  3. 按一次 → 键:CGameWindow 把按键交给 currentScreen 指着的画面,也就是 COverworldScreen。画面自己不挪玩家,它顺着 world 线请 COverworld 往右走一格。
  4. COverworld 把玩家挪到 (11,18),然后顺着 listeners 线通知每个监听者“玩家走了”。登记在里面的正是 COverworldScreen,它收到通知就重画地图,星形图标挪了一格(标注 1)。
  5. 一路走到 (18,16) 的问号箱:COverworld 拿走箱子,把里面的 Poison 放进背包,再通知画面。画面只管显示:卷轴上 Poison 变成 1(标注 1),状态栏写上 You picked up a Poison!(标注 2)。
  6. 踩上 (17,17) 的感叹号(标注 2):COverworld 随机挑一只敌人,再通知画面,COverworldScreen 弹出遇敌框(标注 1)。这已经是第二次踩上来:第一次遇到 Hexwing,点了 Run Away! 退开了。框只是盖在地图上面,currentScreen 没有换。
  7. 点 Fight!:地图画面请 CGameWindow 开始战斗。CGameWindow 先请 COverworld 造出这一场的 CBattle,再 new 一个新的 CBattleScreen,把这个 CBattle 交给它存着(battle 线),最后让 currentScreen 换成战斗画面:左下是你的 Impike(标注 1),右上是敌人 Galecrest(标注 2)。
  8. 点 Stun(标注 1):CBattleScreen 请 CBattle 算这一回合:你出招、敌人出招、写日志。算完以后,画面再回头读 CBattle 的新数据,把卷轴、日志、按钮全部重画一遍。Stun 要花 4 点能量,所以能量从 33 变成 29(标注 2)。
  9. 又用了 Poison 和 Lance,Galecrest 的血到了 0。CBattleScreen 每次重画完都问 CBattle“打完了吗”,这次打完了,而且是你赢,就弹出 You Win 框(标注 1),框里只有一个 Back to it! 按钮(标注 2)。
  10. 点 Back to it!(上一步的标注 2):战斗画面请 CGameWindow 收尾。CGameWindow 告诉 COverworld 这一仗赢了,再让 currentScreen 换回原来那个地图画面,就是第 2 步 new 的那个对象,所以玩家还站在 (17,17)(标注 1)。
  11. COverworld 把这个感叹号从地图上拿掉、记下赢了一场,再通知画面:卷轴上 Fights 变成 1(标注 1),状态栏写上 You beat the enemy Galecrest!(上一步的标注 2)。刚才那个 CBattleScreen 再也没有字段指着它,下一场仗会 new 一个新的。

每一步在代码里是怎么做的,都在后面的章节:启动在 第 5 章,换画面在 第 6 章,走路和监听者在 第 8 章,遇敌在 第 9 章,一个回合在 第 11 章,战斗画面怎么重画在 第 14 章。

怎么用这个网站

按顺序读,每一章都走这四步

章节分成五站(第一站 → 第五站),从“游戏长什么样”一直走到“考试怎么答”,后面的章节会用到前面讲过的东西,所以按顺序读最省力。每一章里的每一节,都按同一个顺序讲:

  1. 看到:先放真实截图,标好号。
  2. 代码:是哪个类、哪几行代码做的,一行一行讲。
  3. 运行结果:代码真的跑出来的日志、数字、截图。
  4. 为什么:为什么这样设计,考试会怎么问。

截图上的红色数字 = 代码里的红色数字

这是整个网站最重要的看图方法。下面先是 CSelectScreen 里的 select 方法:你在名字列表里点一只 Mokepon,就会运行它。再下面是点了 Nightwing 以后的真实截图(放大了右半边)。代码行上的红色数字和截图上的红色数字一一对应:截图上的 1 号框,就是代码里标着 1 的那一行改出来的;2 号框是标着 2 的那一行改的,依此类推。

SelectScreen.select:点一个名字以后ui/SelectScreen.java第 142–167 行
142 /* Remember the choice and show its image, name, type and stats. */
143 private void select(Mokepon mokepon, GameButton button) {
144 if (selectedButton != null) {
145 selectedButton.setHighlighted(false);
146 }
147 selectedButton = button;
148 selectedMokepon = mokepon;
1491 chooseButton.setUsable(true);
150 button.setHighlighted(true);
151
1522 portraitView.setImage(UiStyle.loadImage(mokepon.getImagePath()));
1533 nameLabel.setText(mokepon.getName());
154
155 /* A normal Mokepon has no type image after its name. */
156 MokeponType type = mokepon.getType();
157 showTypeIcon(images.hasTypeImage(type));
158 if (images.hasTypeImage(type)) {
159 typeIcon.setImage(UiStyle.loadImage(images.getTypeImage(type)));
160 }
161
162 for (Stat stat : Stat.values()) {
1634 statValues.get(stat).setText(String.valueOf(mokepon.getStat(stat)));
164 }
1655 skillsValue.setText(formatSkills(mokepon));
1666 typeValue.setText(type.name());
167 }
选中 NORMAL 类型的 Nightwing:名字后面没有类型图标
123456
  1. 1
    第 149 行:Choose Mokepon 按钮变成能点
  2. 2
    第 152 行:头像换成 Nightwing 的图片
  3. 3
    第 153 行:名字那一行写上 Nightwing
  4. 4
    第 162–164 行:右边的六个数值,一个数一个 Label
  5. 5
    第 165 行:技能,用逗号连起来
  6. 6
    第 166 行:类型 NORMAL
点了 Nightwing:右边的内容是 select 方法一行一行改出来的真实运行截图 · 选中 NORMAL 类型的 Nightwing:名字后面没有类型图标

试一下:把鼠标放在截图下面第 4 条说明上(或者放在图上、代码上的红色数字 4 上;手机上就点一下),截图上的框和代码里的第 163 行会一起亮起来。4、5、6 号框左边的 HP:、Skills:、Type: 这些字是造画面时就放好的标签,select 只改右边的值。代码框里绿色底的行是这一段的重点。剩下几行也说一下:第 144–146 行把上一次点的按钮恢复原样;第 150 行 button.setHighlighted(true) 把这次点的按钮的浅灰背景加浓(0.5 → 0.75),看起来更亮(这个按钮在左边的列表里,在这张放大图外面,所以没有标号);第 155–160 行处理名字后面的类型图标:NORMAL 类型没有图标,所以 Nightwing 后面是空的(对比上面第 1 张图里 Impike 后面的火焰)。这段代码在 第 7 章细讲。

有的图是一整张截图,有的只放大了一部分(像上面这张)。字太小看不清的地方,都会单独放大。

彩色的类名、带虚线的词

文字里带小圆圈的彩色名字是这个项目自己写的类。圆圈里的字母和颜色,和老师打印的 UML 图(UMLDoclet 生成)上类名前面的圆圈一样:

圆圈意思例子
绿色 C普通的类(class)CBattle
浅蓝 A抽象类(abstract class),名字是斜体ASkill
紫色 I接口(interface),名字是斜体IEnemyStrategy
橙红 E枚举(enum)EStat

把鼠标放在彩色类名上,会弹出一句话介绍;点它会打开 72 个类百科里这个类的卡片(谁创建它、谁存着它、它的字段和方法、老师 UML 里它长什么样)。

没有小圆圈、只是代码字的类名,比如 Label、TextArea、ScrollPane,是 JavaFX 自带的类,不是项目写的。打印的 UML 不给它们画自己的图;只有被项目的类直接继承的 JavaFX 父类,才会在子类上面画一个只写着名字的小框,比如 CGameButton 上面的 Button(GameButton extends Button),CMain 上面的 Application。另外,网站里所有的代码字(比如 currentScreen、startBattle())都是灰底,灰底只表示“这是代码”。

带虚线的词,比如 构造器、多态,是术语:鼠标放上去会弹出解释,点它会打开 术语表。第一次出现的 Java / OOP 词都会写中文加英文,因为考试用英语。

两种动画

分步动画就是上面“跟着一局游戏”那种:一步一步换截图、亮起图里的方框和线、亮起截图上的标注。后面章节里的分步动画还会多一个代码框,每一步亮起正在运行的那几行;每一步下面的文字说三件事:屏幕上变了什么、哪段代码做的、为什么。

真实帧动画(flipbook)是把真实截图按顺序连起来放。每一帧都是真的截图,不是动画软件做出来的。点 播放,或者拖下面的滑块一帧一帧看:

点 Choose Mokepon 后进入地图,玩家站在起点 (10,18)按右方向键走一格,现在在 (11,18)按右方向键走一格,现在在 (12,18)按右方向键走一格,现在在 (13,18)按右方向键走一格,现在在 (14,18)按右方向键走一格,现在在 (15,18)按右方向键走一格,现在在 (16,18)按右方向键走一格,现在在 (17,18)按右方向键走一格,现在在 (18,18)向上走一格,现在在 (18,17)走到问号箱 (18,16):捡到 Poison,状态栏和卷轴里的数量都变了
地图右下角放大:白色星形的玩家从起点一格一格走到问号箱(每一步发生了什么,见第 8 章)

五种小框

正文旁边常有带标题的小框,标题告诉你它是哪一种。下面就是这五种,每个里面放了一个这份代码里的例子:

标记为已读、搜索

每一章最后有一个按钮“这一章我读懂了,标记为已读”。点了以后,左边目录里这一章后面的小圆圈会变成实心,目录顶上的进度条会变长,下面路线图里这一章的卡片也会变色。这些只记在你自己的浏览器里。

想找一个类或一章,用最上面的搜索框(在页面空白处按 / 也能直接开始打字)。右上角的按钮可以换浅色 / 深色。

学习路线

全部 17 章按顺序读大约 10 小时。每一张卡片写着这一章讲什么、大约要多久,点卡片就进去:

关于截图

所有截图都是真的运行你交上去的这份代码截下来的。启动的方法和你的 CMain 一样:读数据文件 mokepon.game,把里面的内容变成 Mokepon、地图和图片的对象,再 new 出 CGameWindow,打开窗口(这段代码在 第 5 章一行一行讲)。

窗口造好以后,就像你玩游戏一样:按方向键、点按钮、点菜单项,每做一步截一张图,同时记下那一刻程序里的真实数据(玩家在哪个格子、血量多少、日志写了什么)。所以图上的每个数字,和章节里讲的数字是对得上的。

游戏里有随机:每次踩上感叹号,都会重新随机挑一只敌人;出招打不打得中也是随机的。所以不同截图里的敌人名字可能不一样,这是正常的。比如同一个感叹号 (17,17),同一局里踩上去两次:

踩上 (17,17) 的感叹号:弹出遇敌对话框,有两条地形提示
1
  1. 1
    第一次:Hexwing
第一次踩上去,点了 Run Away!真实运行截图 · 踩上 (17,17) 的感叹号:弹出遇敌对话框,有两条地形提示
再次踩上感叹号:又遇到一只(敌人可能不同)
1
  1. 1
    第二次:Galecrest
退回去再踩一次,敌人换了真实运行截图 · 再次踩上感叹号:又遇到一只(敌人可能不同)

随机也有范围。(17,17) 是一个绿色感叹号,绿色感叹号只在“在这块地形上吃亏”的 Mokepon 里随机挑。这一格是 FOREST(森林),在 FOREST 上吃亏的只有 LIGHTNING 类型,所以两次挑到的都是 LIGHTNING 类型的 Mokepon,只是名字不同(第 9 章)。

打中没打中也是随机的。下面是这一局战斗日志的放大图:敌人 Galecrest 用 Tackle 打你,没打中:

用 Stun 命中:敌人被眩晕,3 回合内伤害减半
1
  1. 1
    Enemy Galecrest used Tackle! 和 But they missed.
战斗日志框(放大):最后两行是敌人这一招的结果真实运行截图 · 用 Stun 命中:敌人被眩晕,3 回合内伤害减半

关于代码

每一个代码框都是从你的源文件里原样剪下来的,行号就是文件里的真实行号,你可以在 IntelliJ 里打开同一个文件,跳到同一行对照。拿上面 SelectScreen.select 那个代码框来看:

讲的永远是你交上去的代码真的做了什么,不是“一般应该怎么写”。

老师打印的 UML 在哪

考试时卷子上印的是用 UMLDoclet 生成的 UML 类图。用同一个工具给你这份代码生成的完整 UML 在这里(8 页,A4 横版):Mokepon-Project2-UML.pdf(点开就能看)。它在电脑上的位置是:

C:\Users\louie\Documents\wk12oosdreview\Mokepon-Project2-UML.pdf

网站里也会放单个类的这种图。拿 CGameWindow 来对照:上面是代码里它的字段和 showScreen 方法,下面是 UML 图里的同一个类。代码里的每一个字段,在 UML 的字段区都有一行,顺序也一样:标号 1 的第 18 行 stage 就是 UML 字段区的第一行 stage: Stage;标号 2 的第 25 行 private GameScreen currentScreen; 就是 UML 里的 currentScreen: GameScreen;标号 3 的第 101 行 private void showScreen(...) 就是方法区最后一行 showScreen(GameScreen): void。

GameWindow 的字段和 showScreenui/GameWindow.java第 18–28、101–105 行
181 private final Stage stage;
19 private final List<Mokepon> mokepons;
20 private final GameMap map;
21 private final GameImages images;
22 private final GameMenu menu;
23 private final BorderPane root = new BorderPane();
24 private final SelectScreen selectScreen;
252 private GameScreen currentScreen;
26 /* The game being played and its screen. Both are null on the select screen. */
27 private Overworld world;
28 private OverworldScreen overworldScreen;
⋯ 省略第 29–100 行
1013 private void showScreen(GameScreen screen) {
102 currentScreen = screen;
103 root.setCenter(screen.getView());
104 updateMenu();
105 }
GameWindow 的 UML 类图
GameWindow 的 UML 类图(UMLDoclet)。名字前面的小符号:红色方块 = private,绿色圆点 = public,黄色菱形 = protected,蓝色三角 = 包内可见(package-private,代码里什么修饰符都没写,比如 Overworld 的 startEncounter);空心的是字段,实心的是方法;下划线 = static。上面的 GameNavigator 是它实现的接口(虚线 + 空心三角)

对照着看:UML 字段区的 10 行都是红色小方块,因为代码里 10 个字段都写了 private;showScreen(GameScreen): void 前面也是红色方块(实心,因为是方法),因为它是 private void showScreen(...);其他方法是绿色圆点(public)。注意:代码里有 7 个字段还写了 final(只能赋值一次),UMLDoclet 的图上不显示 final,所以光看 UML 看不出哪个字段是 final。图里所有的符号怎么读,第 3 章一个一个讲。

PDF 每一页大致放了这些(按包排):

页内容
1CMain,battle 包(CBattle、CFighter、IEnemyStrategy……),data 包的 CMapParser
2data 包的其余类,effect 包
3item 包,model 包
4skill 包(ASkill、三个抽象子类 ATargetedSkill / AAttackSkill / ABoostSkill、10 个技能、CSkillCatalogue)
5ui:CGameWindow、AGameScreen、COverworldScreen、CBattleScreen、CGameMenu、IGameNavigator
6ui:CSelectScreen、CGameButton、CStatBar、CUiStyle……
7ui:CFighterPanel、CMapView、CModalDialog……;world:COverworld、CEncounter
8world 包的其余类(CPlayer、CGameMap、CMarker……)

准备好了就从 第 1 章 先玩一局:用真实截图认识游戏 开始。