01先玩一局:用真实截图认识游戏
不看代码,先把游戏从头玩到尾,认出每个画面、每个按钮是哪个类画出来的
- 说出游戏的三个画面分别是哪个类画的:CSelectScreen、COverworldScreen、CBattleScreen
- 指着截图上的任何一块(血条、按钮、日志框、感叹号、卷轴……),说出是哪个类、哪个字段负责它
- 算出地图上的格子 (10,18) 为什么在截图的 (640, 641)
- 说出是哪些按钮和菜单项让画面切换:按钮 Choose Mokepon、Fight!、Back to it!、Return to Main Menu,Game 菜单里的菜单项 Run away、Main menu
- 说出 CGameWindow 用哪几个字段记着画面,为什么没有一个
BattleScreen类型的字段 - 用一句英语说清楚:GameWindow shows one GameScreen at a time.
这一章先不读 Java 代码。我们把游戏真的从头玩到尾:选一只 Mokepon,在地图上走,捡一个问号箱,碰到敌人,打一仗,最后走到出口。每一张图都是程序真实运行时截下来的。图上的红框和红色数字告诉你:这一块是哪个类画的,存在哪个字段里。以后的章节打开代码时,你已经知道每个类在屏幕上是哪一块了。
先认识几个词。后面每一章都会用到,这里只说最少的意思(Java 语法本身放在 第 2 章):
- 类(class):一段负责一件事的代码。比如 CBattleScreen 负责把战斗画出来,CBattle 负责战斗的规则。类名的第一个字母是大写的。类名前面的小圆圈 C / A / I / E 和老师打印的 UML 图上的圆圈是一个意思(第 3 章细讲)。
- 对象(object):程序运行时,按一个类造出来的一个具体的东西。一个类可以造很多个对象。比如战斗画面上你的卷轴和敌人的卷轴,是同一个类 CFighterPanel 造出来的两个对象。
- new:造一个新对象的写法。
new BattleScreen(...)就是“按 BattleScreen 这个类,造一个新的战斗画面对象”。 - 方法(method):类里面一段有名字、能被叫来执行的代码。写成
对象.方法名(...),括号里是交给它的东西。比如chooseButton.setUsable(false):叫chooseButton这个按钮执行它的setUsable方法,交给它false。这叫调用(call)一个方法。 - true / false:是 / 否。null:一个字段现在什么都没指着(空的)。
1. 打开游戏:选择界面 SelectScreen
运行程序以后,CMain 先读数据文件 mokepon.game,把里面写的 36 只 Mokepon、地图和图片路径变成对象,然后造出窗口 CGameWindow。CGameWindow 放进窗口的第一个画面就是选择界面 CSelectScreen。这段启动过程在 第 5 章。
第一眼看到的画面
12345678- 1
- 2
- 3名字列表:一个能上下滚动的
ScrollPane,里面一列 36 个按钮。现在只看得到前 11 个多一点。它造好就放进画面,没有存进字段 - 4其中一个名字按钮:CGameButton(项目自己写的按钮类),一只 Mokepon 一个
- 5灰色的圆
Circle(也没有存进字段)。中间放头像的SelectScreen.portraitView(ImageView)现在是空的 - 6名字一行
SelectScreen.nameLabel:还没选,所以写着 Select a Mokepon - 7卷轴上的数值表:左边一列是名字(HP: Energy: …),右边一列是值,现在全是空的
- 8Choose Mokepon 按钮
SelectScreen.chooseButton(也是 CGameButton),现在是灰的
这张图上的东西,除了第 1 块菜单栏,全都是 CSelectScreen 自己造出来的。造的时间是 new SelectScreen(...) 的那一刻,也就是运行它的构造器(constructor)的时候。构造器就是 new 一个对象时自动运行的那段代码。它先把以后还要改的部件造好,存进字段(头像框 portraitView、名字 nameLabel、类型图标 typeIcon、Choose 按钮 chooseButton、技能和类型的值);然后调用 addToView(...),按下面的顺序把东西放进画面:灰色圆、头像框、标题、名字列表、名字那一行、卷轴图片、Choose 按钮、数值表。后放的画在上面,所以头像会盖在灰色圆上。灰色圆、标题、名字列表、卷轴图片以后都不用再改,所以造好就直接放进画面,没有字段存着它们。
最底下那层棕色背景放得更早。CSelectScreen 构造器的第一行是 super(navigator):先运行父类的构造器。三个画面类 CSelectScreen、COverworldScreen、CBattleScreen 都继承(extends)同一个父类(superclass) AGameScreen:父类里写好的字段和方法,三个子类自动都有。AGameScreen 的构造器第一个把棕色背景放进画面,所以三个画面的背景一样。AGameScreen 是抽象类(abstract class)(圆圈里是 A),它自己不能被 new,只能当父类用。
菜单栏不属于任何一个画面。它是 CGameMenu 造的,CGameWindow 把它放在窗口顶上。不管换到哪个画面,它都在。
点一个名字:右边的内容出现了
12345- 1被点的 Nightwing 按钮变亮了:
setHighlighted(true)让浅灰背景更实(不透明度 0.5 → 0.75),在棕色背景上看起来更亮。字段selectedButton记住它 - 2头像
portraitView:换成 Nightwing 的图片 - 3名字
nameLabel:Nightwing。后面没有图标,因为它是 NORMAL 类型 - 4右边一列的值:六个数值
statValues(155、27、68、83、42、86,一个数一个Label)、技能skillsValue、类型typeValue - 5Choose Mokepon 亮了,现在能点
点名字按钮,会运行 CSelectScreen 里的 select 方法。它做的事,就是你在图上看到的变化:
- 把上一个高亮的按钮变回普通,把 Nightwing 按钮变亮(标注 1)。
- 把这只 Mokepon 记在字段
selectedMokepon里,等一下 Choose 按钮要用。 - 让 Choose 按钮变回能点(标注 5)。
- 换头像、换名字(标注 2、3)。
- 只有带类型图片的 Mokepon 才显示类型图标(下一小节)。
- 把六个数值、技能和类型写进卷轴(标注 4)。
卷轴上的数字是从哪来的?就是数据文件 mokepon.game 第 56–65 行 Nightwing 那一段:
| 数据文件里写的 | 卷轴上显示的 |
|---|---|
mokepon.5.hp=155… mokepon.5.dodge=86 | HP 155、Energy 27、Strength 68、Accuracy 83、Defence 42、Dodge 86 |
mokepon.5.skills=Lance|Aim|Pocket Sand|Wait | Lance, Aim, Pocket Sand, Wait(竖线变成了逗号,是 formatSkills 用 ", " 把技能名连起来的) |
mokepon.5.type=NORMAL | NORMAL |
名字后面的小火苗:类型图标

