Project_2

19302010020 袁逸聪

构架设计

工程文件由两个包构成:GameData和GUI 另有与src同级的文件夹map存放存档文件,audio存放音频,pic存放图片,resources/fonts存放字体

GameData包

分为Lattice、Map、MotaGame三个类,其他类都是Lattice的子类 GameData中构建了游戏逻辑,可以存取完整的游戏数据、设计了游戏内容相关的对象

Map类

用于地图存取,将程序和map文件夹中的txt存档文件构建联系 参数中设置save用于读档存档、redo用于悔棋和撤销 一个数字表示轮次,作为参数传入,读档存档时用0, 第二个数字是存档文件的编号,每轮存档都是从0到4,分别是储存5个楼层的信息(附带勇者信息)

Lattice类

用于表示地图中每一格对象的类,存放在Lattice包中 Lattice包中直接存放Player表示勇者,以及仅改变勇者位置没有过多功能的地图块如楼梯、空地 Lattice包下属4个子包 Item包存放可拾取的物品类,触发事件或改变勇者属性 Monster包存放怪物类,可以与勇者战斗 因此需要额外实现fightWith函数 Npc包存放有额外功能的人物,如老人和商人,可以与勇者对话,产生弹框进一步选择 因此需要存放更多数据,给弹框提供信息 Roadblock包存放会阻碍勇者移动的物块 因此大部分时候需要调用勇者的moveCancle函数,取消移动

Lattice需要继承的函数: affectWith用于在勇者即将走上来的时候与勇者发生交互 getCode返回这一对象在txt存档中对应的代码 getGraphic返回对象相应图片的地址 getAudio返回对象所发声音的地址(如果有)

以上函数需要被将被实例化存入游戏状态的对象继承,而它们的一些父类、Lattice的一些子类不需要 比如蝙蝠BatSmall和大蝙蝠BatLarge的父类Bat就无须继承getCode,getGraphic,getAudio 而需要重写fightWith函数供子类继承 而Bat的父类Monster还需要实现fightEnd函数,用于给所有Monster的子类在战斗结束时运行

MotaGame类

这是综合完整数据的类,具有私有变量map(Lattice的三维数组)和player 除了get类方法外,唯一的方法是move,以Game(GUI包中的类)和int[]为参数 实现游戏数据和GUI界面的信息流通 运作流程: 1 在通过传入的数组改变勇者位置,同时保存旧位置 2 如果勇者原本的位置不是楼梯,就变成空地 3 使新位置的对象与勇者交互,根据对象的不同,可能阻止勇者移动(恢复成旧位置)、进行自动存档(如果这一步有具体意义的话,如果是撞墙就不会执行,自动存档用于撤销和取消撤销) 以上识别内化于affectWith函数的多态性,无须由move函数判断 4 调用Game类实体对象game的public函数,完成UI界面更新

GUI包

有StartMenu和Game两个类,分别是初始菜单和游戏主体的窗口

StartMenu类

主要是提供两种进入游戏的途径————直接开始和存档进入 StartMenu通过Game类的showWindow方法打开窗口,并根据按钮绑定不同的初始化步骤

Game类

是游戏窗口,分为三大块 左上为游戏区,由底层StackPane作为背景,是13*13块空地组成的图片;上层StackPane显示游戏,根据当前map储存的内容改变图片 游戏区还添加了数块visible属性为false的窗口,作为弹窗(用于怪物手册、帮助手册、提示弹窗等),需要调用时setVisible(true),使用完再设为false 右上角是面板区,上半部分是人物属性,由HBox、VBox结合组成;下方是日志区,以一定速度显示不便在弹窗中显示的信息并暂时保留,便于查看(还展现了回合制战斗过程) 下方是按钮区,存放多个按钮供使用

为增加代码可读性,将各个窗口的fx设定写在对应的xxxInit方法中。因为只需要调用一次,保存在文件的最后方 而使用则根据窗口类型的不同设置setXxx或updateXxx等,根据需要把可见性设为最严格

值得一说的基本要求

这一块对照PJ2的基本要求简述思路

重构代码

代码经过两次重构 第一次参照lab8,基本完成了面向对象设计

