m1594730237/JavaFinalProject

★ 0Forks 0JavaGitHub ↗Compare

README

迭代2(2018.12.18)

版本说明

  • 加入了必杀技和必杀特效

  • 同比例降低双方血量,加快战斗进程

  • 修正了一些bug

效果展示

必杀技随机触发,攻击速度越高越容易触发

必杀技触发效果:
必杀技的子弹效果:
必杀技实际效果:

恢复能力和杀伤能力翻倍

实际gif展示:

迭代1(2018.12.14)

关键词

  • JavaFx
  • Maven
  • 多线程
  • 过程记录和过程复现
  • 异常处理
  • 注解
  • 战斗

效果展示

1倍速度

20倍速度

设计优势

美观,卡通!

以下仅代表个人审美

  • 整个战场依托卡通的丛林背景,这也与葫芦娃的战斗场景相符
  • 背景上覆盖了半透明小方格,显示大家的相对位置而且不会覆盖背景
  • 卡通的正面角色,卡通的反面角色,卡通的墓碑,使得对正面角色更为悦爱,使得对反面角色无以相憎,下面展示几个卡通角色的放大图:
大娃
蛇精
蝎子精
墓碑
- 最后自吹一下,其实按钮上的图标也是精挑细选过的

摆脱近战,远程攻击!

近战攻击其实存在一些问题:

  • 葫芦娃和妖怪们要彼此寻路找到对方,所以可能战斗过程漫长(如果大家彼此躲避的话)
  • 随机性差,如果是子弹远程攻击,那么小葫芦们就可能自动的躲避一些攻击,但是近战就很难做到了
  • 如果加上血量可能比较“僵硬”(设想一下小葫芦和妖怪近战血拼,界面上的效果大概是两个人贴在一起,然后一个人的血量条慢慢空了,另一个还余下一点。。。这个过程让人费解)
  • 攻击过程难以显示(怎么显示攻击过程,难道伸出一把剑?可能效果会很差。。。)

因此将战斗变为了基于子弹的远程攻击:

飞行中的子弹

不是随机死亡,而是记录血量!

如果小葫芦和妖怪们随机死亡,尽管这其中可能有很复杂的逻辑,比如你可以计算攻击力的差值和避闪值的差,带入一个很复杂的函数中,之后判断谁死亡,然后那个人变成了墓碑,但是这个复杂的计算过程没法让观众们看到,所以在观众眼里是:哦,那两个人碰在一起了,然后一个不知道怎么就死了❓❓❓❓❓❓

所以使用了记录血量的方式来进行战斗过程的展示:

  • 小葫芦和妖怪们发射子弹
  • 如果被对方的子弹击中,那么如果没能触发避闪或吸收伤害,那么扣除生命值(掉血)
  • 血槽为空则死亡,变成墓碑

这样在观众眼里的效果是:真可怜,被打了七八次最后没顶住😭😭😭😭😭😭。

血量条的显示效果如下:

血量条

绿色的部分为剩余生命,红色的部分为已经丢失的生命

角色属性多样化,随机性强!

想想如果角色属性单一会怎么样,比如只有攻击、生命:

  • 大家一起动,一起攻击,一起受到伤害(感觉像在跳广场舞💃?)
  • 不同的小葫芦之间也只有攻击生命的差别,可是他们7个外加老爷爷也是各有所长,没法充分的展现出来

所以可以丰富一下属性值,以期望达到丰富多彩的战斗效果(虽然每增加一条属性都要额外花不少时间但我觉得还蛮值得的😀)