- 1NORMAL 的 Nightwing:名字后面什么都没有
- 2Type: NORMAL

- 1FIRE 的 Impike:名字后面多了火焰图标
typeIcon(ImageView) - 2Type: FIRE
为什么一个有图标、一个没有?数据文件第 397–400 行只给了四种类型的图片:fire、ice、water、lightning,没有 normal。CSelectScreen 先问 images.hasTypeImage(type):有图片就显示 typeIcon,没有就把它藏起来。藏起来的图标也不占位置,所以 NORMAL 的名字还是居中的。
把这一节看到的东西整理一下。CSelectScreen 用字段存着以后还要改的部件(下图下面一排)。名字列表和灰色圆造好就放进了画面,没有字段指着它们(上面两边)。36 个名字按钮也没有一个个存进字段,只有被点的那一个记在 selectedButton 里,下次点别的名字时好把它变回普通颜色。
2. 进入地图:OverworldScreen
选好 Impike,点 Choose Mokepon。这个按钮做的事,是请窗口开始一局新游戏:调用 startGame 方法,把选好的 Mokepon 交过去。不过画面里没有一个直接指向 CGameWindow 的字段。它手里拿的是一个接口(interface) IGameNavigator,存在父类 AGameScreen 的字段 navigator 里。CGameWindow 实现(implements)了这个接口,所以最后真正运行的是 CGameWindow 的 startGame。在打印的 UML 上,要找的是 AGameScreen 的字段 navigator: GameNavigator(为什么这样绕一下,第 6 章讲)。startGame 按顺序做四件事:
new Overworld(...):造这一局的“地图世界”,里面有玩家、地图上剩下的物体和走路的规则。new OverworldScreen(...):造负责把地图画出来的画面。world.addListener(overworldScreen):把这个画面登记给 COverworld。以后地图上出了事(玩家走了一步、捡到东西……),COverworld 就会通知它(第 3 节会看到)。showScreen(overworldScreen):把窗口中间换成这个新画面。
地图画面上的每一块
12345678910- 1整张地图:CMapView 的
pane,20×20 个格子,每格 32 像素,一共 640×640 - 2玩家:
MapView.playerView(一张白色星形图片),站在起点 (10,18) - 3白色感叹号:一个 CMarker(random 组)
- 4红色感叹号:也是 CMarker(strong 组)
- 5绿色感叹号:也是 CMarker(weak 组)
- 6问号:一个 CMysteryBox(问号箱),里面装着一个道具
- 7洋红色的门:CExit,出口
- 8右上角的卷轴:CPlayerStatsScroll(字段
statsScroll) - 9白色状态栏
statusBar(一个Label),现在没有字 - 10四个方向按钮
moveButtons:4 个圆形 CGameButton,和方向键一样能走路
记住一件事:地图画面上的东西都只是“画”出来的,它们不管规则。能不能往前走、踩到什么会发生什么,是 world 包里的 COverworld、CMarker、CMysteryBox、CExit 决定的。COverworldScreen 的类注释原话是:它 “only shows what happens; the rules for moving live in the Overworld class”。
图里的 CMapView 还画了最底下那层地形格子(草地、小路、石头、水……),每个格子一张 32×32 的小图。玩家图片是最后加的,所以它总在物体上面。COverworldScreen 自己不判断能不能走:玩家要走时,它调用 world.move(...),把事情交给字段 world 指着的 COverworld。
三种颜色的感叹号、问号箱和门
地图上每个物体放在哪、是什么颜色,都写在数据文件里。这是本章唯一一段“代码”:数据文件 mokepon.game 的几行。先看懂它的写法:
- 每一行是
名字=值。比如map.player.startingPos=10,18:名字是map.player.startingPos(玩家的起点),值是10,18。 10,18这样两个数,就是一个格子(第 10 列、第 18 行),下面一小节细讲。map.mokeponRandom.0、.1、.2、.3:同一组里的第 0、1、2、3 个感叹号。.image是这一组用的图片文件,.colour是给图片染的颜色,.type是问号箱里装的道具。
代码里的红色数字和截图上的红色数字一一对应。
4321map.player.startingPos=10,18⋯ 省略第 433–438 行439map.mokeponRandom.0=3,3440map.mokeponRandom.1=14,6441map.mokeponRandom.2=12,12442map.mokeponRandom.3=4,10443map.mokeponRandom.image=assets/sprite_exclamation.png4442map.mokeponRandom.colour=white445 446map.mokeponStrong.0=5,4447map.mokeponStrong.1=16,4448map.mokeponStrong.2=12,16449map.mokeponStrong.image=assets/sprite_exclamation.png4503map.mokeponStrong.colour=red451 452map.mokeponWeak.0=2,12453map.mokeponWeak.1=6,16454map.mokeponWeak.2=17,17455map.mokeponWeak.image=assets/sprite_exclamation.png4564map.mokeponWeak.colour=lime457 458map.exit.image=assets/sprite_door.png4595map.exit.colour=magenta460map.exit.location=6,0461 4626map.items.image=assets/sprite_question.png463map.items.0.type=HEALTH464map.items.0.location=3,12465map.items.1.type=POISON466map.items.1.location=18,16467map.items.2.type=COFFEE468map.items.2.location=2,2
- 1起点 (10,18):第 432 行
startingPos=10,18 - 2白色 4 个:第 439–442 行的 (3,3)、(14,6)、(12,12)、(4,10),第 444 行
colour=white - 3红色 3 个:(5,4)、(16,4)、(12,16),第 450 行
colour=red - 4绿色 3 个:(2,12)、(6,16)、(17,17),第 456 行
colour=lime - 5门:第 459 行
colour=magenta,第 460 行位置 (6,0) - 6问号箱 3 个:第 462 行的图片,第 463–468 行是 (3,12) HEALTH、(18,16) POISON、(2,2) COFFEE
三组感叹号用的是同一张图片(第 443、449、455 行都是 sprite_exclamation.png),只是 CMapView 按 colour 给它们染了不同的颜色。门也染了色(洋红色)。问号箱不染色。颜色不同,挑敌人的方法也不同:
| 你看到的(数据文件里的组) | 是哪个类 | 踩上去会怎样 |
|---|---|---|
白色感叹号 ×4map.mokeponRandom | CMarker + CRandomEnemyPicker | 36 只里随便挑一只当敌人 |
红色感叹号 ×3map.mokeponStrong | CMarker + CTerrainEnemyPicker(ADVANTAGE) | 挑一只在这块地形上占优势的敌人(强敌) |
绿色感叹号 ×3map.mokeponWeak | CMarker + CTerrainEnemyPicker(DISADVANTAGE) | 挑一只在这块地形上吃亏的敌人(弱敌) |
问号 ×3map.items | CMysteryBox | 捡到一个道具:(3,12) HEALTH、(18,16) POISON、(2,2) COFFEE |
门 ×1map.exit | CExit | 游戏结束 |
挑敌人的细节在 第 9 章。这里只要记住:颜色只是样子,真正决定挑哪只敌人的是 Marker 里存着的那个 picker。
格子坐标怎么变成截图上的位置

