随时查REF
术语表
中英对照的 Java / OOP / JavaFX 术语
这里按主题列出全站用到的术语:每条有中文名、英文名(考试用英文,要认得)和一句定义,大多数还附上它在你代码里的例子。每一页上带点状下划线的词(比如 封装),鼠标停上去能看定义,点一下就跳到这里对应的那一条;想直接找某个词,用页面顶上的搜索框,或按 Ctrl+F。
集合与泛型
- ArrayList(可变长列表)ArrayList
- 最常用的 List 实现,可以随时 add 和 remove,长度自动变化。
在你的代码里:CBattle 的private final List<String> log = new ArrayList<>();,每一步log.add(...)加一行日志;COverworld 用new ArrayList<>(map.getObjects())复制出一份自己可以删减的物体列表。 - 自动装箱和包装类autoboxing / wrapper class
- 集合里只能放对象,不能放 int 这样的基本类型,所以有对应的包装类 Integer、Double 等。int 放进集合时 Java 自动包成 Integer(装箱),取出来时自动变回 int(拆箱)。
在你的代码里:CFighter 的Map<Stat, Integer> stats:stats.put(Stat.HP, Math.max(0, getHp() - amount))把 int 自动装箱;getStat里的return stats.get(stat);又自动拆箱成 int。 - 遍历时修改集合ConcurrentModificationException
- 用 for-each 遍历一个 ArrayList 时,如果循环里往这个列表加或删元素,Java 通常会抛出 ConcurrentModificationException。解决办法:用迭代器的 remove,或者遍历一份复制出来的列表。
在你的代码里:COverworld.move 写的是for (MapObject object : new ArrayList<>(objects)),注释说明了原因:被踩到的 CMysteryBox 会通过collect把自己从objects里删掉。 - List.copyOf / Map.copyOf(复制成不可修改)List.copyOf / Map.copyOf
- 把一个集合复制成一份新的、不可修改的集合。原来的集合以后再变,这份复制品也不受影响。
在你的代码里:this.skills = List.copyOf(skills);(CMokepon)、this.objects = List.copyOf(objects);(CGameMap)、this.typeImages = Map.copyOf(typeImages);(CGameImages)。 - EnumMap(以枚举为键的 Map)EnumMap
- 键只能是某一种枚举的 Map。创建时要写明是哪个枚举(例如 Stat.class),遍历时按枚举常量声明的顺序,速度也很快。
在你的代码里:CFighter 的stats = new EnumMap<>(Stat.class)存六个属性的当前值;CMokepon 构造器用new EnumMap<>(stats)复制一份;COverworldScreen 的moveButtons是“方向 → 按钮”的 EnumMap。 - 泛型方法generic method
- 方法自己声明类型参数(写在返回类型前面的尖括号里),同一个方法可以处理多种类型;extends 可以给类型参数加限制,例如必须是某种枚举。
在你的代码里:CImageParser 的private <T extends Enum<T>> Map<T, String> readImages(GameData data, String prefix, T[] values):同一个方法分别读出 EMokeponType、ETerrain、EDirection 三种枚举的图片路径。 - 泛型generics
- 在尖括号里写明容器装的是什么类型,例如列表后面写 Skill 就只能放 Skill。编译器会检查放进去的类型,取出来时也不用再强制转换。右边可以只写一对空尖括号(菱形写法),类型从左边推出来。
在你的代码里:Map<Stat, Integer> stats(CFighter)、List<OverworldListener> listeners(COverworld);new ArrayList<>()里的空<>就是菱形写法。 - getOrDefaultgetOrDefault
- Map 的方法:有这个键就返回它的值,没有就返回你给的默认值,避免得到 null。
在你的代码里:CInventory.getCount 的return counts.getOrDefault(item, 0);:背包里没有记录的道具算 0 个。 - HashMapHashMap
- 最常用的 Map 实现,查找很快,但不保证元素的顺序。它用键的 hashCode 和 equals 来查找。
在你的代码里:CMapView 的objectViews = new HashMap<>()存“地图物体 → 它的图片”,loadedImages存“图片路径 → 已加载的图片”,让每个图片文件只加载一次;CImageParser.readImages 也用 HashMap。 - 迭代器 Iteratoriterator
- 一个一个取出集合元素的对象:hasNext() 问还有没有,next() 取下一个,remove() 删除刚取出的那个。边遍历边删除时要用它,for-each 做不到。
在你的代码里:CFighter.countDownEffects:Iterator<StatusEffect> iterator = effects.iterator();,while (iterator.hasNext())让每个效果倒数一回合,结束了就iterator.remove()。 - keySet(Map 里所有的键)Map.keySet()
- Map 的方法:返回这个 Map 里所有的键。它不是复制品,而是连着原来那个 Map 的“窗口”;想交给别人又不让别人改,就再复制一份。
在你的代码里:CInventory.getItems 的return List.copyOf(counts.keySet());:先拿出三个道具对象(键),再复制成一个不能修改的列表交出去。因为 counts 是 LinkedHashMap,列表的顺序就是放进去的顺序 Health Tonic、Coffee、Poison。 - LinkedHashMap(保持顺序的 Map)LinkedHashMap
- 和 HashMap 一样能按键快速查找,但会记住放入的先后顺序,遍历时按这个顺序取出。
在你的代码里:CInventory 的counts = new LinkedHashMap<>()按 Health Tonic、Coffee、Poison 的顺序放入,所以地图右上角的卷轴也按这个顺序列出道具;CBattleScreen 的itemButtons也是 LinkedHashMap,顺序是战斗按钮的 Health Tonic、Poison、Coffee。 - 列表 ListList
- 有顺序、可以重复、按下标 0、1、2… 取元素的集合。List 本身是接口,真正的对象可能是 ArrayList,也可能是 List.of 造出的不可修改列表。
在你的代码里:CMokepon 的List<Skill> skills:顺序就是战斗界面技能按钮从上到下的顺序,CBattleScreen.updateSkillButtons 用skills.get(i)对应第 i 个按钮。 - indexOf(在列表里找位置)List.indexOf
- 在列表里从头找一个元素,返回它排在第几(从 0 数);列表里没有这个元素就返回 -1。
在你的代码里:CBattleScreen.handleKey 里的SKILL_KEYS.indexOf(event.getCode()):列表是[DIGIT1, DIGIT2, DIGIT3, DIGIT4],按 2 得到 1,按字母 A 得到 -1,所以第 111 行要先检查index >= 0。 - List.of / Map.of(不可修改的集合)List.of / Map.of
- 用几个值一次造出一个不可修改的列表或映射。之后再调用 add、put、remove 会抛出 UnsupportedOperationException。
在你的代码里:CSkillCatalogue 的SKILLS = List.of(new Bite(), new Tackle(), ...)一共 10 个技能对象;CBleeding 返回List.of("The bleeding stopped.");COverworldScreen 的ARROW_KEYS = Map.of(KeyCode.UP, Direction.UP, ...)。 - 映射 MapMap
- 存“键 → 值”配对的集合,用键查值。同一个键只能对应一个值,put 同一个键会覆盖旧值;get 一个不存在的键得到 null。
在你的代码里:CInventory 的Map<Item, Integer> counts存“道具 → 数量”,add就是counts.put(item, getCount(item) + 1);COverworldScreen 的ARROW_KEYS.get(event.getCode())遇到非方向键得到 null,所以后面写if (direction != null)。 - Map.Entry(键值对)Map.Entry
- Map 里的一对键和值。用 entrySet() 可以遍历所有键值对,getKey() 取键,getValue() 取值。
在你的代码里:CBattleScreen.updateItemButtons:for (Map.Entry<Item, GameButton> entry : itemButtons.entrySet()),用entry.getKey()拿到道具、entry.getValue()拿到它的按钮,再写上背包里的数量。 - 只读视图unmodifiable view
- Collections.unmodifiableList(列表) 返回原列表的一个只读窗口:别人能读,调用 add 或 clear 会抛异常。它不是复制品:原列表变了,从这个窗口也能看到变化。
在你的代码里:CBattle.getLog 返回Collections.unmodifiableList(log):CBattleScreen 能读出全部日志,却改不了;COverworld.getObjects 也一样,CMapView 每次 update 都能看到地图上最新剩下的物体。
面向对象
- 抽象类abstract class
- 用 abstract 声明、不能直接 new 的类。它写好子类共有的字段和方法,同时可以留下抽象方法让子类补全。
在你的代码里:new Item()编译不通过,只能new Coffee()这样造具体的子类。你的项目有 8 个抽象类:ASkill、ATargetedSkill、AAttackSkill、ABoostSkill、AStatusEffect、AItem、AMapObject、AGameScreen。 - 抽象方法由谁来写implementing an abstract method
- 一个不是抽象的类,它继承来的每一个抽象方法都必须有内容:可以自己写(加 @Override),也可以从中间的父类继承一个已经写好的版本。只有抽象类可以把抽象方法继续留着不写。
在你的代码里:CWait 自己写了applyEffect;CBite 一个方法都没写,它用的是 ATargetedSkill 写好的applyEffect和 AAttackSkill 写好的applyHit。 - 抽象方法abstract method
- 只有方法头、没有方法体(直接以分号结束)的方法。不是抽象类的子类必须重写它,否则编译不通过。只有抽象类和接口里才能有抽象方法。
在你的代码里:public abstract String use(Fighter player, Fighter enemy);(AItem);protected abstract String applyHit(Fighter user, Fighter target, Random random);(ATargetedSkill)。UML 图里抽象方法用斜体显示。 - 抽象(概念)abstraction
- 只把别人需要知道的部分公开出来,隐藏具体做法。抽象类和接口是 Java 里表达抽象的工具。
在你的代码里:CBattle 只知道 IEnemyStrategy 有一个chooseSkill方法,不知道也不关心 CRandomEnemyStrategy 是怎样挑技能的。 - 具体类concrete class
- 不是抽象类、可以直接 new 的普通类。它继承抽象类时,必须写出所有还没写的抽象方法。
在你的代码里:CBite、CCoffee、CMarker 都是具体类;CStun 写出了 ATargetedSkill 留下的applyHit。 - 构造器链 this(...)constructor chaining
- 一个构造器在第一行用 this(...) 调用本类的另一个构造器,把赋值只写在一处,自己只补上默认值。
在你的代码里:this(player, enemy, inventory, new RandomEnemyStrategy(), new Random());(CBattle):只给 3 个参数时,自动补上随机挑技能的敌人策略和新的随机数生成器;COverworld 和 CSprite 也这样写。 - 声明类型和实际类型declared type vs actual type
- 变量或字段声明时写的类型叫声明类型,它决定你能调用哪些方法;它实际指着的对象可以是这个类的子类(或这个接口的实现类),运行时执行的是实际对象那个类里的方法。
在你的代码里:CBattle 的private final List<String> log = new ArrayList<>();:声明类型是接口 List,实际对象是 ArrayList。useItem(Item item)里 item 的声明类型是 AItem,实际对象可能是 CPoison 或 CCoffee。 - 防御性复制defensive copy
- 收到或交出一个可修改的集合或数组时,先复制一份再存或再给。这样外面的人改他手里那份,不会影响本类内部的数据。
在你的代码里:CMokepon 构造器里this.stats = new EnumMap<>(stats);、this.skills = List.copyOf(skills);;CGameMap 用tiles[row].clone()复制每一行;CInventory.getItems 返回List.copyOf(counts.keySet())。 - 动态绑定(动态分派)dynamic dispatch
- 程序运行时,才根据对象的真实类型决定执行哪个版本的方法,而不是看变量声明的类型。这是多态背后的机制。
在你的代码里:COverworld.move 里的object.interact(this):变量类型是 AMapObject,运行时对象如果是 CMarker 就执行world.startEncounter(this),是 CMysteryBox 就执行world.collect(this),是 CExit 就执行world.reachExit()。 - 封装encapsulation
- 把数据设成 private,只让本类的方法读和改它。这样数据必须遵守的规则(例如 HP 不小于 0)只写在一个地方,别的类没法绕过。
在你的代码里:CFighter.takeDamage 用Math.max(0, getHp() - amount)保证 HP 不会变成负数,spendEnergy保证能量在 0 到最大值之间;外面的类碰不到stats本身,只能调用这些方法。 - extends(继承)extends
- 写在类声明里,表示这个类继承那个类。Java 里一个类只能 extends 一个父类;没写 extends 的类默认继承 Object。
在你的代码里:public class Coffee extends Item;public class Main extends Application和public class GameButton extends Button继承的是 JavaFX 自带的类。 - 取值方法(getter)getter / accessor
- 只把某个私有字段的值交出去、不做修改的方法,通常叫 getXxx;布尔值常叫 isXxx 或 hasXxx。
在你的代码里:getHp()、getMaxHp()(CFighter);isFinished()、hasPlayerWon()(CBattle);CMokepon 只有 getter,没有任何修改方法。 - “有一个”关系has-a relationship
- 一个对象用字段存着另一个对象的引用。课程里叫 containership,UML 里是关联、聚合或组合。
在你的代码里:CBattle 有两个 CFighter(字段player、enemy);CFighter 有一个 CMokepon;CMarker 有一个 IEnemyPicker。 - 不可变对象immutable object
- 创建以后状态永远不会变的对象:字段都是 private final,没有修改方法,里面的集合也是复制过、改不了的。需要“改”时就造一个新对象。
在你的代码里:CPosition 的move不改自己,而是return new Position(x + direction.getXChange(), y + direction.getYChange());;CMokepon、CSprite 和所有技能对象也都不可变,所以可以放心地共享。 - implements(实现接口)implements
- 写在类声明里,表示这个类实现那个接口,必须写出接口里的全部方法(否则它自己得是抽象类)。
在你的代码里:public class OverworldScreen extends GameScreen implements OverworldListener:同时继承一个类、实现一个接口;public class GameWindow implements GameNavigator。 - 继承inheritance
- 子类用 extends 接手父类的字段和方法,再加上自己的部分或改写父类的方法。子类对象“是一种”父类对象(is-a),可以用在需要父类的地方。
在你的代码里:CLance extends AAttackSkill extends ATargetedSkill extends ASkill:Lance 自己没写getName()和use(...),它们是从 Skill 继承来的。 - 接口interface
- 只规定必须有哪些方法(名字、参数、返回类型),不写具体怎么做。类用 implements 承诺实现这些方法,一个类可以实现多个接口。接口不能 new。
在你的代码里:IEnemyStrategy 里只有一行Skill chooseSkill(Fighter enemy, Random random);。你的项目有 4 个接口:IEnemyStrategy、IEnemyPicker、IOverworldListener、IGameNavigator。 - “是一种”关系is-a relationship
- 用继承或实现表达的关系:子类对象是一种父类对象,所以能用在任何需要父类的地方。
在你的代码里:CBite 是一种 AAttackSkill,也是一种 ASkill,所以能放进List<Skill>;COverworldScreen 既是一种 AGameScreen,也是一种 IOverworldListener。 - 重载overload
- 同一个类里有几个名字相同、参数列表不同的方法或构造器,调用时 Java 根据实参的个数和类型选用其中一个。重载和重写不是一回事:重写发生在父类和子类之间。
在你的代码里:CBattle 有 3 个参数和 5 个参数的两个构造器;AGameScreen 有addToView(Node... nodes)和addToView(List<Node> nodes);CGameButton 有接收文字和接收图标的两个构造器。 - 改写(也叫重写、覆盖)override
- 子类写一个和父类方法名字、参数都相同的方法,替换父类的做法。前面加 @Override,编译器会检查父类确实有这个方法,名字写错就会报错。
在你的代码里:CBlinded 重写getMissChance()返回 0.5;CBattleScreen 重写canRunAway()返回!battle.isFinished();CPosition 重写了equals和hashCode。 - 多态polymorphism
- 父类或接口类型的变量可以指向不同子类的对象;调用同一个方法时,实际执行的是对象真实类型里的版本。调用的代码不需要知道具体是哪个子类。
在你的代码里:CBattle.useItem 里只有一句item.use(player, enemy):item 是 CPoison 时敌人 HP 变成 1,是 CCoffee 时能量回满,不需要任何 if/else 判断道具种类。 - 隐私泄露privacy leak
- 课程用语:一个类把自己 private 的可修改对象的引用交了出去(或者直接存下了别人传进来的引用),外面的代码就能不经过这个类的方法直接改它的数据,private 就形同虚设。
在你的代码里:如果 CBattle.getLog 直接return log;,CBattleScreen 就能getLog().clear()清掉日志;你的代码返回Collections.unmodifiableList(log),避免了这一点。CGameMap 构造器的注释也写明复制每一行是为了 “no privacy leak”。 - 职责responsibility
- 一个类负责做的事和负责记住的数据。好的设计让每个类的职责清楚、单一;回答设计题时常用 X is responsible for …。
在你的代码里:CBattle 负责战斗规则,CBattleScreen 负责显示战斗;CInventory 只负责记每种道具有几个。 - 设置方法(setter)setter / mutator
- 直接把字段改成传入的值的方法,通常叫 setXxx。setter 越多,别的类就越容易绕过规则乱改数据。
在你的代码里:Project 1 的 Mokepon 有setHp、setEnergy等 setter;Project 2 的 CMokepon 一个也没有,CFighter 也只提供带规则的修改方法(takeDamage、spendEnergy、raiseStat)。JavaFX 控件的setText、setLayoutX、setDisable是典型的 setter。 - 子类subclass / child class
- 用 extends 继承另一个类的类。子类对象里包含父类的全部字段和方法;不过父类 private 的成员,子类代码不能直接访问,只能通过父类提供的方法。
在你的代码里:CTackle 是 AAttackSkill 的子类,整个类只给出名字 “Tackle”、能量 5、威力 2,通过super("Tackle", ENERGY_COST, POWER)交给父类保存。 - super 关键字super
- 在子类里指父类的那一部分。super(...) 写在构造器第一行,调用父类的构造器;super.方法名(...) 调用父类版本的方法。
在你的代码里:super("Aim", ENERGY_COST, Stat.ACCURACY);(CAim);String hitMessage = super.applyHit(user, target, random);(CLance 先按普通攻击扣血,再加流血);CLabelledStatBar.update 先super.update(current, max)更新进度条,再改数字。 - 父类(超类)superclass / parent class
- 被继承的那个类,它写好子类共有的字段和方法。
在你的代码里:AStatusEffect 是 CBleeding、CBlinded、CStunned 的父类:turnsLeft字段和countDown()方法只在父类写了一次。
设计模式与原则
- 双向依赖(循环依赖)circular dependency
- A 用到 B,B 又用到 A。这样的两个类或两个包只能一起改、一起拿去用,不能单独拿走其中一个。
在你的代码里:ASkill 的use(Fighter user, Fighter target, Random random)用到 battle 包的 CFighter;CFighter 的canUse(Skill skill)又用到 skill 包的 ASkill。所以 battle 包和 skill 包互相 import。battle 和 effect、battle 和 item 也是这样。 - 内聚cohesion
- 一个类里面的字段和方法是不是都围绕同一件事。内聚越高,这个类越容易理解和修改。
在你的代码里:CInventory 只做“记每种道具有几个”;EStat 只存每种属性的名字和上下限,并负责limit;CGameMap 只回答“某一格是什么地形、能不能走”。 - 组合优于继承composition over inheritance
- 想让几种对象行为不同时,优先让它们持有一个可以替换的部件,而不是为每种行为写一个子类。
在你的代码里:白、红、绿三种感叹号都是同一个类 CMarker,区别只在字段picker里放的是哪个 IEnemyPicker(注释:“Random, strong and weak markers only differ in the picker they use”),没有写三个 Marker 子类。 - 耦合coupling
- 一个类依赖其他类的具体细节有多深。耦合越低,改一个类时需要跟着改的类就越少。
在你的代码里:耦合低的地方:CBattle 不认识任何界面类,界面只通过 IGameNavigator 接口和窗口打交道。耦合较紧的地方:COverworldScreen 直接调用 COverworld 的move、canMove、runAway,这些方法一改它也得改。 - 依赖注入(从外面传进来)dependency injection
- 一个对象要用的部件不由它自己 new,而是由别人造好、通过构造器参数(或方法参数)交给它。这样换一个部件时,这个类不用改。
在你的代码里:CBattle 的 5 个参数的构造器收下EnemyStrategy enemyStrategy和Random random,就是给“从外面传进来”留的门;但游戏里只用 3 个参数的构造器,它自己new RandomEnemyStrategy()。AGameScreen 的navigator也是从构造器参数传进来的。 - 依赖倒置Dependency Inversion
- 高层的代码不直接依赖具体的底层类,双方都依赖一个抽象(接口)。这样换掉或修改具体类时,使用它的代码不用改。
在你的代码里:各个界面和 CGameMenu 只认识接口 IGameNavigator(调用startGame、runAway等),由 CGameWindow 实现它;world 包也不 import ui 包,只通过 IOverworldListener 通知界面。 - 设计模式design pattern
- 针对常见设计问题、有名字、可以重复使用的解决办法。说出模式的名字,别人就知道这几个类是怎样配合的。
在你的代码里:你的代码注释里写明了三个:Template Method(ASkill.use、AGameScreen.handleKeyPress)、Strategy(IEnemyStrategy、IEnemyPicker)、Observer(COverworld 和 IOverworldListener);IGameNavigator 的注释写的是 Dependency Inversion 原则。 - 依赖倒置原则(SOLID 的 D)Dependency Inversion Principle (DIP)
- SOLID 的最后一条:高层模块和低层模块都应该依赖抽象(接口或抽象类),而不是高层直接依赖低层的具体类。详见“依赖倒置”。
在你的代码里:CBattle 的字段类型是接口 IEnemyStrategy,只在 3 参数构造器里 new 了一个 CRandomEnemyStrategy 作为默认值;各个界面依赖 IGameNavigator,而不是 CGameWindow。 - 不写重复代码(DRY)Don't Repeat Yourself (DRY)
- 同一条规则只写在一个地方。如果两段代码做的是同一件事,就合成一个方法,用参数区分不同的情况;以后要改规则,只改一处。
在你的代码里:CBattle.playTurn 里你出手和敌人出手用的是同一个use:playerSkill.use(player, enemy, random)和enemySkill.use(enemy, player, random)。扣能量、命中、伤害的规则只写在 ASkill 家族里一次,两边共用。 - 上帝类(什么都管的类)god class
- 一个类管了太多互不相干的事:界面、规则、数据、流程全写在它里面,文件很长,改任何一处都要动它。它违反单一职责原则。
在你的代码里:Project 1 的GameWindow extends Stage有 591 行:选择界面、战斗界面、两套菜单、按键、复制 Mokepon 全在里面。Project 2 把这些拆给了 CGameWindow(106 行)、CSelectScreen、COverworldScreen、CBattleScreen、CGameMenu 和规则类。 - 钩子方法hook method
- 模板方法里留给子类填写或改写的那一步。它可以是抽象方法(必须写),也可以由父类先给一个默认做法(想改再改)。
在你的代码里:AGameScreen.handleKey 默认什么都不做,只有 COverworldScreen(方向键)和 CBattleScreen(数字键 1–4)重写了它;ASkill.applyEffect、ATargetedSkill.applyHit 是子类必须写的钩子。 - 接口隔离原则Interface Segregation Principle (ISP)
- 不要逼一个类依赖它用不到的方法;接口应该小而专一,而不是一个大而全的接口。
在你的代码里:IEnemyStrategy 和 IEnemyPicker 各只有一个方法;IOverworldListener 只列出地图世界里会发生的 5 件事。不足之处:IGameNavigator 有 7 个方法,而 CGameMenu 只用到其中 3 个(runAway、returnToMainMenu、quit)。 - 分层layered architecture
- 把程序按职责分成几层:靠近用户的一层只管显示和接收操作,下面几层管规则和数据。上层可以调用下层,下层不认识上层。
在你的代码里:你的项目大致是:ui 包(画面,用 JavaFX)→ world、battle、skill、effect、item(规则)→ model(基本数据);data 包在启动时读文件造对象。规则包和 model、data 都没有 import ui,也没有 import javafx。 - 里氏替换原则Liskov Substitution Principle (LSP)
- 子类对象必须能放在任何需要父类对象的地方,而且行为不出乎意料,使用者不需要知道它具体是哪个子类。
在你的代码里:CBattle.playTurn 对任何 ASkill 都只调用use(...),每个子类都遵守“付能量、返回一行日志”;AStatusEffect 给了默认值(伤害倍率 1、落空几率 0、没有持续伤害),所以 CFighter 的循环可以把任何效果一视同仁地处理。 - 魔法数字magic number
- 直接写在代码里、看不出含义的数字。用有名字的常量代替它,读代码的人一看就懂,改的时候也只改一处。
在你的代码里:CBattleScreen 不直接写 609,而是LOG_X = PLAYER_X + PLAYER_WIDTH + 9;ABoostSkill 用MAX_BOOST = 10,CBleeding 用TURNS = 2。 - 规则与界面分离separation of model and view
- 把游戏规则和数据的代码,与把它画到屏幕上的代码放在不同的类和包里。规则代码不知道界面的存在;界面只负责显示,并把用户操作转交给规则代码。
在你的代码里:data、model、skill、effect、item、battle、world 七个包都没有 import javafx;CBattle 只更新战斗者、道具和日志,由 CBattleScreen 读出来显示。CSprite 也只存颜色文字,真正的染色在 ui 包的 CMapView 里做。 - 观察者模式Observer pattern
- 被观察的对象维护一个监听者列表,自己发生变化时逐个通知它们;它只认识监听者接口,不知道监听者具体是谁、会做什么。
在你的代码里:COverworld 有List<OverworldListener> listeners和addListener;CGameWindow.startGame 里world.addListener(overworldScreen);玩家移动后notifyListeners(listener -> listener.onPlayerMoved()),COverworldScreen 收到后清空状态栏、更新地图。 - 开闭原则Open/Closed Principle (OCP)
- 对扩展开放、对修改关闭:加新功能时写新代码(例如新的子类),而不去改已经能正常工作的旧代码。
在你的代码里:ASkill 的注释写着 “To add a new skill, write a new subclass instead of changing the existing ones”:加新技能 = 一个新子类 + CSkillCatalogue 里多一个对象。Project 1 的 Skill 则要在if (name.equals("Bite"))这样的 if/else 里再加一个分支。 - 抽象步骤(primitive operation)primitive operation (abstract step of a Template Method)
- 模板方法里由子类必须写出来的那一步:父类只写方法头(abstract),不给默认做法。英文课本里常和 hook method 区分开:hook method 指父类已经给了默认做法、子类想改才改的那一步。
在你的代码里:ASkill.use 里的applyEffect(...)、ATargetedSkill.applyEffect 里的applyHit(...)都是抽象步骤;AStatusEffect 的getMissChance()默认返回 0、只有 CBlinded 改写,更像 hook method。 - 面向接口编程program to an interface
- 字段、参数、变量的类型尽量写成接口或父类,而不是具体的实现类。这样以后换一种实现时,这些代码都不用改。
在你的代码里:CBattle 的字段写成EnemyStrategy enemyStrategy和List<String> log(真正的对象是 CRandomEnemyStrategy 和 ArrayList);AGameScreen 存的是GameNavigator navigator,不是 CGameWindow。 - 单例模式Singleton pattern
- 让一个类在整个程序里只有一个对象的模式:构造器设成 private,再用一个静态方法(例如 getInstance)返回那唯一的对象。
在你的代码里:你的代码没有用单例。“每种技能、每种道具只有一个对象”是靠 CSkillCatalogue 和 CItemCatalogue 的静态字段做到的,CBite、CCoffee 这些类本身仍然可以被 new。 - SOLID 五原则SOLID principles
- 五条面向对象设计原则的首字母:S 单一职责、O 开闭、L 里氏替换、I 接口隔离、D 依赖倒置。
在你的代码里:在你的代码里:S —— CBattle 只管规则、CBattleScreen 只管显示;O —— 加技能只加子类;L —— 任何 ASkill 子类都能放进技能列表;I —— IEnemyStrategy 只有一个方法;D —— IGameNavigator。 - 单一职责原则Single Responsibility Principle (SRP)
- 一个类只负责一件事,只有一种理由需要修改它。
在你的代码里:CBattle 只管战斗规则(注释写着 “It knows nothing about the screen”),CBattleScreen 只负责显示、把点击变成回合;读数据文件也分成 CMokeponParser、CMapParser、CImageParser,各读一部分。 - 静态目录类static catalogue class
- 只有静态成员的类:用静态常量存好全游戏共用的那几个对象,再用静态方法按名字查找。它不需要、也不打算被创建对象。
在你的代码里:CSkillCatalogue 的静态列表SKILLS存着 10 个技能对象,find("Stun")返回其中那一个;CItemCatalogue 存着HEALTH_TONIC、COFFEE、POISON三个道具对象。它们不是单例模式:根本不创建目录类自己的对象;不过它们没写 private 构造器,技术上仍然可以被 new,只是没有意义。 - 策略模式Strategy pattern
- 把可以换着用的做法写成一个接口和几个实现类;使用者只存着接口类型的字段,换一个实现对象就换了行为,使用者本身不用改。
在你的代码里:CBattle 存着enemyStrategy: EnemyStrategy,每回合调用chooseSkill;CMarker 存着picker: EnemyPicker:白色感叹号用 CRandomEnemyPicker,红色用new TerrainEnemyPicker(TerrainEffect.ADVANTAGE),绿色用new TerrainEnemyPicker(TerrainEffect.DISADVANTAGE)。 - 模板方法模式Template Method pattern
- 父类用一个不能被重写的(final)方法定好各个步骤的先后顺序,其中某些步骤交给子类去写(抽象方法或可以重写的钩子方法)。子类只能改步骤的内容,改不了顺序。
在你的代码里:ASkill 的public final String use(...)先user.spendEnergy(energyCost),再return applyEffect(user, target, random);ATargetedSkill 的 finalapplyEffect先做命中判定,命中了才调用子类的applyHit。 - 工具类utility class
- 只放一组相关的静态常量和静态方法、不保存状态的类,用“类名.方法名”直接调用。
在你的代码里:CUiStyle:GAME_WIDTH、GAME_HEIGHT、loadImage、loadGameFont、createImageView、tint等所有界面共用的尺寸和小工具。
游戏规则
- 格子tile
- 地图被切成一个个 32×32 像素的小方块。位置用 (列, 行) 表示,从 (0,0) 开始数。
在你的代码里:MapView.TILE_SIZE是 32;玩家的起点是第 10 列、第 18 行,写成 (10,18)。
游戏里的概念
- 战斗日志battle log
- 战斗中发生的每件事的文字记录,一行一条,显示在战斗界面右下角的文本框里。
在你的代码里:CBattle 的log列表:开头是 “Player chose Impike. Began a fight against enemy Galecrest!”,每回合先加一个空行,再加 “Player's Impike used Stun!” 和以制表符开头的结果行;CBattleScreen.showLog 把它们连起来放进logArea。 - 伤害公式damage formula
- 伤害 = 威力 × 攻击方力量 × 50 ÷ 防守方防御,再乘上攻击方身上最强的减伤倍率(被眩晕时是 0.5),最后转成 int,小数直接去掉。
在你的代码里:CFighter.calculateDamage。Impike 用 Lance(威力 0.5)打 Galecrest:0.5 × 100 × 50 ÷ 30 = 83.33,去掉小数是 83,日志写 “Hit Galecrest for 83 damage!”。Bite 的威力是 1,Tackle 是 2。 - 遇敌encounter
- 玩家走到感叹号上时发生:按这一格的地形挑出一只敌人,弹出对话框让你选择战斗还是逃跑。
在你的代码里:COverworld.startEncounter 创建 CEncounter(存着感叹号、玩家的 Mokepon、敌人和地形),再通知 COverworldScreen.onEncounter,弹出 “Encountered a Hexwing”、两条地形提示和 Fight! / Run Away! 按钮。 - 能量消耗energy cost
- 技能按钮方括号里的数字。用技能时先付能量(就算后面没打中也已经付了);负数表示恢复能量;能量始终在 0 到本场最大值之间。能量不够时按钮变灰、点不了。
在你的代码里:Impike 开局能量 33:用 Wait [-2] 后仍是 33(已经是最大值,多出来的被截掉),再用 Stun [4] 变成 29,就是 battle-stun 里的 “29 / 33”。代码:ASkill.use 里的user.spendEnergy(energyCost)和 CFighter.canUse。 - 倒下faint
- HP 降到 0,isAlive() 变成 false,战斗立刻结束,倒下一方的卷轴被红色遮罩盖住。
在你的代码里:CBattle.finish 写入 “Enemy's Galecrest fainted!” 和 “You win”;CFighterPanel.update 里scroll.setFainted(!fighter.isAlive())显示红色遮罩(battle-win 里是敌人,battle-lose 里是玩家)。 - 命中率hit chance
- 针对对方的技能先要通过命中判定:命中率 = 自己的 Accuracy² ÷(自己的 Accuracy² + 对方的 Dodge²)。通过以后,如果自己被致盲,还要再过一次 50% 的落空判定。
在你的代码里:battle-start 里 Impike 的 Accuracy 是 100、Galecrest 的 Dodge 是 40:10000 ÷ (10000 + 1600) ≈ 0.86;反过来 Galecrest 的 Accuracy 43 对 Impike 的 Dodge 100:1849 ÷ (1849 + 10000) ≈ 0.16,所以它的 Tackle 大多打不中(battle-stun 的日志就是 “But they missed.”)。代码在 ATargetedSkill.calculateHitChance 和hits。 - 使用道具using an item
- 战斗中点道具按钮:一定成功,背包里的数量减 1,日志写一行,不算回合(敌人不出手,效果也不倒数)。数量为 0 的道具按钮变灰。
在你的代码里:CBattle.useItem:inventory.remove(item),然后log.add(item.use(player, enemy))。三种道具:CHealthTonic 回满 HP,CCoffee 回满能量,CPoison 把敌人 HP 变成 1。 - 持续伤害lingering damage
- 回合结束时由状态效果造成的伤害(只有流血会这样)。先算敌人的,敌人倒下就不再算玩家的;流血用施加者一半的力量计算,伤害不写进日志,只在最后一回合写 The bleeding stopped.。
在你的代码里:battle-bleed:Nightwing(力量 68)用 Lance 打 Bubbore(防御 74),命中 0.5 × 68 × 50 ÷ 74 = 22.97 → 22,回合结束时流血再扣 22,HP 195 → 151。下一回合 Bubbore 用 Bunker 把防御升到 82,流血变成 0.5 × 68 × 50 ÷ 82 = 20.73 → 20,HP 151 → 131,并显示 “The bleeding stopped.”。代码:CBattle.endTurn、CBleeding.applyLingeringDamage。 - 三种感叹号marker groups
- 地图上的感叹号分三组,颜色不同,挑敌人的方式也不同:白色随便挑,红色挑在这格地形上占优势的,绿色挑处于劣势的。
在你的代码里:数据文件里map.mokeponRandom.colour=white、map.mokeponStrong.colour=red、map.mokeponWeak.colour=lime;CMapParser 给它们分别配上 CRandomEnemyPicker、new TerrainEnemyPicker(TerrainEffect.ADVANTAGE)、new TerrainEnemyPicker(TerrainEffect.DISADVANTAGE)。 - 类型Mokepon type
- 每只 Mokepon 有一个类型。除了 NORMAL,每种类型在一种地形上占优势、在另一种地形上处于劣势。
在你的代码里:EMokeponType:FIRE 强在 FOREST、弱在 WATER;ICE 强在 ROCKS、弱在 GRASS;WATER 强在 WATER、弱在 ROCKS;LIGHTNING 强在 GRASS、弱在 FOREST;NORMAL 两个都是 null。getEffectOn(terrain)返回对应的 ETerrainEffect。 - 神秘箱子mystery box
- 地图上的问号格子,里面有一个道具。走上去就捡到,箱子从地图上消失,状态栏显示捡到了什么。
在你的代码里:数据文件里map.items.1.type=POISON、map.items.1.location=18,16;CMysteryBox.interact 调用 COverworld.collect:objects.remove(box)、player.getInventory().add(box.getItem()),状态栏显示 “You picked up a Poison!”。 - 逃跑run away
- 有两种:在遇敌对话框里点 Run Away!,或者在战斗中用菜单的 Run away(Ctrl+R)。两种都让玩家回到上一步所在的格子,感叹号留在原地;战斗结束后菜单里的 Run away 会变灰。
在你的代码里:COverworld.runAway:encounter = null; player.moveBack();再通知监听者;战斗中的那条路先经过 CGameMenu → CGameWindow.runAway,再showScreen(overworldScreen)。CBattleScreen.canRunAway 返回!battle.isFinished()。 - 属性stat
- 每只 Mokepon 的六个数值:HP、Energy、Strength、Accuracy、Defence、Dodge。HP 的合法范围是 1 到 1000,其余是 1 到 100。
在你的代码里:EStat 枚举:HP("HP", 1, 1000)、ENERGY("Energy", 1, 100)等;数据文件里 Impike 是mokepon.14.hp=150、mokepon.14.strength=76等。战斗中的当前值存在 CFighter 的stats里。 - 属性提升stat boost
- Aim、Bunker、Nimble、Swole 四个技能一定成功,给自己的一项属性随机加 1 到 10 点;超过上限的部分不算,日志写的是实际增加的点数。
在你的代码里:ABoostSkill.applyEffect:random.nextInt(MAX_BOOST) + 1,再user.raiseStat(stat, boost);battle-blind 里 Bubbore 用 Swole:“They raised their strength by 2 points!”(66 → 68)。Aim 加 Accuracy,Bunker 加 Defence,Nimble 加 Dodge,Swole 加 Strength。 - 状态效果status effect
- 技能命中后加在对方身上、持续几个回合的效果。它存在战斗者的 effects 列表里,每个完整回合结束时倒数一次,数到 0 就被移除。
在你的代码里:一共三种:CBleeding(Lance 造成,2 回合,每回合结束时掉血)、CBlinded(Pocket Sand 造成,3 回合,技能多 50% 落空)、CStunned(Stun 造成,3 回合,伤害减半)。父类 AStatusEffect 给出默认值,子类只重写自己改变的那一项。 - 地形terrain
- 地图每一格的种类,数据文件里每格用一个字母表示,有的地形不能走。
在你的代码里:ETerrain:BARRIER(B,不能走)、FOREST(F)、GRASS(G)、OCEAN(O,不能走)、PATH(P)、ROCKS(R)、WATER(W)。map.tiles.0=RRRRRRPPRRRRRRRRRRRR是地图最上面一行。 - 地形效果terrain effect
- 战斗开始时,按双方类型在这一格地形上的强弱,把每个属性乘 1.5(优势)、1(普通)或 0.5(劣势),四舍五入后再限制在合法范围内。只在创建战斗者时算一次,结果也就是本场的最大 HP 和最大能量。
在你的代码里:Impike(FIRE)在 FOREST 上占优势:HP 150 → 225,能量 22 → 33,力量 76 → 114 → 限制成 100,防御 45 → 67.5 → 68;对手 Galecrest(LIGHTNING)在 FOREST 上处于劣势:HP 185 → 92.5 → 93。代码:ETerrainEffect.adjust 和 CFighter 构造器。 - 格子坐标和像素tile coordinates
- 地图是 20 × 20 格,每格 32 像素。格子坐标 (x, y) 里 x 是列、y 是行,(0, 0) 在左上角,往下走 y 变大。截图里的像素位置 = (320 + 32 × x, 65 + 32 × y)。
在你的代码里:CMapView.placeOnTile 用position.getX() * TILE_SIZE;COverworldScreen 把地图放在 x = (1280 − 20 × 32) ÷ 2 = 320、y = 40(加上 25 像素的菜单栏就是截图里的 65);玩家起点 (10, 18) 在截图里是 (640, 641)。 - 染色tint
- 给一张图片整体加上一种颜色:白色部分变成这个颜色,黑色部分还是黑色,透明部分仍然透明。
在你的代码里:CUiStyle.tint 用一盏彩色的 Lighting 光实现;CMapView.createObjectView 只在sprite.hasTint()时染色:感叹号染成 white、red、lime,出口的门染成 magenta,问号箱子不染色。 - 回合turn
- 点一次技能就是一个回合:玩家先出手;敌人还活着就轮到敌人;双方都还站着,就结算持续伤害、所有效果倒数一次。用道具不算回合,敌人不会出手。
在你的代码里:CBattle.playTurn 和endTurn;battle-poison 的日志里,“You used your Poison on Galecrest. It devastated them!” 后面没有敌人的行动。
Java 基础
- 访问修饰符access modifier
- 写在类、字段、方法前面,规定哪些代码可以使用它。从宽到严:public、protected、什么都不写(包内可见)、private。
在你的代码里:CBattle 的字段全是private,playTurn是public;AGameScreen.handleKey 是protected;CPlayer.moveTo 前面什么都没写(包内可见)。 - 实参(传入的值)argument
- 调用方法时真正放进括号里的值。参数是方法声明里的名字,实参是这一次调用填进去的具体值。
在你的代码里:createMoveButton(Direction.UP, images)(COverworldScreen.createMovePad)把Direction.UP和images两个实参交给方法的参数direction和images。 - 数组array
- 长度固定、按下标(从 0 开始)存同一种类型值的结构,用方括号表示,创建后长度不能改。二维数组就是数组的数组。
在你的代码里:CGameMap 的Terrain[][] tiles是二维数组,写法是tiles[row][column]:第一个下标是 y(行),第二个是 x(列),所以getTerrain写成tiles[position.getY()][position.getX()]。 - 布尔值 booleanboolean
- 只有 true 和 false 两个值的基本类型,用来表示是不是、能不能。
在你的代码里:CBattle 的private boolean finished = false;;COverworld.canMove 返回boolean,COverworldScreen 用它决定四个方向按钮能不能点。 - break 跳出循环break
- 立刻结束所在的循环,跳到循环后面继续执行。
在你的代码里:CFighter.applyLingeringDamage 里if (!isAlive()) { break; }:已经倒下,就不再计算后面的持续伤害。 - 强制类型转换cast
- 写成 (类型) 值,把值转换成另一种类型。double 转 int 会直接去掉小数部分(不是四舍五入);把 Object 转回具体的类时,必须确定它真的是那个类。
在你的代码里:return (int) (damage * getDamageMultiplier());(CFighter.calculateDamage)把 83.33 变成 83;Position position = (Position) other;(CPosition.equals)。 - 字符 charchar
- 存单个字符的基本类型,用单引号写,例如 'B'。
在你的代码里:ETerrain 的BARRIER('B', false);CMapParser 用letters.charAt(column)取出一行地图里的一个字母,再交给Terrain.fromSymbol。 - 限制范围(Math.max / Math.min)clamp
- 用 Math.max(下限, Math.min(值, 上限)) 把一个数压回合法范围:太小就变成下限,太大就变成上限。
在你的代码里:return Math.max(minimum, Math.min(value, maximum));(EStat.limit):Impike 在森林里力量 76 × 1.5 = 114,被限制成 100;CFighter.takeDamage 用Math.max(0, getHp() - amount)让 HP 不会小于 0。 - 类class
- 用 class 关键字写出的一种类型。它规定这种对象有哪些字段(存什么数据)和哪些方法(能做什么)。程序运行时再按它用 new 造出对象。
在你的代码里:public class Position规定每个位置都有x、y两个整数,以及move、equals等方法(CPosition)。你的项目一共 72 个类型:55 个普通类、8 个抽象类、4 个接口、5 个枚举。 - 注释comment
- 写给人看、程序会忽略的文字。斜杠星号开头、星号斜杠结尾的注释可以跨多行;两个斜杠开头的注释到行尾结束。
在你的代码里:你的代码在每个类和大多数方法前都写了一句英文说明,例如 ASkill.use 前面的 “Template Method: pay the energy cost first …”。 - 编译错误compile-time error
- javac 把源代码翻译成程序时就发现的错误。有编译错误,程序根本造不出来,也就不会运行。它和运行时才出现的异常(比如 NullPointerException)不一样:异常是程序已经在跑、跑到那一行才出错。
在你的代码里:CGameWindow 的字段menu是 final 的。如果构造器里先写root.setTop(menu.getMenuBar())、后写menu = new GameMenu(this),javac 会报variable menu might not have been initialized,程序编译不过。 - 常量constant
- 用 static final 声明、程序运行中永远不变的值。习惯上名字全部大写,单词之间用下划线。用常量代替直接写数字,含义更清楚,改的时候只改一处。
在你的代码里:private static final int DAMAGE_SCALE = 50;(CFighter)、public static final int GAME_WIDTH = 1280;(CUiStyle)、CBattleScreen 的PLAYER_X = 50和LOG_X = PLAYER_X + PLAYER_WIDTH + 9等布局常量。 - 构造器(构造方法)constructor
- 名字和类名一样、没有返回类型的特殊方法。用 new 创建对象时自动执行一次,负责给字段赋初始值。
在你的代码里:public Position(int x, int y)把两个参数存进this.x、this.y;CFighter 的构造器把 Mokepon 的每个属性乘上地形倍数后存进stats。 - 构造器(构造方法)constructor
- 名字和类名一样、前面没有返回类型的特殊方法。用 new 创建对象时自动运行一次,负责把新对象准备好(通常是给字段赋初始值)。
在你的代码里:new Position(10, 18)运行 CPosition 的构造器public Position(int x, int y),把 10 和 18 存进this.x、this.y;CFighter 的构造器把 Mokepon 的每个属性乘上地形倍数后存进stats。 - 默认构造器default constructor
- 一个类如果一个构造器都没写,Java 会自动给它一个不带参数、什么都不做的构造器。
在你的代码里:CMokeponParser、CRandomEnemyStrategy、CSkillCatalogue 都没写构造器;CMain.start 里的new MokeponParser()用的就是自动生成的那个。 - 字段的默认值default field value
- 字段如果没有赋值,Java 会自动给一个默认值:对象类型是 null,int 是 0,double 是 0.0,boolean 是 false。方法里的局部变量没有默认值,没赋值就用会编译出错。
在你的代码里:CGameWindow 的字段world在构造器里没有赋值,所以在选择界面上它是 null;点 Choose Mokepon 后 startGame 才给它 new 一个 COverworld。 - 小数 doubledouble
- 存带小数的数的基本类型。倍率、概率、伤害的中间结果都用 double,最后再转回 int。
在你的代码里:AAttackSkill 的power(Tackle 是 2,Lance 是 0.5)、CBlinded 的MISS_CHANCE = 0.5、CFighter.getMissChance() 的返回值都是double。 - 提前返回early return
- 在方法中间遇到 return 就立刻结束这个方法,后面的代码都不执行。常用来先排除不该继续的情况。
在你的代码里:CBattle.playTurn 开头if (finished) { return; };敌人被打倒后finish(true); return;,所以敌人不会再出手。 - 枚举 enumenum
- 用 enum 声明、只有固定几个取值的类型。每个取值(枚举常量)其实是一个对象;枚举也可以有字段、构造器(总是 private 的)和方法。
在你的代码里:EStat 有 HP、ENERGY、STRENGTH、ACCURACY、DEFENCE、DODGE 六个常量;HP("HP", 1, 1000)调用枚举的构造器存下显示名和上下限。你的项目有 5 个枚举:EStat、ETerrain、ETerrainEffect、EMokeponType、EDirection。 - 枚举自带的 values / valueOf / namevalues() / valueOf() / name()
- 每个枚举都自动有:values() 按声明顺序返回全部常量组成的数组;valueOf(名字) 按名字找常量,找不到就抛异常;name() 返回常量的名字。
在你的代码里:for (Stat stat : Stat.values())(CFighter 构造器);MokeponType.valueOf(data.getText(prefix + "type"))(CMokeponParser);stat.name().toLowerCase()得到数据文件里的键名 hp、energy…… - 转义字符escape sequence
- 字符串里以反斜杠开头的特殊写法:\t 是一个制表符(一段缩进),\n 是换行。
在你的代码里:"\tBut they missed."(ATargetedSkill)开头的 \t 让日志里的结果行往里缩;按钮文字"Choose\nMokepon"(CSelectScreen)中间的 \n 让它分成两行。 - 字段(成员变量)field / instance variable
- 写在类里面、方法外面的变量。每个对象各有一份,用来一直记住这个对象的数据。
在你的代码里:CPlayer 的字段position、previousPosition、fightsWon分别记住玩家现在站哪、上一步站哪、赢了几场。 - final(不能再改)final
- 用在字段或变量上:赋值一次后就不能再指向别的值。用在方法上:子类不能重写它。注意 final 只锁住指向哪个对象,对象里面的内容仍然可以变。
在你的代码里:CBattle 的private final List<String> log:引用不能换,但log.add(...)照样能加日志;ASkill 的public final String use(...)不让任何技能子类改掉“先付能量再生效”的顺序。 - final 字段指着一个可变对象final reference to a mutable object
- 字段写了 final,只表示这个字段赋值一次以后不能再指向别的对象;它指着的那个对象(比如一个 ArrayList 或 Map)里面的内容照样可以改。final 管的是“指着谁”,不管“里面装了什么”。
在你的代码里:CFighter 的private final List<StatusEffect> effects = new ArrayList<>();:不能写effects = new ArrayList<>()换一个列表,但effects.add(...)和迭代器的remove()都可以。CInventory 的final Map<Item, Integer> counts也一样,counts.put(...)改的是 Map 里面的值。 - 增强 for 循环(for-each)for-each loop
- for (类型 变量 : 集合或数组) 依次取出每个元素,不需要下标。循环过程中不能往这个集合里加或删元素。
在你的代码里:for (StatusEffect effect : effects)(CFighter.getMissChance)看身上每一个效果,留下最大的落空几率。 - for 循环(计数)for loop
- for (初始化; 条件; 每轮之后) 按计数重复执行一段代码,常用来按下标 0、1、2… 依次处理。
在你的代码里:for (int row = 0; row < rowCount; row++)(CMapParser.parseTiles)从 map.tiles.0 到 map.tiles.19 一行一行读地图。 - 垃圾回收garbage collection
- 一个对象如果已经没有任何字段或变量指着它,就再也用不到了。Java 会自动把它占的内存收回来,你不用(也不能)自己去删对象。
在你的代码里:玩家每走一步,COverworld.canMove 里player.getPosition().move(direction)造出的那个 CPosition 只用来问一句能不能走,用完就没人指着它,之后会被自动回收。 - if / else 条件判断if / else
- 条件为 true 时执行 if 后面的代码块,否则执行 else 的代码块;没有 else 就什么都不做。
在你的代码里:EMokeponType.getEffectOn:if (terrain == strongTerrain) { return TerrainEffect.ADVANTAGE; },再判断是不是弱势地形,都不是就返回TerrainEffect.NEUTRAL。 - Java 自动加上的修饰符implicit modifier
- 有些地方不写修饰符,Java 也会自动加上:接口里的方法自动是 public 和 abstract;枚举的构造器自动是 private。UML 画的是自动加上以后的样子。
在你的代码里:IEnemyStrategy 里只写了Skill chooseSkill(Fighter enemy, Random random);,UML 上却是绿色实心圆加斜体;EStat 的构造器没写 private,UML 上是红色实心方块。 - 导入 importimport
- 写在文件开头,让这个文件可以直接使用别的包里的类名。同一个包里的类不用 import。
在你的代码里:CBattleScreen 有import au.edu.unimelb.swen.oop.mokepon.battle.Battle;和import javafx.scene.control.TextArea;。整个项目只有 ui 包和 CMain import 了 javafx。 - 自增 ++ 和自减 --increment / decrement
- x++ 把 x 加 1,x-- 把 x 减 1。写在表达式里时(例如 f(row++)),先把旧值拿去用,然后再加 1。
在你的代码里:fightsWon++;(CPlayer.addWin);turnsLeft--;(AStatusEffect.countDown);addStatRow(grid, row++, ...)(CSelectScreen)先用当前行号放这一行,再把row加 1 留给下一行。 - 整数 intint
- 存整数(没有小数)的基本类型。HP、能量、属性值、坐标在你的代码里都是 int。
在你的代码里:private int fightsWon = 0;(CPlayer);public int getHp()(CFighter)。 - 整数除法integer division
- 两个 int 相除,结果还是 int,小数部分直接丢掉,例如 29 / 33 得 0。想得到小数,就先让其中一个变成 double(乘 1.0 或写成 2.0)。
在你的代码里:current * 1.0 / max(CStatBar.calculateRatio)先乘 1.0,29 和 33 才能算出约 0.88;CSelectScreen 写PORTRAIT_SIZE / 2.0也是这个原因。伤害公式里power本来就是 double,所以那里不会出现整数除法。 - 局部变量local variable
- 写在方法里面的变量,只在这一次方法执行期间存在,方法结束就没了,别的方法也看不到它。
在你的代码里:AAttackSkill.applyHit 里的int damage = user.calculateDamage(target, power);:damage只在这一次攻击里用来扣血和写日志。 - 逻辑运算符 && || !logical operators
- 两个 & 表示并且,两个竖线表示或者,感叹号表示取反。并且和或者会短路:左边已经能决定结果时,右边根本不执行。
在你的代码里:isInside(position) && getTerrain(position).isWalkable()(CGameMap.isWalkable):格子在地图外时左边是 false,右边不会执行,所以不会用越界的下标去读数组。 - main 方法(程序入口)main method
- public static void main(String[] args) 是 Java 程序开始执行的地方,args 里是命令行参数。
在你的代码里:CMain 的public static void main(String[] args) { launch(args); }只做一件事:把参数交给 JavaFX 的launch,由它打开窗口并调用start。 - 四舍五入 Math.roundMath.round
- Math.round(x) 把小数四舍五入到最近的整数(正好 .5 时往大的方向进)。对 double 它返回 long,所以还要再转成 int。
在你的代码里:return (int) Math.round(value * multiplier);(ETerrainEffect.adjust):Impike 的防御 45 × 1.5 = 67.5,变成 68。 - 方法method
- 写在类里面、有名字的一段代码。别处调用它时才执行;它可以接收参数,也可以返回一个结果。
在你的代码里:public void playTurn(Skill playerSkill)是 CBattle 的方法:每点一次技能按钮,CBattleScreen 就调用它一次,打完一个回合。 - 连着调用method chaining
- a.b().c() 这种写法:先执行 a.b(),它返回一个对象,再对这个返回的对象调用 c()。从左往右一段一段算。
在你的代码里:player.getPosition().move(direction)(COverworld.move):先拿到玩家现在的 CPosition 对象,再叫这个对象的 move,得到一个新的 Position。 - new 关键字new
- 创建新对象的关键字:分配内存、执行构造器,然后得到一个指向新对象的引用。
在你的代码里:showScreen(new BattleScreen(world.createBattle(), images, this));(CGameWindow.startBattle)每次开战都造一个全新的战斗界面。 - null(空引用)null
- 一个特殊值,表示这个引用现在不指向任何对象。对 null 调用方法会抛出 NullPointerException,所以使用前常要先判断。
在你的代码里:在选择界面时 CGameWindow 的world和overworldScreen都是 null;AGameScreen 没有对话框时dialog是 null,所以isDialogOpen()就是return dialog != null;;NORMAL 类型的强弱地形也是 null:NORMAL(null, null)。 - 对象(实例)object / instance
- 按某个类用 new 真正造出来、放在内存里的一份数据。同一个类可以造出很多对象,每个对象有自己的字段值。
在你的代码里:读到map.player.startingPos=10,18时,CGameData.getPosition 执行new Position(...),造出一个 x=10、y=18 的对象,这就是玩家的起点。地图上 10 个感叹号是 10 个不同的 CMarker 对象。 - 对象活多久object lifetime
- 一个对象从 new 出来那一刻开始存在;只要还有某个字段或变量指着它,它就一直在。没有任何东西指着它以后,它就用不到了,Java 会自动把它的内存收回(垃圾回收)。所以“活多久”要看:谁用字段存着它,那个存它的对象又活多久。
在你的代码里:CFighter 被 CBattle 和 CFighterPanel 存着,它们又被 CBattleScreen 存着。打完仗 CGameWindow 换回地图画面,再没有字段指着这个 CBattleScreen,这一整串对象就都没人要了。 - 包package
- 把相关的类放进同一个文件夹和名字空间,文件第一行写 package。包也决定了“包内可见”的范围。
在你的代码里:72 个类分在 9 个包:根包 mokepon(只有 CMain)、data 6 个、model 5 个、skill 15 个、effect 4 个、item 6 个、battle 4 个、world 15 个、ui 16 个。例如 CBattle 的第一行是package au.edu.unimelb.swen.oop.mokepon.battle;。 - 包内可见(默认访问权限)package-private (default access)
- 什么修饰符都不写时,只有同一个包里的类能使用,别的包里的类都用不了。
在你的代码里:CPlayer 的moveTo、moveBack、addWin和 AMapObject 的abstract void interact(Overworld world)都没写修饰符:只有 world 包里的 COverworld 能挪动玩家、触发物体,ui 包里的界面做不到。 - 参数(形参)parameter
- 方法或构造器括号里声明的变量,用来接收调用者传进来的值。它只在这一次调用里有效。
在你的代码里:public Position move(Direction direction)里的direction是参数:传 UP 就得到上面一格,传 RIGHT 就得到右边一格(CPosition)。 - 基本类型primitive type
- Java 内置的简单值类型,如 int、double、boolean、char。变量里直接存数值本身,不是对象,也不能是 null。
在你的代码里:CFighter 的DAMAGE_SCALE是int,ETerrainEffect 的multiplier是double,ETerrain 的walkable是boolean、symbol是char。 - private(私有)private
- 只有同一个类里面的代码能使用。字段通常设成 private,外面只能通过这个类提供的方法来读或改。
在你的代码里:CFighter 的stats是 private,别的类只能通过takeDamage、raiseStat这些方法改属性;private double getDamageMultiplier()只在 Fighter 自己计算伤害时用。 - protected(受保护)protected
- 本类、所有子类、以及同一个包里的类都能使用。常用来给子类留下可以调用或重写的部分,又不对所有人公开。
在你的代码里:ASkill 的构造器protected Skill(String name, int energyCost)只给子类的super(...)用;protected abstract String applyEffect(...)留给子类来写;AGameScreen 的addToView、showDialog只给各个界面子类用。 - public(公开)public
- 任何包里的任何类都能使用。
在你的代码里:public List<String> getLog()(CBattle)让 ui 包里的 CBattleScreen 能读日志。 - 全名(完全限定名)fully qualified name
- 包名加上类名,例如 au.edu.unimelb.swen.oop.mokepon.battle.Battle。import 写的就是全名;import 以后,代码里只写短名 Battle。
在你的代码里:UMLDoclet 图里类名下面那行小字(如au.edu.unimelb.swen.oop.mokepon.skill)就是这个类的包名。 - 引用reference
- 对象类型的变量里存的不是对象本身,而是指向这个对象的引用。两个变量可以指向同一个对象,通过任何一个修改,另一个都能看到。
在你的代码里:背包 CInventory 只在 COverworld 构造器里 new 了一次,CPlayer 和之后每一场 CBattle 拿的都是指向它的引用,所以在战斗里用掉一瓶 Poison,回到地图上数量也少了。 - 返回值return value / return type
- 方法用 return 交回给调用者的结果。方法名前面写的类型就是返回类型;写 void 表示不返回任何东西。
在你的代码里:CFighter.raiseStat 最后return newValue - oldValue;,交回实际涨了几点;ABoostSkill 把这个数写进日志,例如 “They raised their strength by 2 points!”。 - split 切分字符串String.split
- 按分隔符把一个字符串切成一个字符串数组。它的参数是正则表达式,所以竖线这种特殊符号要写成 "\\|"。
在你的代码里:getText(key).split(",")(CGameData.getPosition)把 “10,18” 切成 “10” 和 “18”;skillNames.split("\\|")(CMokeponParser)把 “Lance|Stun|Wait” 切成三个技能名。 - 静态 staticstatic
- 属于类本身、不属于某一个对象。静态字段整个程序只有一份;静态方法用“类名.方法名()”调用,不需要先 new 对象,方法里也没有 this。
在你的代码里:SkillCatalogue.find(skillName.trim())(CMokeponParser)、Terrain.fromSymbol(...)、UiStyle.GAME_WIDTH。CFighter 的DAMAGE_SCALE也是 static:所有 Fighter 共用同一个 50。 - 字符串 StringString
- 一串文字,类型是 String,用双引号写。它是对象而不是基本类型;用 + 可以把文字和数字拼成一个新字符串;创建后内容不能改。
在你的代码里:log.add("Player's " + player.getName() + " used " + playerSkill.getName() + "!");(CBattle.playTurn)拼出日志 “Player's Impike used Stun!”。 - 用 + 拼接字符串string concatenation
- 字符串和别的值之间的 + 是把它们接成一个新字符串,数字会自动变成文字。从左往右算。
在你的代码里:"Unknown skill: " + name(CSkillCatalogue.find);CLabelledStatBar 的current + SEPARATOR + max得到血条上的 "228 / 280"。 - String.join 拼接String.join
- String.join(分隔符, 一组字符串) 把一组文字用同一个分隔符连成一个字符串。
在你的代码里:String.join(", ", skillNames)(CSelectScreen.formatSkills)把技能名连成 “Lance, Stun, Wait”;String.join("\n", battle.getLog())(CBattleScreen.showLog)把每行日志用换行连起来放进文本框。 - String.valueOf(把数字变成文字)String.valueOf
- 把 int、double 等值变成字符串。Label 的 setText 只收字符串,所以数字要先这样转一下。
在你的代码里:CSelectScreen.select 里String.valueOf(mokepon.getStat(stat)):Impike 的 HP 150(int)变成文字 "150",再写进卷轴上的 Label。 - 三元运算符 ? :ternary operator
- 条件 ? 值A : 值B:条件为 true 时得到值A,否则得到值B,相当于一行写完的 if/else。
在你的代码里:setBackgroundOpacity(highlighted ? HIGHLIGHTED : NORMAL);(CGameButton.setHighlighted)选中时背景是 0.75,否则是 0.5;setOpacity(usable ? 1 : DISABLED_OPACITY);不能用时整个按钮变成 0.4。 - this 关键字this
- 在类的代码里指正在执行这段代码的这个对象自己。this.x 表示这个对象的字段 x(用来和同名参数区分);构造器第一行写 this(...) 表示调用本类的另一个构造器。
在你的代码里:this.x = x;(CPosition 构造器);this(imagePath, NO_TINT);(CSprite);object.interact(this)把当前这个 COverworld 对象交给地图物体。 - void(不返回)void
- 写在方法名前面,表示这个方法只做事、不交回结果,调用它的地方拿不到值。
在你的代码里:public void takeDamage(int amount)(CFighter)只把 HP 减掉,不返回任何东西。 - while 循环while loop
- 只要条件是 true 就一直重复。事先不知道要循环几次时用它。
在你的代码里:while (has(prefix + count + suffix)) { count++; }(CGameData.countEntries)一直数到第一个不存在的 mokepon.N.name,数出 36 只 Mokepon。
Java 进阶
- 注解 @Overrideannotation
- 以 @ 开头、写在代码前面的标注,是给编译器或工具看的说明。你的项目只用到一种:@Override。
在你的代码里:CRandomEnemyStrategy 里@Override public Skill chooseSkill(Fighter enemy, Random random):表示这个方法实现的是接口 IEnemyStrategy 里的方法。 - 缓存cache
- 第一次算出(或读出)一个结果后,把它存起来;以后再要同一个东西时,直接拿存好的,不再重新算或重新读。
在你的代码里:CMapView.getImage:if (!loadedImages.containsKey(path))才UiStyle.loadImage(path)。地图有 400 个格子,但 7 种地形图片每种只从文件读一次。 - 回调callback
- 先把一段代码或一个对象交给别人,等以后某件事发生时,由对方反过来调用它。
在你的代码里:按钮的 setOnAction lambda 是回调;COverworld 调用 IOverworldListener 的onEncounter(...)也是回调;CMarker.interact 反过来调用world.startEncounter(this),让 COverworld 自己去改数据。 - 受检异常和非受检异常checked vs unchecked exception
- 受检异常(例如 IOException)编译器强制你要么 catch,要么在方法头写 throws;非受检异常(RuntimeException 的子类,例如 IllegalArgumentException、NullPointerException)不强制。
在你的代码里:CGameDataReader.read 必须写throws IOException,但它抛出的IllegalArgumentException(缺少 data= 参数)不用声明;CMain.start 用catch (IOException | IllegalArgumentException e)把两种都接住。 - 类字面量(X.class)class literal
- 在类名后面写 .class,得到代表这个类型本身的对象(类型是 Class)。把它交给别的方法,就是告诉那个方法“我说的是这一种类型”。
在你的代码里:CMokeponParser 第 33 行new EnumMap<>(Stat.class):告诉 EnumMap 它的键是枚举 EStat 的常量。 - 命令行参数command-line argument
- 启动程序时写在程序名后面的文字,会传进 main 的 args。JavaFX 程序里也可以用 getParameters().getRaw() 取到。
在你的代码里:build.gradle.kts 里的args("data=mokepon.game")让 gradle run 带上这个参数;CGameDataReader.read 检查第一个参数是不是以data=开头,不是就抛出 “data file: missing argument in the form data=relativeFilePath”。 - 编译期常量compile-time constant
- 一个 static final 的基本类型(或 String)常量,如果它的初始值只由数字和别的这种常量算出来,编译器在编译的时候就把结果算好、直接写进程序里,运行时不用再算。
在你的代码里:CBattleScreen 的PLAYER_CONTENT_X = PLAYER_X + 42:PLAYER_X 是static final int的 50,所以编译时就得到 92;LOG_X = PLAYER_X + PLAYER_WIDTH + 9也一样,编译时就是 609。 - Consumer 接口Consumer
- java.util.function 包里的函数式接口:方法 accept(x) 接收一个值,拿它做点事,不返回结果。
在你的代码里:COverworld 的private void notifyListeners(Consumer<OverworldListener> message)对每个监听者执行message.accept(listener);调用时传入listener -> listener.onPlayerMoved(),就等于让每个监听者执行onPlayerMoved()。 - 接口的默认方法(default)default method
- Java 8 以后,接口里的方法前面可以写 default 并给出方法体。实现这个接口的类可以不写这个方法,直接用默认的版本;想要不同的行为再自己改写。你的项目里没有用到它。
在你的代码里:如果以后有一个监听者只关心出口,可以把 IOverworldListener 的方法写成default void onPlayerMoved() { },那个新类就只需要改写onExitReached。现在 COverworldScreen 五个方法都用到,所以不需要。 - 菱形语法(空着的尖括号)diamond operator
- new 一个泛型类时,右边的尖括号可以空着,只写一个小于号和一个大于号,意思是“类型和左边声明的一样”,Java 自己照着左边填上。这个网站的代码字体会把这两个字符连起来画成一个小菱形。
在你的代码里:CSelectScreen 第 80 行new EnumMap<>(Stat.class):左边写的是Map<Stat, Label>,所以右边空着的尖括号就是<Stat, Label>。 - equals 和 hashCodeequals and hashCode
- equals 决定两个对象算不算相等,默认只有同一个对象才相等。重写 equals 时必须同时重写 hashCode,让相等的对象得到相同的哈希值,否则放进 HashMap 或 HashSet 时会找不到。
在你的代码里:CPosition 重写equals:x 和 y 都相同就相等;hashCode返回Objects.hash(x, y)。AMapObject.isAt 用position.equals(other)比较,因为玩家每走一步都是一个新的 Position 对象,用 == 永远不会相等。 - 异常exception
- 程序运行中出错时被抛出的对象。它会立刻打断当前代码,一层层往调用者传,直到被某个 catch 接住;没人接住,程序就报错。
在你的代码里:数据文件里出现不存在的技能名时,CSkillCatalogue.find 执行throw new IllegalArgumentException("Unknown skill: " + name);。CMain.start 的 catch 只包住了读文件那一句,解析 Mokepon 和地图写在 try 外面,那里的异常不会被它接住。 - 灵活的构造器正文flexible constructor bodies (Java 25)
- 老版本 Java 规定:构造器里的 this(...) 或 super(...) 必须是第一句。Java 25 起放宽了:它们前面可以先写几句不碰正在造的这个对象(this)的语句,比如先检查参数。这个项目用 Java 25 编译,但代码里的 this(...) 和 super(...) 仍然都写在第一句。
在你的代码里:CBattle 三个参数的构造器只有一句this(player, enemy, inventory, new RandomEnemyStrategy(), new Random());,它就是第一句。 - 函数式接口functional interface
- 只有一个抽象方法的接口。凡是需要这种接口的地方,都可以直接写一个 lambda,lambda 的内容就是那个方法要做的事。
在你的代码里:JavaFX 的EventHandler<ActionEvent>(方法 handle)和 Java 的Consumer<OverworldListener>(方法 accept)都是函数式接口。你自己的 IEnemyStrategy 也只有一个方法,不过代码里是用 CRandomEnemyStrategy 这个类来实现它的。 - Gradle 构建工具Gradle
- 用来下载依赖库、编译和运行 Java 项目的工具。设置写在 build.gradle.kts 里,用 gradlew run 运行程序。
在你的代码里:你的 build.gradle.kts 用了application和 JavaFX 插件,设置 Java 25、javafx.controls模块、mainClass.set("au.edu.unimelb.swen.oop.mokepon.Main"),以及运行参数args("data=mokepon.game")。 - new 一个对象时的执行顺序initialization order
- new 一个子类对象时:先运行父类的构造器(构造器第一行的 super(...)),再给本类那些写了初始值的字段赋值(例如 = new ArrayList<>()),最后运行本类构造器里 super 后面的代码。
在你的代码里:new BattleScreen(...):先跑 AGameScreen 的构造器(放进背景图),再执行字段的skillButtons = new ArrayList<>()和itemButtons = new LinkedHashMap<>(),然后才运行第 75 行以后的代码。所以 createSkillButtons 往 skillButtons 里加按钮时,这个列表已经存在了。 - Lambda 表达式lambda expression
- 一小段被当作值传来传去的代码,写成 参数 -> 要做的事;要做好几件事时用大括号包起来。它通常被交给别人保存,等某件事发生时才执行。
在你的代码里:CBattleScreen.createSkillButtons 里的button.setOnAction(event -> useSkill(skill));:创建按钮时并不执行,玩家点这个按钮时才调用useSkill(skill)。 - 模块(module-info.java)module
- Java 的模块系统:module-info.java 写明这个程序需要哪些别的模块(requires),以及把自己的哪些包开放给外面(exports)。
在你的代码里:你的 module-info.java 只有两句:requires javafx.controls;和exports au.edu.unimelb.swen.oop.mokepon;。只导出根包,让 JavaFX 能找到并启动 CMain,其余 8 个包不对模块外公开。 - 一个 catch 接几种异常multi-catch
- catch 的括号里用竖线 | 写几种异常类型,这几种异常都用同一段代码处理。
在你的代码里:CMain.start 的catch (IOException | IllegalArgumentException e):文件读不了,或者没给 data= 参数,都打印 Error loading 加上异常的说明文字,然后结束程序。 - Object 类Object (java.lang.Object)
- 所有 Java 类最终的父类。没写 extends 的类自动继承它,所以每个对象都有 equals、hashCode、toString 等方法。
在你的代码里:CPosition 的equals(Object other)参数类型就是 Object:它重写了 Object 自带的 equals,自带的版本只有在“同一个对象”时才相等。 - 解析parse
- 把文字读懂,变成程序里的数值或对象。
在你的代码里:Integer.parseInt(getText(key))(CGameData.getNumber)把文字 “150” 变成整数 150;CMokeponParser.parse 把 mokepon.0 到 mokepon.35 变成 36 个 CMokepon 对象。 - 属性文件(Properties)properties file
- 一行一个“键=值”的文本文件。Java 的 Properties 类用 load 把它读进来,再用 getProperty(键) 取值,取到的都是字符串。
在你的代码里:mokepon.game 就是属性文件,例如mokepon.14.name=Impike、map.player.startingPos=10,18;CGameDataReader 用properties.load(input)读入,CGameData.getText 用properties.getProperty(key)取值。 - 随机数 Randomjava.util.Random
- 产生随机数的对象。nextInt(n) 得到 0 到 n-1 之间的整数,nextDouble() 得到大于等于 0、小于 1 的小数。
在你的代码里:ABoostSkill 的random.nextInt(MAX_BOOST) + 1得到 1 到 10;ATargetedSkill.hits 里random.nextDouble() >= calculateHitChance(user, target)时算没打中。CBattle 的 5 参数构造器可以传入一个指定的 Random,方便测试得到固定结果。 - 随机种子(seed)random seed
- 造 Random 时可以给它一个数,比如 new Random(42),这个数叫种子。种子相同,之后 nextDouble()、nextInt() 得到的一串随机数就完全相同,所以每次运行的结果都一样,适合测试。不给种子(new Random())时,每次运行都不一样。
在你的代码里:CBattle 的 5 参数构造器可以传进new Random(42):每次打中、没打中都一样,测试就能检查一份固定的getLog()。游戏本身用的是 3 参数构造器,里面是new Random(),所以每局都不同。 - == 和 equals 的区别== vs equals
- 用在对象上时,== 比较是不是同一个对象,equals 比较内容是否相等(要看这个类怎样写 equals)。比较 String 的内容要用 equals。每个枚举常量全程只有一个对象,所以枚举可以直接用 ==。
在你的代码里:skill.getName().equals(name)(CSkillCatalogue.find)比较文字;terrain == strongTerrain(EMokeponType)和effect != TerrainEffect.NEUTRAL(CEncounter)比较枚举。 - 相对路径relative path
- 不从盘符或根目录写起的文件路径,例如 mokepon.game、assets/Impike.png。Java 从程序运行时所在的文件夹(工作目录)开始找它。
在你的代码里:args("data=mokepon.game")和数据文件里的assets/Impike.png都是相对路径。gradlew run 在项目文件夹里启动程序,所以 CGameDataReader 能找到项目根目录下的 mokepon.game,CUiStyle.loadImage 也能找到 assets 文件夹里的图片。 - 浅复制shallow copy
- 复制一个列表时,只造一个新的列表“外壳”,里面装的还是原来那些对象(同样的引用),对象本身没有被复制。往新列表里加、删,不影响旧列表;但如果去改里面某个对象,两边看到的是同一个对象。
在你的代码里:COverworld 构造器this.objects = new ArrayList<>(map.getObjects());:新列表可以 remove,CGameMap 的列表不受影响;两个列表里是同一批 CMarker、CMysteryBox、CExit 对象。它们的字段都是 final,不会变,所以共享是安全的。 - 异常堆栈(stack trace)stack trace
- 异常没有被任何 catch 接住时,Java 打印的一串 at …… 行。最上面一行是真正出错的地方,往下一行是调用它的地方,一层一层退回到最开始。
在你的代码里:把一个不是游戏数据的文件交给程序时,打印的最后一段是at ...GameData.getPosition(GameData.java:30)、at ...MapParser.parse(MapParser.java:27)、at ...Main.start(Main.java:36):CMain 第 36 行调用 CMapParser.parse,它第 27 行调用 getPosition,第 30 行出错。 - throw 抛出异常throw
- throw 后面跟一个异常对象,表示这里出错了:立刻停止当前方法,把异常抛给调用者。
在你的代码里:throw new IllegalArgumentException("Unknown tile: " + symbol);(ETerrain.fromSymbol):地图里出现 B F G O P R W 以外的字母时报错。 - throws 声明throws clause
- 写在方法头后面,告诉调用者这个方法可能抛出这种异常、需要处理。受检异常必须这样声明,或者在方法里自己接住。
在你的代码里:public GameData read(List<String> arguments) throws IOException(CGameDataReader):文件不存在时,Files.newInputStream抛出的异常交给调用它的 CMain.start 处理。 - try / catch 捕获异常try-catch
- 把可能出错的代码放进 try 块;出错时跳到类型匹配的 catch 块里处理,程序不会直接崩溃。catch 里用竖线可以一次接住几种异常。
在你的代码里:CMain.start:try { data = new GameDataReader().read(getParameters().getRaw()); } catch (IOException | IllegalArgumentException e) { ... },出错时打印 “Error loading ” 加上异常信息,然后Platform.exit()并 return,不打开游戏窗口。 - try-with-resources(自动关闭资源)try-with-resources
- 在 try 后面的括号里打开文件这类用完必须关闭的资源,代码块结束时 Java 自动关闭它,即使中途出错也会关。
在你的代码里:CGameDataReader.read:try (InputStream input = Files.newInputStream(dataFile)) { properties.load(input); }读完 mokepon.game 后文件自动关闭。这里没有 catch,出错时异常继续往外传给 CMain.start。 - 不支持的操作异常UnsupportedOperationException
- 对一个不允许修改的列表(List.of、List.copyOf 造的,或者 Collections.unmodifiableList 交出来的只读视图)调用 add、remove 这类修改方法时,Java 在运行时抛出的异常。编译时不会报错,运行到这一句才出错。
在你的代码里:CBattle.getLog() 返回Collections.unmodifiableList(log):CBattleScreen 能读它、能用String.join把它接起来,但如果写battle.getLog().add("hi"),运行时就会抛出 UnsupportedOperationException。 - 可变参数varargs
- 参数类型后面写三个点(例如 Node... nodes),调用时可以传任意多个值,方法里把它们当成一个数组使用。
在你的代码里:AGameScreen 的protected void addToView(Node... nodes);CBattleScreen 一次传了 6 个:addToView(skillButtonColumn, itemButtonRow, actionLabel, logArea, playerPanel.getFaintMask(), enemyPanel.getFaintMask());。 - 变量捕获captured variable
- lambda 里可以使用它外面的局部变量,并记住创建那一刻这个变量的值。被用到的局部变量之后不能再被重新赋值(必须事实上是 final)。
在你的代码里:button.setOnAction(event -> select(mokepon, button));(CSelectScreen.createMokeponList)在循环里执行 36 次,每个 lambda 记住的都是自己那一轮的mokepon和button,所以点哪个名字就选中哪一只。
JavaFX
- 快捷键(accelerator)accelerator
- 给菜单项绑定的键盘组合键,不打开菜单、直接按键也能执行它。菜单项被禁用时,快捷键也不起作用。
在你的代码里:CGameMenu:runAway.setAccelerator(new KeyCodeCombination(KeyCode.R, KeyCombination.CONTROL_DOWN));是 Ctrl+R;Main menu 是 Ctrl+B,Exit 是 Alt+F4。 - 不透明度 alphaalpha (opacity of a colour)
- 颜色的第四个数:0 是完全透明,1 是完全不透明。半透明的颜色盖在别的东西上,下面的东西还看得见,只是被染上了这个颜色。
在你的代码里:CFighterScroll 的FAINT_TINT = Color.web("#b41e1e", 0.35):35% 不透明的深红色,所以倒下一方的卷轴变红,但头像和名字还看得见;CModalDialog 的深色那一层是rgba(0, 0, 0, 0.45)。 - Application 类javafx.application.Application
- JavaFX 程序的起点类。你的类继承它并重写 start(Stage);调用 launch(args) 后,JavaFX 准备好界面线程、创建第一个窗口,再调用 start。
在你的代码里:public class Main extends Application,main里launch(args);,然后 JavaFX 调用start(Stage stage):读数据、解析、new GameWindow(stage, mokepons, map, images)、gameWindow.show()。 - JavaFX 界面线程JavaFX Application Thread
- JavaFX 用同一个线程来运行 start、所有事件处理器和画界面。处理器要尽快做完,否则界面会卡住;修改界面也必须在这个线程上进行。
在你的代码里:你的项目没有自己开任何线程:点技能按钮后,battle.playTurn(skill)和update()都在这个线程上一口气做完,之后 JavaFX 才把新画面画出来。 - BorderPaneBorderPane
- 分成上、下、左、右、中五块区域的布局容器,用 setTop、setCenter 等方法放内容。
在你的代码里:CGameWindow 的root:root.setTop(menu.getMenuBar())把菜单栏固定在上面,root.setCenter(screen.getView())显示当前界面,换界面就是换中间这一块。 - 可以点击的控件。setOnAction 注册点击后要运行的代码;fire() 用代码按一下它;被禁用的按钮点不动,fire() 也没有效果。
在你的代码里:CGameButton extends Button;CBattleScreen.handleKey 用skillButtons.get(index).fire();让数字键 1–4 等于点对应的技能按钮,能量不够的按钮是禁用的,按数字键也没用。 - JavaFX 按钮自带的方法:用代码代替鼠标点一下。按钮能用时,它运行 setOnAction 里登记的那段代码;按钮被禁用(disabled)时,什么都不做。
在你的代码里:CBattleScreen.handleKey 里的skillButtons.get(index).fire();:按数字键 2,就等于点了第 2 个技能按钮,走的是同一个 lambdaevent -> useSkill(skill)。 - 裁剪 setClipclip
- 给节点设置一个形状,只有形状里面的部分会显示,外面的部分被剪掉。
在你的代码里:CFighterScroll.createFaintMask 造一个半透明红色的Rectangle,再mask.setClip(scrollShape)用卷轴图片的形状裁剪它,所以倒下时只有卷轴那块变红,卷轴外面透明的角落不变红。 - 图文位置 ContentDisplayContentDisplay / setGraphic
- 按钮和 Label 除了文字,还能用 setGraphic 放一个图形(比如一张小图)。setContentDisplay 决定图形放在文字的哪一边:TOP 是图在上、字在下。
在你的代码里:CBattleScreen.createItemButtons:button.setGraphic(UiStyle.createIcon(...))放道具图标,button.setContentDisplay(ContentDisplay.TOP)让图标在上、数量在下。 - 禁用和透明度(setDisable / setOpacity)disable / opacity
- setDisable(true) 让控件不能点,也不响应 fire 和快捷键;setOpacity 设置整个节点的不透明度,1 是完全不透明。
在你的代码里:CGameButton.setUsable:setDisable(!usable),再setOpacity(usable ? 1 : DISABLED_OPACITY),所以能量不够的技能按钮会变淡(0.4);地图上走不过去的方向按钮也是这样变灰的。 - 禁用(变灰)disabled
- 按钮或菜单项被设成“不能用”:点了没有反应,看起来变淡、变灰。
在你的代码里:CGameButton 的setUsable(false)会调用setDisable(true),并把整个按钮的透明度降到 0.4。 - 绘制顺序(谁盖住谁)drawing order
- 同一个容器里,子节点按加入的先后画出来,后加入的画在上面,会挡住前面的。
在你的代码里:AGameScreen.addToView 的注释 “later nodes are drawn on top”;CBattleScreen 最后才加两个倒下遮罩;CMapView 先加地形层、再加物体层、最后加玩家,所以玩家站在感叹号上时会盖住它。 - 事件event
- 表示发生了一件事的对象,例如点了按钮(ActionEvent)、按了键(KeyEvent)。JavaFX 把它作为参数交给事件处理器。
在你的代码里:lambdaevent -> useSkill(skill)里的event就是点击产生的 ActionEvent(这段代码没有用到它);AGameScreen.handleKeyPress 的参数KeyEvent event是一次按键。 - 消费事件 consume()consuming an event
- 对一个事件调用 event.consume(),表示“这个事件已经处理完了”,JavaFX 就不再把它继续交给别的部件处理。
在你的代码里:COverworldScreen.handleKey:是方向键时先movePlayer(direction),再event.consume(),这个方向键就不会再被画面上的按钮拿去做别的事(比如移动键盘焦点)。 - 事件驱动event-driven programming
- 程序启动、把界面搭好以后就停下来等待;之后每一次点击或按键产生一个事件,才运行对应的处理代码,处理完又继续等。
在你的代码里:CMain.start 执行完、选择界面出现以后,你的代码一行也不运行;直到你点一个名字按钮,event -> select(mokepon, button)才执行。 - 事件过滤器event filter
- 用 addEventFilter 注册的处理器,会在事件到达真正被点中或有焦点的控件之前先运行,所以能抢先拿到事件。
在你的代码里:CGameWindow 构造器:scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> currentScreen.handleKeyPress(event));,注释说明用过滤器是为了让界面在任何按钮或文本框之前拿到按键。 - 事件处理器event handler
- 事件发生时要运行的代码。在 JavaFX 里它是 EventHandler 接口的对象,通常直接写成 lambda,用 setOnAction 等方法注册到控件上。
在你的代码里:CModalDialog.addButton 的参数是EventHandler<ActionEvent> action,里面button.setOnAction(action);COverworldScreen 传进去的event -> { closeDialog(); getNavigator().startBattle(); }就是 Fight! 按钮的处理器。 - 事件循环event loop
- JavaFX 界面线程里一直转的循环:等一件事(点击、按键)→ 找到登记好的处理器 → 运行它 → 重画窗口 → 接着等。start() 返回以后,程序就停在这个循环里。
在你的代码里:异常堆栈最下面的WinApplication._runLoop(Native Method)就是 Windows 上的这个循环:它调用了 CMain.start,以后也由它调用event -> select(mokepon, button)这样的 lambda。 - 用代码按按钮 fire()Button.fire()
- JavaFX 按钮自带的方法:在代码里“按一下”这个按钮,效果和用鼠标点一样,会运行 setOnAction 登记的动作。按钮被禁用(disabled)时,fire() 什么也不做。
在你的代码里:CBattleScreen.handleKey:按数字键 1 时执行skillButtons.get(0).fire(),等于点了第一个技能按钮;如果这个按钮因为能量不够变灰了,就没有任何反应。 - 键盘焦点focus
- 当前接收键盘输入的那个控件,按空格键会作用在有焦点的按钮上。requestFocus() 让某个控件拿到焦点。
在你的代码里:CModalDialog.focusFirstButton 让对话框的第一个按钮(例如 Fight!)requestFocus();同时 AGameScreen.handleKeyPress 在对话框打开时不处理方向键,所以玩家走不动。 - 字体 FontFont
- 文字的字体和大小。Font.loadFont 从字体文件加载,Font.font(名字, 粗细, 大小) 使用 JavaFX 的逻辑字体。
在你的代码里:CUiStyle.loadGameFont 加载assets/GrenzeGotisch-Black.ttf(标题和按钮上的哥特字体);日志和数字用createMonospaceFont,也就是等宽的 Monospaced 字体。 - GridPane(网格布局)GridPane
- 按行和列摆放子节点的布局容器;add(节点, 列, 行) 把节点放进第几列第几行,都从 0 开始数。
在你的代码里:COverworldScreen.createMovePad 用pad.add(createMoveButton(Direction.UP, images), 1, 0);把“上”放在第 1 列第 0 行,左、下、右放在第 1 行;CSelectScreen 和 CPlayerStatsScroll 的属性表也是 GridPane。 - ImageView(图片视图)ImageView / Image
- Image 是加载进来的图片数据,ImageView 是把它显示在界面上的节点,可以设置显示的大小和位置。同一个 Image 可以给很多 ImageView 共用。
在你的代码里:CUiStyle.loadImage 用new Image("file:" + path)加载图片,createImageView(image, x, y, width, height)造出显示它的 ImageView;地图上每一个格子都是一个 32 × 32 的 ImageView。 - JavaFXJavaFX
- Java 用来做图形界面的库:窗口、按钮、文字、图片、进度条、菜单都来自它。
在你的代码里:只有 ui 包(除接口 IGameNavigator 外的 15 个类)和 CMain import 了 javafx;CBattle、COverworld 这些规则类完全不知道 JavaFX,所以不开窗口也能运行和测试。 - JavaFX 样式(setStyle)JavaFX CSS style
- 用 setStyle 给节点写类似网页 CSS 的样式,属性名以 -fx- 开头,例如背景色、圆角、文字颜色。
在你的代码里:CGameButton.setBackgroundOpacity 写的是"-fx-background-color: rgba(211,211,211," + opacity + ");"再加上圆角和黑色文字;CModalDialog 的遮罩是rgba(0, 0, 0, 0.45)。 - 视觉效果 EffectEffect (DropShadow, Lighting)
- 加在节点上的画面效果,例如阴影、光照,用 setEffect 设置。
在你的代码里:CUiStyle.tint 用Lighting加一盏有颜色的光,给感叹号和门染色;createWhiteOutline()用白色的DropShadow让黑字在背景上更清楚(选择界面的标题和名字)。 - 键盘事件 KeyEventKeyEvent
- 按键产生的事件。getCode() 得到是哪个键(KeyCode.UP、KeyCode.DIGIT1 等),isControlDown() 等判断有没有同时按着 Ctrl 之类的键,consume() 表示这个事件已经处理完、不再往下传。
在你的代码里:COverworldScreen.handleKey 用ARROW_KEYS.get(event.getCode())把方向键变成 EDirection,移动后event.consume();AGameScreen.handleKeyPress 遇到按着 Ctrl、Alt、Meta 或 Shift 的组合键就直接 return,留给菜单快捷键。 - 标签 LabelLabel
- 只显示一段文字(也可以带图片)的控件,不能点击,用 setText 改文字。
在你的代码里:地图下面的状态栏是 COverworldScreen 的statusBar,一个 Label:捡到道具时statusBar.setText("You picked up a " + item.getName() + "!")。 - 布局与坐标(layoutX / layoutY)layout
- layoutX 是节点离父容器左边多少像素,layoutY 是离上边多少像素。BorderPane、VBox、HBox、GridPane、StackPane 这些布局容器会自动排位置,Pane 则要自己设坐标。
在你的代码里:CBattleScreen 的PLAYER_Y = 272是界面 Pane 里的坐标;因为窗口上方有 25 像素高的菜单栏,截图里玩家卷轴出现在 y = 297。 - 窗口顶部的菜单栏(MenuBar)里放一个或多个菜单(Menu),点开菜单会出现菜单项。
在你的代码里:CGameMenu 造了一个叫 “Game” 的Menu,放进new MenuBar(gameMenu);CGameWindow 用root.setTop(menu.getMenuBar())把它放在窗口最上面(截图里 y 是 0 到 25)。 - 菜单里可以点的一行。setOnAction 设置点了以后做什么,setDisable(true) 让它变灰、点不了。SeparatorMenuItem 是一条分隔线。
在你的代码里:CGameMenu 有三个菜单项 “Run away”、“Main menu”、“Exit”,Exit 前面有一条分隔线;update(screen)里runAway.setDisable(!screen.canRunAway()),按当前界面决定变不变灰。 - 模态对话框modal dialog
- 弹出以后必须先在它上面做出选择,才能继续操作后面界面的对话框,“模态”就是这个意思。
在你的代码里:CModalDialog 用一层盖满整个界面的半透明黑色 StackPane 挡住后面的东西,中间是白色的框(标题、消息、按钮):遇敌时是 “Encountered a Hexwing” 加 Fight! 和 Run Away!,打赢时是 “You Win” 加 Back to it!。 - 节点 NodeNode
- 场景图里的任何一个东西:按钮、文字、图片、形状、容器都是 Node 的子类。
在你的代码里:AGameScreen 的addToView(Node... nodes)能接收任何节点;CFighterPanel.getNodes 返回List<Node>,里面有卷轴图片、头像、名字和两条进度条。 - 面板 PanePane
- 最简单的容器:子节点放在哪里由你用 layoutX 和 layoutY 自己决定,它不会自动排版。
在你的代码里:AGameScreen 的private final Pane view = new Pane();大小是 1280 × 768,每个界面的东西都按坐标常量放进去;CMapView 也用 Pane 叠出地形层、物体层和玩家。 - Platform.exit(结束程序)Platform.exit
- 让 JavaFX 关闭所有窗口、结束程序。
在你的代码里:CGameWindow.quit 里的Platform.exit();(菜单的 Exit 和对话框的 Quit 按钮都会走到这里);CMain.start 读数据失败时也调用它。 - 希望的大小(setPrefSize)preferred size
- setPrefSize(宽, 高) 告诉 JavaFX “我希望这个部件这么大”。真正排版时 JavaFX 还会考虑里面的内容和最小大小,最后量出来可能差一点。
在你的代码里:CSelectScreen.createChooseButton 写了setPrefSize(160, 160),截图里 Choose 按钮量出来是 160 × 161。 - 进度条 ProgressBarProgressBar
- 按 0 到 1 之间的比例显示一条填充的进度条;setProgress 改比例,样式里的 -fx-accent 改颜色。
在你的代码里:CStatBar 里new ProgressBar(calculateRatio(current, max)),HP 用 limegreen,能量用 royalblue;只要数值大于 0 就至少显示 3%(MIN_VISIBLE_RATIO = 0.03),所以 HP 只剩 1 时还能看到一点绿色。 - Rectangle2D(描述一块长方形)Rectangle2D
- 只装 4 个数字的小对象:左上角的 x、y,和宽、高。它自己画不出来,只用来说明“哪一块地方”。用 getMinX()、getMinY()、getWidth()、getHeight() 取出数字。和画得出来的 javafx.scene.shape.Rectangle 不是一回事。
在你的代码里:CBattleScreen 的构造器写new Rectangle2D(PLAYER_X, PLAYER_Y, PLAYER_WIDTH, PLAYER_HEIGHT),告诉 CFighterPanel 你的卷轴放在哪;CFighterPanel 再用area.getMinY()当作卷轴的顶边 top。 - 行宽列宽的规定(ColumnConstraints / RowConstraints)ColumnConstraints / RowConstraints
- 交给 GridPane 的“这一列多宽”“这一行多高、里面的东西靠上还是居中”的规定。按加进去的顺序,第 0 个规定管第 0 列(或第 0 行),第 1 个管第 1 列,以此类推。
在你的代码里:CSelectScreen.createStatsGrid 加了两列new ColumnConstraints(74)和new ColumnConstraints(260);addStatRow 每加一行就加一个new RowConstraints(height),Skills 那一行的 height 是 2 × 25 = 50。 - Scene(场景)Scene
- 窗口里装着的全部内容,它持有一个根节点(root)。键盘事件会先经过 Scene,所以可以在 Scene 上加事件过滤器。
在你的代码里:CGameWindow 只造了一个new Scene(root);换界面时不换 Scene,只是root.setCenter(screen.getView())换掉中间的内容。 - 场景图(节点树)scene graph
- JavaFX 把界面上的东西组织成一棵树:Scene → 根节点 → 子节点 → 更小的子节点。
在你的代码里:Scene →root(BorderPane)→ 上面是菜单栏,中间是当前界面的view(Pane)→ 里面依次是背景图、卷轴、按钮、日志框等;地图界面的view里还有 CMapView 的 Pane,它又分成地形层、物体层和玩家图片。 - ScrollPane(滚动面板)ScrollPane
- 内容比显示区域大时,出现滚动条让你滚动查看的容器。
在你的代码里:CSelectScreen.createMokeponList 把 36 个名字按钮所在的 VBox 放进new ScrollPane(buttonColumn):横向滚动条是NEVER,纵向滚动条是AS_NEEDED(需要时才出现)。 - setOnAction(注册点击动作)setOnAction
- 按钮和菜单项的方法:交给它一段代码(通常是 lambda),以后每次被点击、被 fire() 或被快捷键触发时运行。
在你的代码里:CSelectScreen 的 Choose 按钮:button.setOnAction(event -> getNavigator().startGame(selectedMokepon));;CGameMenu:mainMenu.setOnAction(event -> navigator.returnToMainMenu());。 - 形状 Circle / Rectangleshape
- JavaFX 里可以直接画出来的几何图形节点,可以设置填充色和边框。
在你的代码里:CSelectScreen.createPortraitCircle 用new Circle(CIRCLE_CENTRE_X, CIRCLE_CENTRE_Y, CIRCLE_RADIUS)画头像后面的灰色圆圈(半径 163,黑色边框);CFighterScroll 的倒下遮罩是一个Rectangle。 - StackPane(叠放布局)StackPane
- 把子节点一层层叠在一起,并默认放在正中间的容器。
在你的代码里:CModalDialog 的layer是一个 1280 × 768 的 StackPane,背景是半透明的黑色,白色的对话框放在它正中间。 - Stage(窗口)Stage
- JavaFX 里的一个窗口。可以设置标题、能不能改大小、放哪个 Scene,最后 show() 显示出来。
在你的代码里:CGameWindow 构造器:stage.setTitle("Mokepon");、stage.setResizable(false);、stage.setScene(scene);、stage.sizeToScene();;show()方法里是stage.show();。 - 多行文本框 TextAreaTextArea
- 可以显示很多行文字、能滚动的文本框;setEditable(false) 后用户只能看,不能改。
在你的代码里:CBattleScreen 的logArea就是战斗日志框:area.setEditable(false)、等宽字体;每次更新都logArea.setText(String.join("\n", battle.getLog())),再滚到最底下。 - VBox 和 HBoxVBox / HBox
- VBox 把子节点从上到下排成一列,HBox 从左到右排成一行;构造器里的数字是相邻两个之间的间距。
在你的代码里:CBattleScreen 的技能按钮放在new VBox(SKILL_GAP)(间距 8)里,道具按钮放在new HBox(ITEM_GAP)里;CModalDialog 的消息区是 VBox,按钮行是 HBox。 - 看不见和不占位置(setVisible / setManaged)visible / managed
- setVisible(false) 只是不画出来,位置还留着;setManaged(false) 让父容器(HBox、VBox、GridPane……)排版时当它不存在,不给它留位置。两个都设成 false,才是“看不见,也不占地方”。
在你的代码里:CSelectScreen.showTypeIcon 同时调用typeIcon.setVisible(visible)和typeIcon.setManaged(visible):NORMAL 类型没有图标时,名字一行里不留空位,名字还在正中间。 - 自动换行(setWrapText)wrap text
- Label 默认只写一行,太长的字会被截断显示成省略号。setWrapText(true) 让它在宽度不够时换到下一行接着写。
在你的代码里:CSelectScreen 的构造器对skillsValue调用setWrapText(true),再让 Skills 这一行有两行高,技能名太长时就会写到第二行。
OOP
- 监听者listener (observer)
- 先登记在另一个对象那里、等着收消息的对象。被登记的对象出事时,会挨个调用监听者的方法。
在你的代码里:COverworldScreen 是 COverworld 的监听者:玩家走一步后,Overworld 调用它的onPlayerMoved()。
UML 与课程用语
- 聚合aggregation
- 一种“整体—部分”的关联,但部分可以独立存在,也可以被别的对象共用:整体没了,部分还在。UML 画成实线,整体那一端是空心菱形。
在你的代码里:CBattle 的inventory: Inventory:背包由 COverworld 创建,CPlayer 也拿着它,战斗结束后它还在;CMokepon 技能列表里的技能对象也被所有 Mokepon 共用。 - 关联association
- 一个类长期认识另一个类:在字段里存着对方的引用,随时可以调用对方。UML 画成实线,可以加箭头表示方向。
在你的代码里:CMarker 的字段picker: EnemyPicker;CBattleScreen 的字段battle: Battle。打印的 UMLDoclet 图不画这种线,要从字段类型看出来。 - 类图class diagram
- 一个类画成分三格的方框:最上面是类名(抽象类和接口的名字是斜体),中间是字段,写成“名字: 类型”,下面是构造器和方法,写成“名字(参数类型): 返回类型”。只写参数类型,不写参数名;静态成员加下划线,抽象方法用斜体。
在你的代码里:CFighter 的图里有stats: Map<Stat, Integer>和calculateDamage(Fighter, double): int;带下划线的DAMAGE_SCALE: int表示它是 static。 - 组合composition
- 最强的“整体—部分”关系:部分由整体自己创建、只属于这个整体,整体不用了,部分也一起被丢掉。UML 画成实线,整体那一端是实心菱形。
在你的代码里:CFighter 自己创建的stats映射和effects列表;CBattleScreen 在构造器里 new 出来的两个 CFighterPanel,战斗界面被换掉后它们也一起被丢掉。 - 包含关系(谁存着谁)containership
- 课程用语,指一个类的对象在字段里存着另一个类的对象,也就是 has-a。回答设计题时要说清:哪个类、用哪个字段、存着谁、在哪里创建的。
在你的代码里:CBattle 存着两个 CFighter(player、enemy)、一个 CInventory、一个 IEnemyStrategy、一个 Random 和日志log;每个 CFighter 又存着它的 CMokepon 和effects列表。 - 控制流control flow
- 代码执行和方法调用的先后顺序:先运行哪一句、调用了谁、什么时候返回。
在你的代码里:点 Stun:按钮的 lambda → CBattleScreen.useSkill → CBattle.playTurn → ASkill.use → ATargetedSkill.applyEffect → CStun.applyHit,一层层返回以后再执行update()。 - 创建消息(顺序图里的 new)create message
- 顺序图里表示“new 一个对象”的箭头。按 UML 习惯画成虚线、实心箭头头,指向被造出来的那个对象。它和“返回”不一样:返回也是虚线,但箭头头是空心的(两道线),方向指回调用的人。
在你的代码里:点 Fight! 以后:CEncounter →new Fighter(…) ×2、new Battle(…),CGameWindow →new BattleScreen,都是创建消息。 - 数据流data flow
- 数据存在哪里、被谁读、被谁改、怎样传到界面上。
在你的代码里:HP 存在 CFighter 的stats里;技能通过takeDamage修改它;CFighterPanel.update 用fighter.getHp()读出来交给 CStatBar 显示;CMokepon 里的原始数值全程不变。 - 委托delegation
- 一个对象自己不做某件事,而是调用另一个对象的方法让它来做,再把结果拿回来用。
在你的代码里:CBattleScreen.useSkill 把整个回合委托给battle.playTurn(skill);CFighter.getName 委托给mokepon.getName();CMarker.chooseEnemy 委托给picker.pick(mokepons, terrain, random)。 - 依赖dependency
- A 用到了 B(作为参数、局部变量、返回值,或者调用 B 的静态方法),但没有在字段里长期存着 B;B 一改,A 可能也要改。UML 画成带箭头的虚线。
在你的代码里:ASkill 没有任何 Fighter 字段,但use(Fighter user, Fighter target, Random random)依赖 CFighter;CMokeponParser 调用SkillCatalogue.find,所以依赖 CSkillCatalogue。 - 可扩展性extensibility
- 以后加新功能时,需要改动的旧代码有多少。好的设计让加功能主要是加新类,而不是到处改旧类。
在你的代码里:加一个新技能:写一个 ASkill 的子类,再在 CSkillCatalogue 的列表里加一个对象;CBattle、CFighter、CBattleScreen 都不用改。 - 结束状态final state
- 状态图里“走到这里就结束、再也出不来”的状态。一张图里可以画好几个结束状态,它们是同一个意思,多画几个只是为了让线不交叉。
在你的代码里:第 15 章状态图里的两个“程序结束”框:出口框和 Game Over 框的 Quit 都走到这里,运行的是 CGameWindow.quit() 里的Platform.exit()。 - 泛化(继承关系的画法)generalization
- UML 里对继承的叫法:实线加空心三角箭头,箭头指向父类。
在你的代码里:CBite 的 UML 图里一条实线指向 AAttackSkill;CMain 的图里实线指向 JavaFX 的 Application。 - 实例化(谁创建谁)instantiation
- 用 new 创建某个类的对象。设计题常问:哪个类在哪个方法里创建了哪个对象。
在你的代码里:CEncounter.createBattle 创建两个 CFighter 和一个 CBattle;CGameWindow.startBattle 创建 CBattleScreen;CLance.applyHit 每次命中都执行new Bleeding(user)。 - 交互(类之间怎样配合)interaction
- 课程设计题的一类:说明为了完成一件事,哪些类的对象怎样配合——谁创建谁(instantiation)、谁存着谁(containership)、谁调用谁(delegation),数据存在哪里、怎样传递。
在你的代码里:一个战斗回合:CBattleScreen 存着 CBattle 并调用playTurn;CBattle 存着两个 CFighter 并把它们交给Skill.use;技能再调用 CFighter 的spendEnergy、takeDamage改数据。 - Javadoc 文档工具Javadoc
- JDK 自带的工具,读取源码生成 API 说明网页。它只把两个星号开头的文档注释当作说明文字;UMLDoclet 就是在 Javadoc 运行时顺便画类图的插件。
在你的代码里:你的项目生成的 apidocs 文件夹里,每个类有一个网页和一张同名的 png 类图。你的注释都以一个星号开头(不是文档注释),所以网页里不会出现这些说明文字。 - 多重性(数量)multiplicity
- 关联线两端写的数量,表示一个对象关联几个对方,例如 1、0..1、0..*(任意多个)。打印的 UMLDoclet 图不画这些线,答题时可以用字段类型说明数量。
在你的代码里:CBattle 对 CFighter 是 2 个(player和enemy两个字段);CFighter 对 AStatusEffect 是 0..*(List<StatusEffect> effects);COverworld 的encounter是 0..1,没有遇敌时是 null。 - 实现关系的画法realization
- UML 里对“类实现接口”的画法:虚线加空心三角箭头,箭头指向接口。
在你的代码里:CRandomEnemyStrategy 的图里一条虚线指向 IEnemyStrategy;CGameWindow 的图里虚线指向 IGameNavigator。 - 顺序图(也叫时序图)sequence diagram
- 按时间从上往下画“谁调用了谁”的图:每个对象一条竖线(生命线),每次调用是一条横向箭头。类图看不出调用顺序,时序图专门画这个。
在你的代码里:一个回合:CBattleScreen →battle.playTurn(skill)→playerSkill.use(player, enemy, random)→enemyStrategy.chooseSkill(enemy, random)→enemySkill.use(enemy, player, random)→endTurn()。 - 状态图state diagram (state machine diagram)
- 一种 UML 图:每个框是程序可能处在的一个“状态”(比如正在显示哪个画面、有没有对话框开着),每条箭头是让程序从一个状态走到另一个状态的事件,常常在箭头上写出触发它的方法。它和类图不同:类图画“有哪些类、谁存着谁”,状态图画“程序现在在哪、接下来能去哪”。
在你的代码里:第 15 章的状态图:CSelectScreen →startGame()→ COverworldScreen → 踩到感叹号 → 遇敌框 →startBattle()→ CBattleScreen → 菜单 Run away →runAway()→ 回到 COverworldScreen。 - UML 统一建模语言UML (Unified Modeling Language)
- 一套画软件设计图的标准画法。最常用的是类图:用方框表示类,用不同的线和箭头表示类之间的关系。
在你的代码里:考试卷上打印的是用你这份代码生成的 UML,每个类一张图,例如 CFighter 的图里列出了它的全部字段、构造器和方法。 - 类框的格子compartment
- UML 的类框用横线分成几格:最上面一格是类头(种类圆圈、类名、包名),中间一格是字段,最下面一格是构造器和方法。某一格一样东西都没有时,UMLDoclet 就不画这一格。
在你的代码里:CBattle 的图有三格:类头、8 个字段、2 个构造器加 10 个方法。CMain 没有字段,所以只有类头和方法两格。 - 类别圆圈 C / A / I / Ekind icon
- UMLDoclet 在类名左边画一个带字母的小圆圈:C 是普通类(绿色),A 是抽象类(浅蓝色),I 是接口(紫色),E 是枚举(橙红色)。
在你的代码里:CFighter 是 C,AGameScreen 是 A,IEnemyStrategy 是 I,EStat 是 E;这个网站里类名小标签用的也是同样的字母和颜色。 - 父类 / 接口的方框parent box (UMLDoclet)
- UMLDoclet 在一个类的图上方,画出它直接的父类或它实现的接口,用实线(extends)或虚线(implements)加空心三角连起来。这个方框里只列出父类或接口的抽象方法(斜体),不列字段、构造器和已经写好内容的方法;没有抽象方法时,方框里只有名字。
在你的代码里:CBite 图上的 AAttackSkill 方框是空的;CRandomEnemyStrategy 图上的 IEnemyStrategy 方框里有chooseSkill(Fighter, Random): Skill;CMain 图上的Application方框里有start(Stage): void。 - UMLDocletUMLDoclet
- 一个配合 Javadoc 使用的工具,从 Java 源码自动画出 UML 类图(你的图是 2.3.2 版画的)。每个类单独一张图;有继承或实现时,会画出父类或接口的方框,但不列出它的成员;其他关系(谁存着谁、谁调用谁)都不画。
在你的代码里:CBite 的图上方是只有名字的 AAttackSkill 方框,中间一条实线加空心三角;CRandomEnemyStrategy 的图上方是 IEnemyStrategy,连线是虚线加空心三角。 - 可见性符号visibility
- UML 用符号表示访问权限:+ 是 public,- 是 private,# 是 protected,~ 是包内可见。UMLDoclet 把它们画成彩色小图标:绿色圆点是 public,红色方块是 private,黄色菱形是 protected,蓝色三角是包内可见;空心的图标是字段,实心的是方法。
在你的代码里:CPlayer 的图:5 个字段前面是空心红方块(private),构造器和 4 个 getter 前面是实心绿圆点(public),moveTo、moveBack、addWin前面是蓝色三角(包内可见);AGameScreen 的handleKey前面是黄色菱形(protected)。