Mokepon代码导读
这个网站只讲一份代码:你交上去的 Project 2(commit 355252e)。它是一个用 JavaFX 写的小游戏 Mokepon,一共 9 个包、72 个类。我们从零开始,一点一点往深里走:先看游戏真实运行时的截图,再看画出这一块的是哪个类、哪几行代码,然后看代码跑出来的真实结果,最后说清楚为什么这样设计、考试会怎么问。
页面里每一张游戏图都是这份代码真的跑起来截下来的,不是画的。每一段代码都是从你的源文件里原样剪下来的,行号就是文件里的真实行号。
先看一眼:游戏的样子,和画它的类
游戏一共有三个画面(screen)。每个画面是 ui 包里的一个类:选择界面 CSelectScreen、地图界面 COverworldScreen、战斗界面 CBattleScreen。下面四张图按一局游戏的顺序排:左上 → 右上 → 左下 → 右下。图上的红框加红色数字是标注,图下面编号相同的那一行是它的说明。
123- 1名字列表:一个能上下滚动的
ScrollPane(CSelectScreen 第 212–227 行造的),里面是 36 个 CGameButton(项目自己写的按钮类),一只 Mokepon 一个。这里只看得到前 11 个 - 2点了 Impike:圆里换成它的头像,下面的名字、火焰图标和数值也换了(Impike 的按钮在列表下面,要往下滚才看得到,所以这张图里没有它)
- 3Choose Mokepon 按钮:点它开始一局
123- 1地图:CMapView 画的 20×20 个格子
- 2玩家:地图上那个白色的星形图标,站在起点 (10,18)
- 3右上角卷轴:CPlayerStatsScroll
12- 1遇敌框:一个 CModalDialog,盖在地图画面上面
- 2玩家(白色星形图标)踩在 (17,17) 的感叹号上
1234- 1你的卷轴:一个 CFighterPanel(字段
playerPanel) - 2敌人的卷轴:同一个类 CFighterPanel 的另一个对象(字段
enemyPanel) - 3Stun 技能按钮:也是 CGameButton
- 4日志框:字段
logArea,里面的字来自 CBattle 的日志
注意第 3 张:遇敌框不是第四个画面。它是一个 模态对话框(CModalDialog),盖在地图画面上面,后面的画面并没有换。点框里的 Fight! 以后,才换成战斗画面(第 4 张是进了战斗、又打了两回合以后的样子)。
每个画面里的东西,第 1 章会一块一块标出来,告诉你是哪个类、哪个字段(类里面用来存东西的变量)负责它。
先看全局:一张图
整个程序只有一个窗口,它是 JavaFX 的一个 Stage 对象。管这个窗口的是一个 CGameWindow 对象:它的字段 stage 存着这个窗口,类注释原话是 “Owns the window and decides which screen it shows”。CMain 一启动就 new 出这个 CGameWindow。窗口顶上是菜单栏,中间一次只放一个画面。下面这张图是整份代码最重要的结构,先只看方框和线上的字(手机上图太小的话,用两指放大):
方框里圆圈字母的意思,下面 彩色的类名 那一节有一张表。现在一条一条读:
- 上面两排是 ui 包,负责“画”。CGameWindow 用字段
selectScreen和overworldScreen记着两个画面(线上写着这两个名字),用字段currentScreen记着现在显示的那一个(右上角的虚线框)。currentScreen的类型写的是三个画面共同的父类 AGameScreen,所以三个画面都能放进去(这一层关系下面单独画了一张小图)。 - 没有
BattleScreen类型的字段。写着 new 的那根实心箭头:每打一场仗,CGameWindow 的startBattle()就 new 一个新的 CBattleScreen,只放进currentScreen。地图画面不一样:同一局里一直是同一个对象,打完仗还换回它。 - 最下面两排是规则。COverworld(world 包)管一局地图:玩家在哪、能不能走、踩到了什么(第 8 章)。CBattle(battle 包)管一场仗:两边轮流出招、扣血、写日志、判断输赢(第 10 章)。它们完全不知道屏幕长什么样,两个类的类注释里都写着 “knows nothing about the screen”。
- 两条线都写着
world:一条从 CGameWindow 出发,一条从 COverworldScreen 出发。这两个类各有一个叫world的字段,指着同一个 COverworld 对象。 - 规则变了,画面怎么知道?地图这边用的是观察者模式(Observer)。COverworld 有一个字段
listeners(写着 listeners 的线),类型是List<OverworldListener>,意思是“一个列表,里面每一个都是 IOverworldListener”。IOverworldListener 是一个接口(interface):只规定有哪些方法,不写方法里做什么。COverworld 出了事(走了一步、捡到东西、碰到敌人……)就对列表里的每一个调用接口里的方法,比如onPlayerMoved()。它只认识这个接口,不认识 COverworldScreen 这个类。COverworldScreen 实现(implements)了 IOverworldListener(写着 implements 的虚线),所以能登记进listeners,成为监听者。 - 战斗这边没有监听者。你每点一个按钮,CBattleScreen 先让 CBattle 算,再自己回头去读 CBattle 的新数据,把整个画面重画一遍(第 14 章)。CSelectScreen 后面也没有规则对象:它只用 Mokepon 的数据造出名字按钮,选好以后把你选的那只交给 CGameWindow。
- 画面怎么叫 CGameWindow 换画面?三个画面手里都没有 CGameWindow 类型的字段,只有父类 AGameScreen 里的字段
navigator,类型是接口 IGameNavigator。CGameWindow 实现了这个接口,所以画面调用getNavigator().startGame(...),真正运行的是 CGameWindow 的startGame。图里没画这条线,第 6 章细讲为什么要这样绕一下。
第一条说的“父类”,单独画出来是这样(这就是打印的 UML 里的画法):
跟着一局游戏,看这张图怎么动起来
下面是一个分步动画(这个网站里叫 stepper),一共 11 步,从程序启动走到打赢一仗。每一步:左边是那一刻的真实截图;右边是刚才那张大图,这一步用到的方框加上红边,用到的线变成流动的红色虚线,没用到的变淡;最下面一两句话说这一步发生了什么。这里先不看代码,只看是哪几个对象在配合。每一步的代码,后面的章节会一行一行讲,学完可以回来再看一遍。