- 1左上角第一格 (0,0):截图上的 (320, 65)
- 2门 (6,0):第 6 列、第 0 行
- 3玩家起点:第 10 列、第 18 行
- 4问号箱:第 18 列、第 16 行
- 5右下角最后一格 (19,19)
位置写成 (列, 行),比如 (10,18) 就是“第 10 列、第 18 行”。注意行号是从上往下数的,所以 18 很靠下。换成截图上的像素,只要一个公式:
截图 x = 320 + 列 × 32 截图 y = 65 + 行 × 32
| 格子 | x = 320 + 列 × 32 | y = 65 + 行 × 32 | 截图里它的左上角 |
|---|---|---|---|
| (0,0) 左上角 | 320 + 0 = 320 | 65 + 0 = 65 | (320, 65) |
| (6,0) 门 | 320 + 192 = 512 | 65 + 0 = 65 | (512, 65) |
| (10,18) 起点 | 320 + 320 = 640 | 65 + 576 = 641 | (640, 641) |
| (18,16) 问号箱 | 320 + 576 = 896 | 65 + 512 = 577 | (896, 577) |
320 和 65 是怎么来的?
- 320:窗口宽 1280,地图宽 20 × 32 = 640。COverworldScreen 让地图左右居中:(1280 − 640) ÷ 2 = 320。
- 65:COverworldScreen 把地图放在游戏区往下 40 像素的地方(常量
MAP_Y = 40)。截图多了上面 25 像素的菜单栏,所以是 40 + 25 = 65。 - × 32:CMapView 的常量
TILE_SIZE = 32。它把每张小图放在“列 × 32、行 × 32”的位置(方法placeOnTile)。
每个格子在截图上的位置,只在 CMapView 的 placeOnTile 里算。COverworldScreen 只用 TILE_SIZE 算整张地图放在哪(左右居中),以及地图下面的状态栏放多低。规则类(CPosition、CPlayer、COverworld)只认识格子,不知道像素。
右上角的卷轴 PlayerStatsScroll

- 1Mokepon: Impike(你选的那只)
- 2六个数值:150、22、76、74、45、70,和选择界面一模一样
- 3Fights: 0——赢了几场,字段
fightsValue - 4Inventory(背包):三种道具各有几个,字段
itemValues,现在都是 0
CPlayerStatsScroll 里有一句注释:“The Mokepon's stats never change on the overworld, so they are written once.” 所以六个数值只写一次;会变的只有 Fights 和三个道具数量,它们在 update() 里重新写。记住这里的 HP 是 150,后面打仗时你会看到 225。
3. 走路,捡到一个问号箱
按方向键(或者点右下角的方向按钮),玩家一次走一格。下面是真实的一串截图:从起点往右走 8 格,再往上走 2 格,正好踩到 (18,16) 的问号箱。点“播放”看它动起来。











最后一步前后,放在一起比一比:

- 1问号箱还在 (18,16)
- 2玩家在 (18,17)
- 3↑ 按钮能点

- 1问号箱不见了,玩家站在 (18,16) 这一格
- 3↑ 按钮变灰:再往上 (18,15) 是 BARRIER(墙),走不过去

- 4状态栏
statusBar:空的

- 4状态栏:You picked up a Poison!(COverworldScreen 的
onItemCollected写上去的)

- 5卷轴里 Poison: 0

- 5Poison: 1
这一步背后发生了什么?用大白话说:
- 按键先到 CGameWindow:它在窗口上装了一个“按键过滤器”,任何按键都先交给当前画面的
handleKeyPress。当前画面是 COverworldScreen,它把“往上”交给 COverworld 的move。 - COverworld 检查能不能走,能走就让 CPlayer 对象换到新格子(
player.moveTo(...)),然后通知画面“玩家动了”。 - 新格子上有一个 CMysteryBox。COverworld 让它自己决定怎么办(方法
interact)。问号箱的做法是:请 COverworld 把自己从地图上拿掉,把道具 Poison 放进背包。 - COverworld 再通知画面“捡到了 Poison”。画面在状态栏写
You picked up a Poison!,然后重画地图(箱子没了)和卷轴(Poison 变 1),并重新算四个方向按钮能不能点(↑ 灰了)。
下面这张图把同一件事画成顺序图(sequence diagram)。这是网站里第一张顺序图,先学怎么看:
- 最上面一排方框是参加的对象,每个方框下面拖着一根竖的虚线,叫生命线。
player : Player的意思是“一个 CPlayer 类的对象,名字叫 player”。 - 带底色、没有箭头的小框是说明,不是调用。
- 从上往下是时间顺序。箭头上的小数字就是先后:1、2、3……
- 实线箭头 = 调用对方的方法,箭头上写着方法名和括号里交过去的东西。
- 绕回自己的箭头 = 这个对象调用自己的方法。
第 4 和第 7 个箭头是 COverworld 在通知画面(第 7 个交过去的 item 就是箱子里的 Poison)。它为什么知道要通知 COverworldScreen?因为第 2 节 startGame 的第 3 步 world.addListener(overworldScreen) 已经把这个画面登记给了 COverworld。这种“先登记,出事时收到通知”的对象叫监听者(listener),这种设计叫观察者模式(Observer),第 8 章细讲。
4. 踩到感叹号:遇敌对话框
从 (18,16) 往下走一格到 (18,17),再往左一格,就踩上了 (17,17) 的绿色感叹号。画面上弹出一个框:
123- 1半透明的黑色一层
layer:盖住整个画面,后面的东西都点不到 - 2白色的框:CModalDialog(模态对话框)的构造器里造的一个
VBox。它只是构造器里的局部变量box,不是字段。整个 CModalDialog 对象 = 黑色的layer+ 里面这个框。下面放大看 - 3玩家站在 (17,17) 的绿色感叹号上(感叹号被玩家图片盖住了)