角色的属性为:

  • 生命值:万物之源,有生命即生,无生命即亡
  • 剩余生命:万物之源,同上
  • 攻击:万恶之源,没有攻击就没有伤害!(没有攻击就没有葫芦娃战斗,没有葫芦娃战斗就没有Java大作业😂😂)
  • 恢复:生命之源,子弹击中己方恢复它的生命(救我一下,我还能打!)
  • 移动速度:灵动之源,移动速度越高,那么他将移动的更加的快,使得所有的角色们不是整齐的运动,而是不同时随机的跑(移动速度快有什么用呢?因为我设计了子弹躲避机制,如果你比子弹快,那么子弹没法击中你)
  • 攻击速度:灵力之源,攻击速度越高,他的攻击频率就越高,使得所有的角色们不是整齐的运动,而是不同时随机的攻击(攻击速度和攻击力有什么区别呢?实际上高攻击速度就相当于将一颗攻击力很高的子弹拆分成一系列攻击力较低的子弹,避免“The Best Or Nothing”)
  • 步长:神行之源,一次移动的移动范围(步长的作用在于,如果你被一团墓碑团团围住,步长越大越容易跳出)
  • 避闪率:逍逸之源,每次受到攻击时将判断是否躲避,躲避值越高躲避的概率就越高!
  • 吸收率:福泽之源,如果躲避失败,可以触发吸收伤害,吸收率越高吸收伤害的概率就越高!

葫芦娃的属性和各自特点关联强(每个葫芦一台戏)!

设计丰富属性的另一个左右就是显示不同角色的差异性,尤其是正义阵营:

特点 属性特点 关联性说明
红娃 变大 步长极大,攻击、生命较高 变大后一步当然会变长而且攻击生命也有所提高
橙娃 千里眼,顺风耳 避闪较高 千里眼顺风耳,那么子弹还没有到达之时他就有所预判,自然避闪高
黄娃 钢筋铁骨 生命极高 显然
绿娃 喷火 攻击力极高 火焰总是给人吞噬一切的感觉🔥
青娃 喷水 攻击速度极高 怒涛总是给人绵绵不绝的感觉💧
蓝娃 隐身,聪明机灵 移动速度极高 因为蓝娃能隐身,他可以在你看不见的时候移动,所以移动速度高
紫娃 宝葫芦吸万物 吸收率极高 显然
爷爷 葫芦娃鼓励师 恢复值极高 老爷爷照顾葫芦娃、凝聚葫芦娃、鼓励葫芦娃

内部实现可修改性强!

所有关于界面和属性的实现都不是“写死”的,这就便于了扩展和修改,例如:

  • 战场背景将自适应画布大小(居中且同比例缩放),所以可以自由调节画布大小
  • 战场方格自适应战场背景(整体居中且每个格子大小同比例缩放),所以可以自由调节画布大小和战场方格数
  • 步长、速度、攻击速度的逻辑也不是写死的,可以根据不同的值来动态变化,所以可以自由调节参数

精致的游戏平衡!

因为属性太多了,而且很多都带有随机性(比如避闪率,吸收率等),所以想要游戏平衡很难,而且更令平衡性变得困难的是,仅通过几次尝试很难得知游戏是不是平衡了,最好的办法是多次尝试。

好在程序员的智慧是无穷的,于是我将每个小葫芦的延迟调至最低,重复1000次实验(1000次实验耗费时间约1200s),为了进一步加快进度:

  • 如果一方累计胜利次数超过550次,那么立即终止
  • 可以多线程测试:保持8个线程一起测试
  • 为了进一步加快测试速度,测试过程中放弃GUI显示

经过上述优化,重复1000次实验约耗费700s

最终经过不懈地测试,目前1000次实验小葫芦胜利531次,胜率53.1%,达到了平衡性。

智能角色!

每个角色都具有智能躲避己方、墓碑的能力,并且可以在两格内检测子弹并进行一定的躲避(前提是到达他行动的时刻),这也使得战斗更加精彩,不会出现小葫芦主动扑向子弹英勇就义的“悲壮”场景

结尾小彩蛋

当一方获胜后,等待一秒后会有如下彩蛋:

以此告知观众最后的结果

类间关系

UML图

下图为主要类之间的UML类图

重点类的说明

1. Being类

Being类是所有方格中上可显示的一切物体和角色的父类,包括卡通角色、子弹、墓碑等,它掌握着对象所具有的公共属性:

它具有一系列等待被Override的方法,例如:

如果未加重定义而直接使用将assert fail

2. CartoonCharacter类

CartoonCharacter类的所有数据成员都继承自Being,但它重定义了Being中的方法,也即认为所有的卡通角色的move()、attack()、hurt()、isDead()、isOver()语义均相同,例如hurt()的实现:

这里所有的卡通角色都具有同样的hurt操作,而不对任何对象进行区别处理,这样大大简化编码难度

3. Evil类、Justice类

作为卡通角色的分类标准,正义阵营的卡通角色继承自Justice类,邪恶阵营的卡通角色继承自Evil类,这样可以直接使用instanceof来判断:

由此进一步简化了编码

4. Bullet类

Bullet继承自Being,但他与其它的Cartoon Character有着重要区别:它自己要额外拥有一个Being类对象,表示子弹的发出者,这样做的好处是:

  • 便于判断究竟是何人发出的子弹,以此确定子弹自身的移动方向,攻击目标
  • 在击中对方后,将Being传给他,有对方进行伤害判断,简化了子弹的工作,也使得类间条理更为清晰

内部实现

“自适应”的实现

1. 背景图片自适应画布

背景图片自适应画布是指背景图片根据画布的大小进行伸缩,也就是同比例放大缩小,并且居中放置,而不是直接设置为画布的大小,实现思想如下:

  • 求出背景图片的长宽与画布长宽的比值的最大值max
  • 背景图片的长宽同比例除以max倍
  • 背景图片的起始位置为(Canvas.location - backgroundImage.location/max)/2(这里只是示意,不是java代码)
2. 战场方格自适应背景

战场方格自适应的效果和背景图片自适应的效果近似,只是除了位置居中,每个方格尺寸也会发生变化,实现思想如下:

  • 计算每个正方形方格的边长latticeSize:
    • 获得背景图片width和height
    • length = min(width/横向方格数, height/纵向方格数)
  • 计算边界冗余,即如果方格无法铺满整个背景时的左右上下预留空间widthRedundancy和heightRedundancy
  • 双层for循环绘制方格,第(row, col)个方格的位置是:
    • 横坐标 = widthRedundancy + backgroundImage.getX() + col * latticeSize
    • 纵坐标 = heightRedundancy + backgroundImage.getY() + row * latticeSize

多线程机制

讲多线程的实现机制之前,想先讲讲“心路”

实际上从操作系统的学习开始,就感到多线程非常的恐怖,尤其是除了bug相当难以定位,回想当年做OS Lab的时候,自己实现一个具有多线程的OS,当时进程调度加锁上出了问题,导致发生嵌套中断,在gdb下调试了整整3天。到了Java大实验,一开始对于多线程也是一头雾水,以为不管哪里加了synchronized就万事大吉,后来发先还是bug频发,又加上本身程序的执行中有大量的随机数的使用,这进一步加大了调试的复杂性,所以在刚上手多线程的时候感到举步维艰,调试起来也是毫无头绪,不过写的多了自然也就熟悉了,而且也掌握了一定的调试方法(有的时候System.out.println()真的很好用),调试也加快了,而且对于多线程尤其是Java中的多线程机制的理解也进一步深化了,最主要的是,大实验消除了我对于多线程的恐惧(所以很感谢Java课和大实验)。

整个葫芦娃世界中有如下线程:

  • GUI交互线程
  • 战场主循环
  • 卡通角色循环
  • 子弹循环

下面我将详细讲述

1. 为什么将GUI与战场主循环分裂到不同的线程

在没学多线程之前,有一个问题困扰了我很久:怎么能点击一次按钮让画面刷新好几帧?

后来进过Debug单步追踪发现,当点击按钮后,实际上并不能立即显示,而是要等按钮交互函数返回后才显示,所以这就引出了战场主循环线程的作用

战场主循环线程的逻辑很简单:

//创建线程
while(true) {
    //写文件记录当前信息
    //显示当前战场情况
    if(战斗结束) {
   		//显示战斗结果
    }
}

//善后工作,如关闭文件流等

主循环最核心的作用就是显示画面,写文件,这样才可以实现点击一次按钮后不停地显示新的内容

2. 为什么加入战场主循环线程

实际上没有战场主循环线程也不会影响正确性(不过实现效果嘛。。。😱),这种情况下实现方法如下:

  • 每个卡通角色运动时显示画面
  • 每个卡通角色运动时写入文件

这就导致一系列问题(下面的问题都是我踩过的坑😥😥😥):

  • 卡通角色有一个时刻可能会集中式的一起活动(称为峰值时刻),这样导致屏幕刷新过快,产生闪烁,效果不好
  • 并不一定每次移动都要显示,因为人眼的识别能力是有限的,尤其是子弹这种移动极其迅速的,频繁地刷新将浪费宝贵的计算资源
  • 写入文件的时候如果不写入当前时刻,那么复现的时候就没法严格按照上一次的执行来展现,但是写入当前时刻这会造成文件过大,处理过于复杂

所以加入战场主循环线程

复现战斗过程机制

有一种保存方法是,记录每个角色每时刻的移动和攻击情况,但这样做的问题如下:

  • 多线程下可能无法精准复现
  • 处理难度很高

所以考虑可以保存每一帧的图画,但是如果保存成一系列.jpg那么时间开销和空间开销都太大了,所以可以考虑保存每一帧中战场方格上的生物信息

战场主循环在每次刷新屏幕时都写入如下信息:

整个战场的每个方格的信息,包括:对象标识、对象信息

为了简化处理的复杂度和写入文件的字符串长度,对象信息只记录血量,因为对象的其它信息是不会变化的

这样每个战场方格的存储最需要至多6个字符(对象标识1个字符,对象信息至多5个,但通常只有4个)

核心的代码为如下两个函数:

/*战场方格List转化为可保存的String,用于保存战场信息*/
private String battleFieldLatticeListToString() {
    StringBuilder resultString = new StringBuilder();
    for(int i = 0; i < battleFieldLatticeList.size(); i++) {
        Being being = battleFieldLatticeList.get(i).getBeing();
        resultString.append(being.getLabel()).append(being.getRemainedHp())
                                             .append(System.lineSeparator());
    }
    return resultString.toString();
}
/*String转化为战场方格和其中的对象信息,用于复现战斗过程*/
private BattleFieldLattice stringToBattleFieldLattice(String string) {
    String label = string.substring(0, 1);
    int hp = Integer.parseInt(string.substring(1));
    if (label.equals("*")) {
        return new BattleFieldLattice(new Bullet(new Being()));
    }
    else if (label.equals("R")) {
        Being being = new Red();
        being.setRemainedHp(hp);
        return new BattleFieldLattice(being);
    }
    else if (……){
        //……
    }
    //一系列if-else判断对象类型
}

在此方法下,运行1分钟大概需要生成0.5MB的文件

致谢

    首先感谢曹老师和余老师的精彩讲解,两位老师的讲解非常生动而且内容充实,使得我对于面向对象和Java语言的理解更进一步深入了,并且也使得我了解到了Java中的许多区别于C++的特性,最主要的,也使得我能第一次系统化的接触面向对象思想和Java语言。

    其次感谢Java课程和大作业,Java课程的学习是辛苦的,但能体会到一种新的语言之美却是获益匪浅的;大作业的阶段性完成是艰辛的,但能看到葫芦娃的跳动却是充满趣味的。Java课程和大作业让我真正应用了面向对象思想和Java语言,并且也消除了我之前对于多线程等的很多恐惧。由衷地感到这一趟学习之旅不虚此行。

    最后感谢辛勤付出的助教学长学姐们,认真阅读我们的.md和.java,维护着java-2018f-homework上的存在各种各样问题的pull request。

Issues