- 1菜单栏:最顶上的白色窄条
- 2下面这一整块:CSelectScreen 的画面

- 1卷轴:Impike 的数值,和选择界面上一样
- 2Fights 0,三种道具都是 0:新的一局

- 1按一次 →:玩家从 (10,18) 走到 (11,18)

- 1卷轴:Poison 变成 1
- 2状态栏:You picked up a Poison!

- 1遇敌框:这一次是 Galecrest
- 2玩家踩在 (17,17) 的感叹号上

- 1你这边:Impike
- 2敌人这边:Galecrest

- 1点 Stun
- 2能量 33 → 29

- 1You Win 框
- 2Back to it! 按钮

- 1回到原来的地图画面,玩家还在 (17,17)
- 2状态栏:You beat the enemy Galecrest!

- 1Fights 变成 1
- 程序启动:CMain new 出一个 CGameWindow。CGameWindow 先造好最上面的菜单栏(标注 1),再 new 一个 CSelectScreen 存进字段
selectScreen,并让currentScreen指着它,窗口中间就显示出选择界面(标注 2)。 - 选好 Impike、点 Choose Mokepon:CGameWindow new 出这一局的规则 COverworld 和这一局的地图画面 COverworldScreen(两条 world 线指着同一个 COverworld),把地图画面登记成 COverworld 的监听者(listeners 线),再让
currentScreen换成地图画面。右上角卷轴上是 Impike 的数值(标注 1);这是新的一局,Fights 和道具都从 0 开始(标注 2)。 - 按一次 → 键:CGameWindow 把按键交给
currentScreen指着的画面,也就是 COverworldScreen。画面自己不挪玩家,它顺着 world 线请 COverworld 往右走一格。 - COverworld 把玩家挪到 (11,18),然后顺着 listeners 线通知每个监听者“玩家走了”。登记在里面的正是 COverworldScreen,它收到通知就重画地图,星形图标挪了一格(标注 1)。
- 一路走到 (18,16) 的问号箱:COverworld 拿走箱子,把里面的 Poison 放进背包,再通知画面。画面只管显示:卷轴上 Poison 变成 1(标注 1),状态栏写上 You picked up a Poison!(标注 2)。
- 踩上 (17,17) 的感叹号(标注 2):COverworld 随机挑一只敌人,再通知画面,COverworldScreen 弹出遇敌框(标注 1)。这已经是第二次踩上来:第一次遇到 Hexwing,点了 Run Away! 退开了。框只是盖在地图上面,
currentScreen没有换。 - 点 Fight!:地图画面请 CGameWindow 开始战斗。CGameWindow 先请 COverworld 造出这一场的 CBattle,再 new 一个新的 CBattleScreen,把这个 CBattle 交给它存着(battle 线),最后让
currentScreen换成战斗画面:左下是你的 Impike(标注 1),右上是敌人 Galecrest(标注 2)。 - 点 Stun(标注 1):CBattleScreen 请 CBattle 算这一回合:你出招、敌人出招、写日志。算完以后,画面再回头读 CBattle 的新数据,把卷轴、日志、按钮全部重画一遍。Stun 要花 4 点能量,所以能量从 33 变成 29(标注 2)。
- 又用了 Poison 和 Lance,Galecrest 的血到了 0。CBattleScreen 每次重画完都问 CBattle“打完了吗”,这次打完了,而且是你赢,就弹出 You Win 框(标注 1),框里只有一个 Back to it! 按钮(标注 2)。
- 点 Back to it!(上一步的标注 2):战斗画面请 CGameWindow 收尾。CGameWindow 告诉 COverworld 这一仗赢了,再让
currentScreen换回原来那个地图画面,就是第 2 步 new 的那个对象,所以玩家还站在 (17,17)(标注 1)。 - COverworld 把这个感叹号从地图上拿掉、记下赢了一场,再通知画面:卷轴上 Fights 变成 1(标注 1),状态栏写上 You beat the enemy Galecrest!(上一步的标注 2)。刚才那个 CBattleScreen 再也没有字段指着它,下一场仗会 new 一个新的。
每一步在代码里是怎么做的,都在后面的章节:启动在 第 5 章,换画面在 第 6 章,走路和监听者在 第 8 章,遇敌在 第 9 章,一个回合在 第 11 章,战斗画面怎么重画在 第 14 章。
怎么用这个网站
按顺序读,每一章都走这四步
章节分成五站(第一站 → 第五站),从“游戏长什么样”一直走到“考试怎么答”,后面的章节会用到前面讲过的东西,所以按顺序读最省力。每一章里的每一节,都按同一个顺序讲:
- 看到:先放真实截图,标好号。
- 代码:是哪个类、哪几行代码做的,一行一行讲。
- 运行结果:代码真的跑出来的日志、数字、截图。
- 为什么:为什么这样设计,考试会怎么问。
截图上的红色数字 = 代码里的红色数字
这是整个网站最重要的看图方法。下面先是 CSelectScreen 里的 select 方法:你在名字列表里点一只 Mokepon,就会运行它。再下面是点了 Nightwing 以后的真实截图(放大了右半边)。代码行上的红色数字和截图上的红色数字一一对应:截图上的 1 号框,就是代码里标着 1 的那一行改出来的;2 号框是标着 2 的那一行改的,依此类推。
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 }
- 1第 149 行:Choose Mokepon 按钮变成能点
- 2第 152 行:头像换成 Nightwing 的图片
- 3第 153 行:名字那一行写上 Nightwing
- 4第 162–164 行:右边的六个数值,一个数一个
Label - 5第 165 行:技能,用逗号连起来
- 6第 166 行:类型 NORMAL
试一下:把鼠标放在截图下面第 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)是把真实截图按顺序连起来放。每一帧都是真的截图,不是动画软件做出来的。点 播放,或者拖下面的滑块一帧一帧看:











五种小框
正文旁边常有带标题的小框,标题告诉你它是哪一种。下面就是这五种,每个里面放了一个这份代码里的例子:
标记为已读、搜索
每一章最后有一个按钮“这一章我读懂了,标记为已读”。点了以后,左边目录里这一章后面的小圆圈会变成实心,目录顶上的进度条会变长,下面路线图里这一章的卡片也会变色。这些只记在你自己的浏览器里。
想找一个类或一章,用最上面的搜索框(在页面空白处按 / 也能直接开始打字)。右上角的按钮可以换浅色 / 深色。
学习路线
全部 17 章按顺序读大约 10 小时。每一张卡片写着这一章讲什么、大约要多久,点卡片就进去:
关于截图
所有截图都是真的运行你交上去的这份代码截下来的。启动的方法和你的 CMain 一样:读数据文件 mokepon.game,把里面的内容变成 Mokepon、地图和图片的对象,再 new 出 CGameWindow,打开窗口(这段代码在 第 5 章一行一行讲)。
窗口造好以后,就像你玩游戏一样:按方向键、点按钮、点菜单项,每做一步截一张图,同时记下那一刻程序里的真实数据(玩家在哪个格子、血量多少、日志写了什么)。所以图上的每个数字,和章节里讲的数字是对得上的。
游戏里有随机:每次踩上感叹号,都会重新随机挑一只敌人;出招打不打得中也是随机的。所以不同截图里的敌人名字可能不一样,这是正常的。比如同一个感叹号 (17,17),同一局里踩上去两次:

- 1第一次:Hexwing

- 1第二次:Galecrest
随机也有范围。(17,17) 是一个绿色感叹号,绿色感叹号只在“在这块地形上吃亏”的 Mokepon 里随机挑。这一格是 FOREST(森林),在 FOREST 上吃亏的只有 LIGHTNING 类型,所以两次挑到的都是 LIGHTNING 类型的 Mokepon,只是名字不同(第 9 章)。
打中没打中也是随机的。下面是这一局战斗日志的放大图:敌人 Galecrest 用 Tackle 打你,没打中:

- 1Enemy Galecrest used Tackle! 和 But they missed.
关于代码
每一个代码框都是从你的源文件里原样剪下来的,行号就是文件里的真实行号,你可以在 IntelliJ 里打开同一个文件,跳到同一行对照。拿上面 SelectScreen.select 那个代码框来看:
- 框顶上:左边是标题,右边是文件路径(从
au/edu/unimelb/swen/oop/mokepon/后面算起,比如ui/SelectScreen.java)和“第几到第几行”。 - 左边灰色的数字是行号。绿色底的行是这一段的重点(第 149、152–153、162–166 行)。行号旁边的红色数字是标号,对应截图上编号相同的那个标注;没有截图的地方,对应旁边文字里的同号说明(比如下面 UML 那一节的标号 1–3)。
- 有的代码框中间有一行
⋯ 省略第 29–100 行这样的字,表示那几行跳过了没放。下面“老师打印的 UML 在哪”那一节的代码框里就有一处。 - 代码框太长或太宽时,框里面可以上下、左右滚动。
- 代码框的字体会把几对字符画成一个符号:减号加大于号画成一个箭头,感叹号加等号画成不等号(比如上面第 144 行的
selectedButton != null),一对尖括号画成一个小菱形。真实代码里它们都是两个普通字符,打代码时照常打 - 和 >、! 和 =、< 和 >。
讲的永远是你交上去的代码真的做了什么,不是“一般应该怎么写”。
老师打印的 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。
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 }
对照着看:UML 字段区的 10 行都是红色小方块,因为代码里 10 个字段都写了 private;showScreen(GameScreen): void 前面也是红色方块(实心,因为是方法),因为它是 private void showScreen(...);其他方法是绿色圆点(public)。注意:代码里有 7 个字段还写了 final(只能赋值一次),UMLDoclet 的图上不显示 final,所以光看 UML 看不出哪个字段是 final。图里所有的符号怎么读,第 3 章一个一个讲。
PDF 每一页大致放了这些(按包排):
| 页 | 内容 |
|---|---|
| 1 | CMain,battle 包(CBattle、CFighter、IEnemyStrategy……),data 包的 CMapParser |
| 2 | data 包的其余类,effect 包 |
| 3 | item 包,model 包 |
| 4 | skill 包(ASkill、三个抽象子类 ATargetedSkill / AAttackSkill / ABoostSkill、10 个技能、CSkillCatalogue) |
| 5 | ui:CGameWindow、AGameScreen、COverworldScreen、CBattleScreen、CGameMenu、IGameNavigator |
| 6 | ui:CSelectScreen、CGameButton、CStatBar、CUiStyle…… |
| 7 | ui:CFighterPanel、CMapView、CModalDialog……;world:COverworld、CEncounter |
| 8 | world 包的其余类(CPlayer、CGameMap、CMarker……) |
准备好了就从 第 1 章 先玩一局:用真实截图认识游戏 开始。