- 4标题:Encountered a Hexwing(遇到了一只 Hexwing)
- 5两条地形提示
messages:你占优势,敌人吃亏 - 6Fight! 按钮:开打
- 7Run Away! 按钮:不打,退回去
这个框是 COverworldScreen 收到 COverworld 的通知 onEncounter 以后造的:标题写 “Encountered a ” 加敌人名字,每条地形提示加一行,再加两个按钮,最后调用父类的 showDialog 把它盖到画面上。框开着的时候,按方向键也没用:三个画面共同的父类 AGameScreen 收到按键时,先看有没有对话框开着,开着就什么都不做(这种“父类先统一检查、再交给各画面自己处理”的写法叫模板方法(Template Method),第 6 章细讲)。
两条提示是怎么算出来的

- 1FOREST(深绿色):数据文件第 418–421 行里的字母 F(第 16、17 行的第 16–19 列,第 18、19 行的第 12–19 列)
- 2绿色感叹号 (17,17) 就在这片森林里
- 3玩家在 (18,17),下一步往左就踩上去
(17,17) 这一格的地形是 FOREST(森林,地图右下角那片深绿色)。每种类型都有一块“强项地形”和一块“弱项地形”(EMokeponType):
| 谁 | 类型 | FOREST 对它来说 | 框里的提示 | 战斗里的数值 |
|---|---|---|---|---|
| 你:Impike | FIRE | 强项地形 → ADVANTAGE | Your mokepon will be at an advantage here! | ×1.5 |
| 敌人:Hexwing | LIGHTNING | 弱项地形 → DISADVANTAGE | Your enemy will be at a disadvantage here! | ×0.5 |
如果是 NORMAL 类型,或者地形不是它的强项也不是弱项,就是 NEUTRAL(不变),这时不写提示。所以有些遇敌框里一条提示都没有。绿色感叹号专挑“在这里吃亏”的敌人:在 FOREST 上吃亏的是 LIGHTNING 类型,数据文件里一共 5 只(Hexwing、Voltusk、Skydart、Galecrest、Stormtalon),每次从里面随机挑一只。
先点 Run Away!,再踩一次

- 1点了 Run Away!:玩家退回上一格 (18,17)
- 2绿色感叹号还在 (17,17)

- 3再踩一次:这次是 Galecrest,换了一只
- 4提示一样:Galecrest 也是 LIGHTNING
点 Run Away! 只是关掉框,再让 COverworld 把玩家退回上一格(CPlayer 一直记着“上一格”在哪)。感叹号不会消失,所以可以再踩。每次踩都会重新随机挑敌人,所以第二次变成了 Galecrest。这一次我们点 Fight!。
5. 战斗画面 BattleScreen
点 Fight! 以后,CGameWindow 的 startBattle 做了两件事:请 COverworld 按这次遇敌造一场新的 CBattle(战斗规则),再 new 一个新的 CBattleScreen(战斗画面)放到窗口中间。真正 new 出 CBattle 的是 CEncounter(“这一次遇敌”)的 createBattle:它按地形给双方各造一个 CFighter,再用这两个 CFighter 和背包造 CBattle。每打一场,都是一个新的 CBattleScreen。
从踩上感叹号到战斗画面出来,整条调用链是这样的(读法和第 3 节的顺序图一样):
第 1 步发生在玩家走上 (17,17) 那一刻(和第 3 节问号箱的 interact 是同一个方法名,感叹号的做法不同)。第 3 步 CMarker 用它存着的 picker 挑敌人(第 9 章)。最右边一列是第 4 步造出来、存在 COverworld 的字段 encounter 里的那个 CEncounter 对象。第 7 步和第 2 节一样,是通过接口 IGameNavigator 调到 CGameWindow 的。虚线箭头是“返回”:把造好的 battle 交回给调用的人。第 9 步造 CFighter 和 CBattle 的细节在 第 10 章。
战斗画面上有什么
123456- 1左下的大卷轴:你的 CFighterPanel,字段
playerPanel。里面有名字、头像、HP 条、能量条 - 2右上的卷轴:敌人的 CFighterPanel,字段
enemyPanel - 3三个道具按钮
itemButtons:Health Tonic、Poison、Coffee,下面的数字是还剩几个 - 4Actions 和技能按钮
skillButtons:Impike 有几个技能就有几个按钮 - 5日志框
logArea(一个TextArea):一行一行写战斗经过 - 6还是同一个菜单栏。战斗中 Game 菜单里的 Run away 能点了(第 7 节)
两个卷轴是同一个类 CFighterPanel 造出来的两个对象:一个给你,一个给敌人。CFighterScroll 是卷轴那张图,外加一层平时看不见的红色遮罩(打输的那一方会变红,下面会看到)。注意图里两根条的字段指向的是 CStatBar,不是 CLabelledStatBar——下一小节说为什么。
你的条有数字,敌人的条没有

- 1HP 条:绿色的条 + 写在上面的 225 / 225(字段
numbers,一个Label) - 2能量条:蓝色,33 / 33

- 3敌人的 HP 条:只有条,没有数字
- 4敌人的能量条:也没有数字
条本身是 JavaFX 的进度条 ProgressBar,由 CStatBar 管。CLabelledStatBar 是 CStatBar 的子类(subclass):条的功能全部继承下来,再多放一个写数字的 Label。CFighterPanel 造条的时候看一个开关 showNumbers:给你的面板传 true,造 CLabelledStatBar;给敌人的传 false,造普通的 CStatBar。所以敌人的 HP 你只能看条的长短,猜不到准确数字。
为什么上面的图里 CFighterPanel 只连着 CStatBar?因为它的两个字段 hpBar、energyBar 写的类型都是 CStatBar,你的面板只是往里面装了子类 CLabelledStatBar 的对象。子类的对象可以放进父类类型的字段,这叫多态(polymorphism),第 14 章细讲。

Impike 的 HP 为什么变成了 225

- 1HP: 150
- 2Energy: 22

- 1HP 225 / 225
- 2Energy 33 / 33
选择界面和地图卷轴上 Impike 的 HP 都是 150,战斗里却是 225 / 225。这不是 bug,是地形加成:开打时,每一方的六个数值都按地形乘一个倍数,四舍五入,再限制在合法范围里(HP 1–1000,其他 1–100)。这件事由 CFighter(“参加这一场战斗的 Mokepon”)在它的构造器里做,ETerrainEffect 提供倍数 1.5 / 1 / 0.5。真实数字:
| 数值 | Impike(FIRE):原来 × 1.5 → 战斗里 | Galecrest(LIGHTNING):原来 × 0.5 → 战斗里 |
|---|---|---|
| HP | 150 × 1.5 = 225 → 225 | 185 × 0.5 = 92.5 → 93(四舍五入) |
| Energy | 22 × 1.5 = 33 → 33 | 28 × 0.5 = 14 → 14 |
| Strength | 76 × 1.5 = 114 → 100(最多 100) | 73 × 0.5 = 36.5 → 37 |
| Accuracy | 74 × 1.5 = 111 → 100(最多 100) | 86 × 0.5 = 43 → 43 |
| Defence | 45 × 1.5 = 67.5 → 68 | 59 × 0.5 = 29.5 → 30 |
| Dodge | 70 × 1.5 = 105 → 100(最多 100) | 79 × 0.5 = 39.5 → 40 |
Galecrest 的数字屏幕上看不到(敌人的条没数字),表里是运行时记录下来的真实值。CMokepon 对象本身一直没变,变的是为这一场新造的 CFighter 对象。第 10 章专门讲为什么要有 CFighter。
按钮上的 [4] 和数字