但是对javafx的不熟悉,摸着石头过河的过程搅乱了构架思路 每次需要添加新功能的时候都非常困难,需要先理清自己之前的思路 于是有了重构第二次 重新规划了各类文件的目录结构、类的命名、fx类静态变量的选择等

面向对象设计

1、类名、类的作用及意义 在构架设计部分已经基本说到了 补充一些细节的命名规则 第一次重构时,命名方式很朴素、如绿史莱姆是GreenSlime,但是文件排列通常根据字母顺序,这样命名并不便于检索,各种史莱姆相隔甚远 第二次重构时,把主体写在前面,修饰写在后面,这样SlimeGreen和SlimeRed能够排列在一起 所有的图片也用统一方式命名,实现getGraphic方法时更容易对照2 2、类的父类、子类和这么设计的意义 主要体现在Lattice类中,架构设计部分提到一部分,其余基本类似 3、类的构造函数、可见域和可见方法 如各种怪物 SlimeGreen的构造函数不需要参数,而是调用Slime的构造函数并设定好参数,便于调用 为了在Map中生成地图时调用,可见域设为default(GUI包中则不需要直接调用它们) 4、类的私有域及其作用 以Game类为例,大部分部件都设为private,通过相应方法进行更改 虽然自己写的小游戏不太有安全性考量,但也便于修改维护————只要更改对应方法就好了 5、类的私有方法 主要是提取重复代码 如Game类中getText和getImageView的方法,可以大段的实例化和setStyle而得到想要的文本和图片对象

玩家移动(主要是鼠标控制)

鼠标控制的部分,使用setOnMouseClicked和event.getX、event.getY,得到鼠标点击的坐标 实现getClickedPosition将像素坐标转化为地图格子坐标 实现moveWhenClicked方法,检测所点击格子和勇者的相对位置,如果在四周则调用走路的方法

拓展功能

音效、好看的界面、完善的商人和等级系统

音效

拾取物品:宝石、血瓶、钥匙 走路:开门、撞墙、走路 战斗:史莱姆、蝙蝠、骷髅、石头人 勇者:购买、升级、死亡 以上都设置了对应音效

菜单和游戏各自设置了背景音乐

好看的界面

日志区更新

说说代码难度最高的部分吧……其实反而对美观的影响不大 日志区的文字刷新,在第一次重构时的效果是一大段文字直接更新 (比如打一个怪,产生了25行记录,而日志的范围是20行,就直接显示后20行) 第二次重构时希望做成一次次更新

我尝试在怪物的战斗方法中更改Game类中对应日志内容的Text,然而依然是大段文字直接显示的效果 经过研究发现,javafx的更新机制是单线程的 流程如下: 事件处理-执行代码-更新界面 也就是说,在代码执行过程中多次修改,也要等到执行完毕后统一更新界面 另外,单线程的限制导致如何代码需要长时间执行,窗口会进入无响应状态

这时就需要用到javafx的多线程更新机制 创建service类,将在循环中更新它的message属性 (日志内容新增n行则更新n次) 将Text的textProperty与service的message属性绑定,message更新时Text内容也能更新

达到的效果:多线程使得游戏逻辑和日志区的刷新可以并行不悖地同时进行 日志区的文字按照设定的时间间隔稳步更新,对于过长的内容,玩家也不必等待更新可以继续游戏

属性图标

属性区的属性图标都是一个个码像素自己画出来的,虽然丑,但挺有成就感

完善的商人和等级系统

商人系统

商人处可以购买攻击力、血量和钥匙,每多买一次价格翻倍 由于游戏中的钥匙数量经过设定,不可溢出太多,钥匙购买设置为抽奖 50%抽到黄钥匙,35%抽到蓝钥匙,15%抽到红钥匙,减少对买钥匙的依赖,促使玩家思考选择如何利用有限资源

还可以进行金钱赌博,金额为1-30随机,一半概率获胜,赚得同等于价格的金钱,一半概率赔光 虽然数学期望看起来是0,但由于本金不会太多,通常难以承受波动的风险,勇者连赌常把钱赔光 以此告诉大家,赌博是充满祸水的深渊,远离赌博

等级系统

所需经验为100*(1.6^(level-1)),即每次增长为1.6倍 升级效果是血量上限+100,攻防+2,恢复上限50%的生命 (不恢复所有生命是为了更好保留资源有限的感觉、增强最大限度分配资源的空间)