- 1Health Tonic:0 个,所以是灰的
- 2Poison:1 个(就是地图上捡的那个),能点
- 3Coffee:0 个,灰的

- 4Lance [4]:方括号里是这个技能要花的能量
- 5Stun [4]
- 6Wait [-2]:负数的意思是“回 2 点能量”
技能按钮上的字是 CBattleScreen 拼出来的:技能名 + “ [” + 能量消耗 + “]”。道具按钮的图片在上、数字在下,数字就是背包里剩几个;一个都没有就变灰。也可以按数字键 1–4 代替点技能按钮。
打一仗:一步一步看
这一仗我们点了四下:Wait、Stun、道具 Poison、Lance。点“下一步”或者“自动播放”,看每一下以后屏幕上变了什么。每一步左边是整张截图,右边是放大的日志框(框住的是这一步新多出来的几行)。红框编号在 5 步里意思相同:1 日志,2 你的能量,3 敌人 HP,4 敌人能量,5 Poison 按钮,6 红色遮罩,7 对话框。
123- 2你的能量 33 / 33
- 3敌人 HP 满的(93 / 93)
12- 2还是 33 / 33:已经是满的,Wait 回的 2 点加不上去
124- 229 / 33
- 4敌人能量 14 → 9(条短了)
135- 3敌人 HP 只剩一丝:1 / 93
- 5Poison:1 → 0,变灰
12367- 225 / 33
- 3敌人 HP 0
- 6敌人的卷轴变红
- 7You Win 对话框

- 1日志(放大):只有第 1 行

- 1日志(放大):空一行,再多 Wait 的 4 行

- 1日志(放大):空一行,再多 Stun 和 Tackle 的 4 行

- 1日志(放大):空一行,再多 1 行 Poison

- 1日志(放大):Lance 的 2 行、空两行、fainted 和 You win。最上面几行被滚上去了
- 战斗开始。日志里只有一行:
Player chose Impike. Began a fight against enemy Galecrest!。这一行是 CBattle 的构造器一造出来就写进日志的;CBattleScreen 只是把 CBattle 的日志显示在logArea里。 - 点 Wait [-2]。日志先空一行,再多 4 行:
Player's Impike used Wait!、They think about what they will have for dinner.、Enemy Galecrest used Wait!、They wonder if this is a good strategy.。你先出手,敌人再出手,这就是一个回合。能量还是 33 / 33:Wait 的能量消耗写成 -2,意思是回 2 点;但能量最多只能回到开打时的 33,本来就是满的,所以没变(第 10 章)。 - 点 Stun [4]。日志先空一行,再多 4 行:
Player's Impike used Stun!、Galecrest was stunned! Their damage is halved for 3 turns.、Enemy Galecrest used Tackle!、But they missed.。你的能量 33 − 4 = 29;敌人用 Tackle 花了 5 点,14 − 5 = 9,所以它的蓝条变短了。Tackle 没打中,为什么还扣能量?因为技能一用就先扣能量,打没打中是后面才算的(第 12 章)。 - 点道具 Poison。日志先空一行,再多 1 行:
You used your Poison on Galecrest. It devastated them!。Poison 把敌人的 HP 直接变成 1,所以红框 3 里的绿条只剩最左边一小截(CStatBar 规定:只要 HP 大于 0,条至少显示 3%,免得看起来像已经倒下)。注意没有 “Enemy Galecrest used …” 这一行:用道具不给敌人出手的机会。能量也没花,还是 29。 - 点 Lance [4]。日志先空一行,再多:
Player's Impike used Lance!、Hit Galecrest for 83 damage! They started bleeding!,空两行,Enemy's Galecrest fainted!、You win。83 是用双方战斗里的数值算出来的(怎么算,见 第 12 章);第 3 步的眩晕只让 Galecrest 打人减半,不影响你的 Lance。敌人只剩 1 点 HP,被打 83 点就到 0 了(HP 最低是 0,不会变成负数)。敌人倒下了,没机会出手。你的能量 29 − 4 = 25。
看第 5 步放大的日志:最上面一行是 Player's Impike used Stun!。日志比框长的时候,CBattleScreen 每次更新都会自动滚到最底下(它的 showLog 方法),所以最前面 7 行——开头那一行、一个空行、Wait 的 4 行、又一个空行——被滚到上面去了;从 Stun 开始的几行还看得见。
赢了:红色遮罩和 You Win
123456- 1敌人的卷轴变红:CFighterScroll 的
faintMask(红色、35% 不透明的长方形)现在显示出来了 - 2HP 条空了:0 / 93
- 3You Win 对话框:又是一个 CModalDialog
- 4唯一的按钮 Back to it!:回地图
- 5三个技能按钮全灰了:战斗已经结束(
battle.isFinished()是 true) - 6道具按钮也全灰了
每次点按钮以后,CBattleScreen 都会把整个画面和 CBattle 对一遍(它的 update):两根条、红色遮罩、日志、按钮能不能点。发现战斗结束了,就弹出结果框:你赢了是 “You Win”,输了是 “Game Over”(第 7 节有输的截图)。同时它让菜单重新检查一次,于是 Game 菜单里的 Run away 也变灰了:仗都打完了,不能再逃。
6. 回到地图,走到出口
点 Back to it!,CGameWindow 的 winBattle 先告诉 COverworld “这场赢了”(world.winEncounter()),再把原来那个 COverworldScreen 放回窗口中间(showScreen(overworldScreen))。注意不是 new 一个新的:地图上的状态(捡过的箱子、走到哪了)全都还在。

- 1玩家还站在 (17,17)。绿色感叹号没了:赢了以后 COverworld 把这个 CMarker 从地图上拿掉
- 2状态栏:You beat the enemy Galecrest!

- 3HP 还是 150,不是 225:卷轴写的是 Mokepon 本来的数值
- 4Fights: 1
- 5Poison: 0:战斗里用掉了。战斗和地图用的是同一个背包
为什么 HP 又是 150?战斗里那个 225 存在为这一场造的 CFighter 对象里。战斗结束,这个 CFighter 就没人要了。CMokepon 自己从头到尾没有被改过,所以卷轴上还是 150。
接下来往出口走。出口是地图最上面一行的那扇门 (6,0)。下面这串截图每隔几步截一张:












- 1标题:赢了几场就写几(这里是 1)。英文 “1 fights” 是程序原样拼出来的
- 2Return to Main Menu:回到选择界面
- 3Quit:关掉整个程序
门是一个 CExit 对象。玩家踩上去,它请 COverworld 通知画面“到出口了”,并带上赢了几场。COverworldScreen 就弹出这个框。这个框和打输时的 Game Over 框是同一个方法造的(AGameScreen 的 createGameEndDialog):标题不同,按钮都是 Return to Main Menu 和 Quit。
1234- 1名字一行又变回 Select a Mokepon
- 2数值全空了
- 3Choose Mokepon 又灰了
- 4没有哪个名字按钮是高亮的
点 Return to Main Menu,CGameWindow 把这一局整个丢掉(存着这一局的两个字段 world 和 overworldScreen 都设成 null),让 CSelectScreen 清空选择(它的 clearSelection),再把 CSelectScreen 放回中间。选择界面这个对象从程序开始就一直是同一个,只是被清空了。下次点 Choose,又会 new 一个全新的 COverworld:地图上的箱子和感叹号全部回来,背包清零。
7. 其他你会遇到的画面
上面那一局没有碰到的情况,这里用另外几局的真实截图补上。
同一个 Game 菜单,在三个画面里不一样

- 1Run away:灰
- 2Main menu:灰
- 3Exit:能点

- 1Run away:灰
- 2Main menu:能点
- 3Exit:能点

- 1Run away:能点
- 2Main menu:能点
- 3Exit:能点
菜单只有一个(CGameMenu),三个画面共用。每次换画面,CGameWindow 都让菜单问一问当前画面:“你允许逃跑吗?你允许回主菜单吗?”然后把不允许的选项变灰。变灰的选项连快捷键(Ctrl+R、Ctrl+B)也不起作用。
| 画面 | Run away | Main menu | 为什么 |
|---|---|---|---|
| CSelectScreen | 灰 | 灰 | 还没开始玩,没有一局可以丢。CSelectScreen 改写(override)了 canReturnToMainMenu(),返回 false |
| COverworldScreen | 灰 | 能点 | 两个都用父类 AGameScreen 的默认答案:不能逃跑,能回主菜单 |
| CBattleScreen 还在打 | 能点 | 能点 | CBattleScreen 改写了 canRunAway():战斗没结束就能逃 |
| CBattleScreen 已经打完 | 灰 | 能点 | 战斗结束了,canRunAway() 变成 false(上面 You Win 那张图) |
Exit 不归画面管,永远能点。
CGameMenu 只认识父类 AGameScreen,每次都问同样的两个问题;当前画面是哪个子类,就由那个子类自己的方法回答(没改写的,用父类的默认答案)。这也是多态(polymorphism),第 6 章细讲。

能量不够:技能按钮变灰

- 1Tackle [5]:要 5 点能量,只有 0 点,灰
- 2Aim [3]:要 3 点,也灰
- 3Wait [-2]:不花能量(反而回 2 点),还能点
- 4能量 0 / 12
每次更新,CBattleScreen 都一个一个问:“这个技能现在用得起吗?”(CFighter 的 canUse:能量够不够)。用不起的按钮就变灰。所以能量花光的时候,你只能按 Wait 回能量。
战斗中从菜单逃跑

- 1战斗中打开 Game 菜单,点 Run away(或按 Ctrl+R)

- 2回到地图:玩家退回踩感叹号之前的那一格,就在感叹号 (12,12) 左边,也就是 (11,12)
- 3白色感叹号还在 (12,12)
菜单的 Run away 和遇敌框里的 Run Away! 结果一样:退回上一格,感叹号留着,这场不算赢也不算输。区别是时间点:Run Away! 是在开打之前,画面一直是 COverworldScreen;菜单的 Run away 是在开打之后,CGameWindow 还要把画面从 CBattleScreen 换回 COverworldScreen。完整流程在 第 15 章。
用道具:Coffee 和 Health Tonic

- 1Health Tonic:1 个
- 2Coffee:1 个
- 3HP 228 / 280:被 Lance 打掉 26,回合结束又流血掉 26(流血伤害不写进日志)
- 4能量 1 / 10:Tackle [5] 和 Aim [3] 都灰了

- 2Coffee:1 → 0,变灰
- 3HP 不变,228 / 280
- 4能量回满 10 / 10,Tackle 和 Aim 又能点了

- 1Health Tonic:1 → 0,变灰
- 3HP 回满 280 / 280
这两下的日志
You used your Coffee on Ironshell. It restored all your energy!
You used your Health Tonic on Ironshell. It healed you to max hp!
和 Poison 一样,两行后面都没有敌人出手的那一行:用道具不算一个回合。所以这几下之间,敌人 Frostback 一直没动:程序记录的状态里,它的 HP 一直是 345 / 345,能量一直是 9 / 21(图里裁掉了敌人那边)。
三种道具按钮各自的数量来自同一个背包 CInventory,就是地图卷轴下半部分显示的那个。道具在 第 13 章。
输了:Game Over
123- 1这次是你这边变红:玩家的 Owlet 倒下了。红色遮罩盖住的是整个左下卷轴,不只是这个头像
- 2HP 0 / 120
- 3Game Over 对话框(下面放大)

- 4Return to Main Menu:回到选择界面
- 5Quit:关掉程序
这一局是怎么输的?日志里写着:Owlet 先用了 Swole(力量 +5)、Bunker(防御 +6),然后一直按 Wait。Impike 的 Stun 和 Lance 前几次都没打中,第 5 回合 Lance 打中了:Hit Owlet for 45 damage! They started bleeding!。这一回合结束时和下一回合结束时,流血又各掉 45,而且流血的伤害不写进日志,只在第二次流完写一句 The bleeding stopped.(流血在 第 13 章)。所以 HP 是 120 → 75(Lance)→ 30(第一次流血)→ 0(第二次流血)。
红色遮罩用的是同一个规则:谁的 HP 到 0,谁的 CFighterScroll 就变红。两个按钮和出口框一模一样,因为是同一个方法 createGameEndDialog 造的。日志最后两行是 Player's Owlet fainted! 和 You lose。
前面是墙:方向按钮变灰

- 1玩家(这局是 Ironshell)站在 (5,12)
- 2右边一格 (6,12) 是 BARRIER(灰色石墙),不能走
- 3→ 按钮变灰
每次画面更新,COverworldScreen 都对四个方向各问一次 COverworld:“往这边能走吗?”能走就亮,不能走就灰。BARRIER 和 OCEAN(左下角深蓝色那块)是不能走的地形,地图外面也不能走。
8. 总结:画面、类和章节
把这一章看到的东西列成一张表。考试时看到 UML 上的一个类名,就能想起它在屏幕上是哪一块。
| 你在屏幕上看到的 | 负责它的类(字段) | 细讲 |
|---|---|---|
| 整个窗口;决定中间放哪个画面 | CGameWindow | 5、6 |
| 顶上的 Game 菜单 | CGameMenu(GameWindow.menu) | 6 |
| 菜单里哪些变灰 | AGameScreen 的 canRunAway() / canReturnToMainMenu() | 6 |
| 每个画面的棕色背景、对话框、按键 | AGameScreen(三个画面的父类) | 6 |
| 选择界面 | CSelectScreen | 7 |
| 左边一列名字按钮 | CSelectScreen 的 ScrollPane + CGameButton | 7 |
| 头像、名字、类型图标 | SelectScreen.portraitView、nameLabel、typeIcon | 7 |
| 选择界面的数值卷轴 | SelectScreen.statValues、skillsValue、typeValue | 7 |
| Choose Mokepon 按钮 | SelectScreen.chooseButton(CGameButton) | 7 |
| 地图画面 | COverworldScreen | 8 |
| 地形格子 | CMapView(画)+ CGameMap(数据) | 8 |
| 玩家图标 | MapView.playerView + CPlayer(位置) | 8 |
| 白 / 红 / 绿感叹号 | CMarker + CRandomEnemyPicker / CTerrainEnemyPicker | 8、9 |
| 问号箱 | CMysteryBox | 8、13 |
| 出口的门 | CExit | 8、15 |
| 右上角卷轴 | CPlayerStatsScroll(OverworldScreen.statsScroll) | 8 |
| 白色状态栏 | OverworldScreen.statusBar(Label) | 8 |
| 四个方向按钮 | OverworldScreen.moveButtons(CGameButton) | 8 |
| 遇敌框和地形提示 | CModalDialog + CEncounter | 9 |
| 战斗画面 | CBattleScreen | 14 |
| 两个大卷轴 | CFighterPanel(playerPanel / enemyPanel)+ CFighterScroll | 14 |
| HP 条、能量条 | FighterPanel.hpBar / energyBar,字段类型都是 CStatBar;你的面板里装的是子类 CLabelledStatBar(有数字),敌人的是普通 CStatBar | 14 |
| 战斗里的数值(225 这种) | CFighter(不是 CMokepon) | 10 |
| 技能按钮 [能量] | BattleScreen.skillButtons + ASkill | 11、12 |
| 道具按钮和数量 | BattleScreen.itemButtons + CInventory | 13 |
| 日志框 | BattleScreen.logArea(TextArea),内容来自 CBattle 的日志 | 11 |
| 倒下时的红色遮罩 | FighterScroll.faintMask | 14 |
| You Win / Game Over / 出口框 | 都是 CModalDialog。Game Over 和出口框由 GameScreen.createGameEndDialog 造(两个按钮);You Win 是 CBattleScreen 的 showResult 直接 new 的(只有 Back to it!) | 14、15 |
一次只显示一个画面
整个游戏只有一个窗口 CGameWindow。窗口顶上永远是菜单栏,中间一次只放一个画面:CSelectScreen、COverworldScreen 或 CBattleScreen。三个都是 AGameScreen 的子类。先看 CGameWindow 用哪几个字段记着它们(打印的 UML 上 CGameWindow 的字段表里都能找到;另外几个字段 stage、mokepons、map、images、root 和画面无关,这里不画):
selectScreen : SelectScreen:窗口一造好就有,从头到尾是同一个对象。回主菜单时只是被清空。world : Overworld和overworldScreen : OverworldScreen:这一局的地图世界和地图画面。点 Choose Mokepon 时新造,回主菜单时都设成null。currentScreen : GameScreen:现在放在中间的那个画面。它的类型写的是父类 AGameScreen,所以三个子类的对象都能放进去(又是多态)。- 没有
BattleScreen类型的字段。战斗画面只放在currentScreen里;离开战斗、currentScreen换成别的画面以后,就没有字段再指着它了,这个对象也就没用了。下一场仗会 new 一个新的。
换画面只靠一个私有方法 showScreen:记下新画面(currentScreen = screen)、把它放到窗口中间、让菜单重新问一遍。按键也是交给 currentScreen.handleKeyPress(...),CGameWindow 不用知道现在是哪个画面。
下面把整个旅程重放一遍。动画最上面的图里,亮框就是那一刻 currentScreen 指着的画面;会换画面的那一步,让画面切换的那根箭头也会亮(没有箭头亮的步骤,就是画面没换,只是画面自己在更新或弹了个框)。图的下面,左边是那一刻的完整截图,右边是放大的关键部分。
















- 1Choose Mokepon:灰的,还不能点

- 1Impike 和火焰图标
- 2Choose Mokepon 亮了

- 1起点 (10,18)
- 2状态栏是空的

- 1Poison: 1

- 1Fight!
- 2Run Away!(这次先点它)

- 1退回 (18,17)
- 2感叹号还在 (17,17)

- 1刚捡的 Poison:1 个

- 1Back to it!
- 2敌人 HP 0
- 3敌人的卷轴变红

- 1Fights: 1
- 2Poison: 0(用掉了)

- 1Return to Main Menu

- 1Select a Mokepon
- 2又灰了

- 1Run away(Ctrl+R)

- 1退回 (11,12)
- 2感叹号还在 (12,12)

- 1Return to Main Menu

- 1回到 Select a Mokepon
- 程序启动:CGameWindow 的构造器造好菜单和 CSelectScreen(存进字段
selectScreen),再调用showScreen(selectScreen):字段currentScreen(类型是 AGameScreen)现在指着 CSelectScreen,所以它亮了。下面每一步,亮框的那个就是当时currentScreen指着的画面。还没选,Choose Mokepon 是灰的。 - 点 Impike。画面没换,还是 CSelectScreen,只是它把右边的内容换了,Choose Mokepon 亮了。
- 点 Choose Mokepon:CGameWindow new 一个 COverworld 和一个 COverworldScreen,把画面登记给 COverworld(
addListener),再换到地图。玩家在起点 (10,18)。 - 走路、捡到 Poison:画面没换。COverworldScreen 收到 COverworld 的通知,自己重画地图、卷轴和状态栏。
- 踩到 (17,17) 的感叹号:弹出的是一个 CModalDialog,盖在 COverworldScreen 上面。对话框不是画面,所以箭头不亮。
- 先点框里的 Run Away!:框关掉,玩家退回 (18,17)。还是 COverworldScreen。
- 再踩一次,这次点 Fight!:CGameWindow 请 COverworld 造一场 CBattle(真正 new 出两个 CFighter 和 CBattle 的是 CEncounter 的
createBattle),再 new 一个新的 CBattleScreen,currentScreen换成它。 - Wait、Stun、Poison、Lance,打赢了。You Win 框盖在 CBattleScreen 上,画面还没换。
- 点 Back to it!:CGameWindow 告诉 COverworld 赢了,然后把原来那个 COverworldScreen 放回来。Fights 变成 1,Poison 用掉了。
- 走到门 (6,0):COverworldScreen 弹出游戏结束框。又是对话框,画面没换。
- 点 Return to Main Menu:CGameWindow 丢掉这一局,清空选择,换回 CSelectScreen。地图上点菜单的 Main menu,走的也是这根箭头。
- 另一局(玩家选了 Shellurk),正在 CBattleScreen 里。打开 Game 菜单:战斗中 Run away 能点。
- 点 Run away:CGameWindow 让 COverworld 把玩家退回 (11,12),再换回 COverworldScreen。这场战斗直接作废,感叹号还在。,也就是 (11,12)
- 又一局(玩家选了 Owlet),在战斗里输了:你的卷轴变红,Game Over 框盖在 CBattleScreen 上。
- 点 Return to Main Menu:从战斗画面直接回到 CSelectScreen。战斗中点菜单的 Main menu 也走这根箭头。另外,任何时候点 Quit 或菜单的 Exit,整个程序就关掉了。
看完这个动画,你应该能自己说出:只有 6 种操作会换画面(4 个按钮 Choose Mokepon、Fight!、Back to it!、Return to Main Menu,和 Game 菜单里的 2 个菜单项 Run away、Main menu);遇敌框、You Win、Game Over、出口框都只是盖在当前画面上的对话框。每个画面只管“显示”,真正的规则在 COverworld 和 CBattle 里。下一章先补 Java 语法:第 2 章 Java 最小知识包。
自测
截图标注里写着 BattleScreen.playerPanel.hpBar.bar,这串名字怎么读?它在屏幕上是哪一块?
从左往右读“里面的”:CBattleScreen 对象的字段 playerPanel(一个 CFighterPanel,你的卷轴),它的字段 hpBar(类型写的是 CStatBar,你的面板里装的是 CLabelledStatBar 对象),再里面的字段 bar(JavaFX 的 ProgressBar)。在屏幕上就是左下卷轴里那根绿色的 HP 条。
English: It is the ProgressBar inside the LabelledStatBar that the player's FighterPanel uses as its HP bar.
地图上的格子 (18,16) 在截图的哪个像素位置?320 和 65 是怎么来的?
x = 320 + 18 × 32 = 896,y = 65 + 16 × 32 = 577,和截图里问号箱的位置 (896, 577) 一样。320 = (1280 − 20 × 32) ÷ 2,让地图左右居中;65 = 地图离游戏区顶部 40 像素(MAP_Y)+ 菜单栏 25 像素。32 是 MapView.TILE_SIZE。
为什么敌人的血条上没有数字,你的有?
CFighterPanel 造条的时候看 showNumbers:CBattleScreen 给你的面板传 true,造的是 CLabelledStatBar(CStatBar 的子类,多一个写数字的 Label);给敌人的传 false,造的是普通 CStatBar。
English: Only the player's panel uses LabelledStatBar, a subclass of StatBar that also writes the numbers on the bar.
战斗里 Impike 的 HP 是 225 / 225,回到地图卷轴上又写 150。哪个是对的?为什么会不一样?
两个都对。150 是 CMokepon 本来的数值,卷轴 CPlayerStatsScroll 显示的就是它。开打时为这一场新造了一个 CFighter,按地形 FOREST 把 FIRE 类型的 Impike 所有数值乘 1.5:150 × 1.5 = 225。战斗结束,这个 CFighter 被丢掉,CMokepon 从来没被改过。
English: The battle stats live in a Fighter object made for that one battle; the Mokepon itself never changes.
为什么选择界面的 Game 菜单里 Main menu 是灰的,地图上却能点?Run away 又是什么时候能点?
菜单只有一个 CGameMenu,每次换画面都会问当前画面。AGameScreen 的默认答案是“不能逃跑、能回主菜单”。CSelectScreen 改写了 canReturnToMainMenu() 返回 false(还没有一局可丢);CBattleScreen 改写了 canRunAway(),战斗没结束时返回 true。所以 Run away 只有在战斗进行中能点。
English: Each GameScreen decides which menu options it allows, and GameMenu.update() asks the current screen.
踩到感叹号弹出遇敌框,算不算换画面?一共有哪几种操作会换画面?
不算。遇敌框是一个 CModalDialog,盖在 COverworldScreen 上面,CGameWindow 的 currentScreen 没变。会换画面的只有:Choose Mokepon(选择 → 地图)、Fight!(地图 → 战斗)、Back to it! 和 Run away(战斗 → 地图)、Main menu 和 Return to Main Menu(回到选择界面)。
English: GameWindow shows one GameScreen at a time; dialogs are drawn on top of the current screen.
CGameWindow 有没有一个 BattleScreen 类型的字段?那它怎么记住正在显示的战斗画面?
没有。CGameWindow 跟画面有关的字段是 selectScreen : SelectScreen、overworldScreen : OverworldScreen 和 currentScreen : GameScreen。战斗画面只放在 currentScreen 里:这个字段的类型是父类 AGameScreen,所以子类 CBattleScreen 的对象也能放进去(多态)。每打一场都 new 一个新的 CBattleScreen,因为它的字段 battle 是 final 的,一个画面只对应一场仗。
English: GameWindow has no BattleScreen field; the battle screen is only held in currentScreen, which is declared as GameScreen, and a new BattleScreen is made for every battle.
遇敌框里的 Run Away! 和战斗中菜单里的 Run away,结果有什么一样、什么不一样?
一样的是:都退回踩感叹号之前的那一格,感叹号留在地图上。不一样的是时间点:Run Away! 在开打之前,画面一直是 COverworldScreen,只是关掉对话框;菜单的 Run away 在战斗中,CGameWindow 还要把画面从 CBattleScreen 换回原来的 COverworldScreen。