# magic-game **Repository Path**: yan-guiyuan/magic-game ## Basic Information - **Project Name**: magic-game - **Description**: 一个魔法小游戏,玩家通过选择各种不同的魔法特性创造出属于自己的魔法,并使用魔法进行战斗。 - **Primary Language**: Java - **License**: GPL-3.0 - **Default Branch**: develop - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-07-26 - **Last Updated**: 2023-06-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # magic-game #### 介绍 一个双人pvp的魔法小游戏,玩家通过选择各种不同的魔法特性创造出属于自己的魔法,并使用魔法进行战斗。 ## 1.项目结构 项目主要分为抽象层和具体实现层,抽象层提供框架和各种操作方法。具体实现层调用框架层完成游戏的各种功能和游戏逻辑。 项目的整体结构是树形结构 ### 1.1.抽象层(框架) 框架的结构图: ![img.png](img.png) - GameApplication 游戏程序的入口和启动类,并负责初始化配置和场景管理 - Scene 场景,主要的游戏内容展示容器 - Canvas 画布,用于渲染游戏中的画面,包括实体和一些图像数据,所以也是实体的容器 - Entity 游戏中的主要对象,每一个游戏对象都是一个实体,例如玩家,怪物,阻挡物等都是实体 - EntityProxy 实体的代理,用于实现一些比较复杂的实体功能和操作 - NavigationPath 用于提供实体的导航和寻路功能 - EntityThread 实体线程 - EntityAction 实体事件,对应于一些场景的监听事件,每一个实体事件在场景渲染完毕都将交由场景的SceneEventListener托管 - EntityBuilder 实体构建者,提供了较多api用于构建各种各样地实体,比如玩家和怪物实体等 - Hook 实体的生命周期钩子 - Drawing 提供给调用者绘制一些除实体外的其它图像 - SceneEventListener 场景的事件监听,事件监听都绑定在场景中,场景切换,对应的事件监听也会发送改变 - SceneMusic 主要用于处理和控制音频数据的播放 - SceneHook 场景的生命周期钩子,提供给调用者执在场景的不同阶段执行对应的方法 - DB 游戏的公共临时数据库,用于场景间的数据交换并存储一些公共的数据,同时也负责各种配置文件的读取 - Assert 用于读取一些需要用到的游戏资源,比如图片,音频,或者存储地图数据的json文件,对资源进行统一读取后加入内存缓存中,进行统一管理 - MGF 提供一些通用的工具类方法,例如碰撞的检测,线程的控制,图像的合成和处理 - Broadcaster 广播者,主要用于信号和方法的连接和控制,其它程序发送信号给广播,广播接收到信号调用事先连接好的信号对应的方法 ### 1.2.具体实现层(游戏的实际逻辑代码) #### 1.2.1.场景设计 - 菜单页面 - 战斗准备页面:进行词条组合,构建魔法 - 战斗地图:人物进行战斗的地图,P1和P2两名可供控制的角色,一些怪物,还有大量的阻挡和障碍物 - 游戏结束页面 --- ## 2.游戏介绍 ### 2.1.游戏实体 #### 2.1.1人物(主角) ##### 2.1.1.2.人物属性 - 生命值 - 魔法值 - 速度 实际程序中人物属性比这多,但是大多玩家是不可见的 ##### 2.1.1.2.人物功能 - 上下左右移动 - 能释放能量球(施法) - 拥有三个法术槽能够进行施法 - 显示生命值和魔法值(最好有血条),速度不显示 - 生命值减少时显示减少数字,增加时亦然。 #### 2.1.2.怪物或动物 和人物是差不多的,只不过能够自动进行某些操作,需要AI算法支持,不清楚要不要加 #### 2.1.3.障碍物 阻挡人物和怪物移动,以及阻碍魔法部分效果,不可移动,无生命值等属性但仍是实体,会触发作用于实体的魔法。 --- #### 2.1.4.魔法 玩家的主要攻击手段,一开始是默认白板属性。 ##### 2.1.4.1.魔法属性 - **白板** 默认拥有词条,能在角色身前生成一个能量球,触碰实体则执行相关(事件函数)效果,之后消失。(因为默认是无效果的,所以默认就是触碰实体后消失) - **弹射** 能量球生成时会沿角色朝向,也就是魔法球本身朝向做一定速度的直线运动,直到触碰到实体。 - **爆炸** 与实体接触后爆炸,瞬间扩大与实体接触范围 - **伤害** 与生物类实体接触后扣除其一定血量 - **治愈** 与生物类实体接触后恢复一定血量 - **自我** 使得能量球生成在自己体内,即能量球一召唤,自己便是第一触发者。 - **护盾** 接触到的生物类实体将会获得一个持续一定时间的护盾,抵消部分伤害 - **转移** 一定时效内使得接触到的实体拥有能量球的特性 - **破坏** 破坏接触到的非生物类实体 - **缓慢** 使接触到的生物类实体移动速度减慢 ----- 等等(后面可以进行增加,程序设计时强调可扩展性)------- ### 2.2.主要玩法 玩家通过不同的魔法属性组合,创造出具有不同效果的魔法,然后通过魔法进行战斗。 组合示例: - 弹射+伤害:传统的魔法伤害 - 转移+伤害:陷阱 - 转移+伤害+爆炸:地雷 - 自我+治愈:传统意义的治疗术 - 自我+伤害:自残法术 ### 2.3.游戏定位 2D游戏,游戏视角为从上面斜向下投影,只有上下左右(东西南北,区别于左右跳跃和下蹲)4个方向,没有跳跃。 双人PVP,因为这个目前来说最容器实现 可以的话,最好做成单人游戏,因为双人PVP有局限性,那就是单人的话玩不了,不过单人游戏需要部分野怪,野怪需要一定的AI 后面也可以利用websocket制作成双人联机模式甚至多人联机. --- ## 3.技术整理 ### 3.1.技术栈 - javaSE 进行游戏业务逻辑的开发和算法的实现 - JavaGUI(swing) 实现游戏图像的渲染和绘制,监听键盘和鼠标事件 - fastjson 实现地图数据的序列化 - animated-gif-lib 第三方包,主要是对gif图像的处理 - Java多线程 ### 3.2.技术难题及解决 #### 1.如何实现碰撞效果,障碍物的阻挡功能如何实现? ​ Java中的Rectangele提供了一个方法intersects(),可以判断两个矩形是否重合。 ​ 借助这个功能我们可以实现碰撞的检测。基本方案就是给实体挂载一个矩形,在游戏中我们叫它为HBOX(碰撞箱),HBOX的中心位置和实体中心保持一致,宽高可以设置,但如何不设置宽高,默认就是实体图像的宽高。同样地,实体的碰撞体积也可以不局限于矩形,也可以用椭圆或者圆代替,游戏碰撞检测的实质就是用抽象的数据模型模拟游戏实体的实际形状和体积,是否碰撞就相当于两个图像是否相交。但考虑到我们的游戏大多都是矩形的图块,并且矩形的检测最简单,所以以矩形模拟实体的碰撞体积。 ​ 以下是部分碰撞检测的函数: ```java //判断当前实体是否与特定类型的其它实体发生碰撞 public boolean isCollision(String entityType) //检测当前实体碰撞是否发生,并返回与之碰撞的对象(未碰撞返回null) public Entity collision() //检测实体e是否与特定的边界发生碰撞 public static Boolean isBorderCollision(Entity e,int w,int h) ``` ​ 既然已经实现了碰撞的检测,那障碍物的阻挡如何实现呢? ​ 障碍物的阻挡主要是将实体的每一次移动细化到每一个像素点,每一个像素点的变化后判断当前实体是否发生碰撞,如何发生了碰撞则撤销这一变化并终止整个的移动操作。 ​ 以下是人物的移动阻挡代码实现: ```java if (status == Status.WALK_EAST || status == Status.WALK_WEST || status == Status.WALK_SOUTH || status == Status.WALK_NORTH) { Point2D vec = moveDir.getVec(); for (int i = 0; i < moveSpeed; i++) { e.translate(vec.getX(), vec.getY());//移动一像素 if (e.isCollision("StaticObstacle")||e.isCollision("Obstacle") || MGF.isBorderCollision(e, DB.getInt("map.width"), DB.getInt("map.height"))) {//检测到碰撞 e.translate(-vec.getX(), -vec.getY());//反方向移动亿像素,相当于撤销本次移动 break;//终止整个移动过程 } } } ``` #### 2.魔法词条的组合时,如何动态地实现组合效果,如何保障各个词条之间不冲突? 多词条的组合效果实现,主要是通过遍历执行各个词条对象的对应方法实现的。而为了保障各个词条之间有序进行,不发生冲突,则需要设定好每个词条的优先级,即谁先执行,谁后执行,其次是规定好每个词条的作用期,另外也要规定好每个词条的作用对象。 关于词条的作用期,主要分为三个过程:1.魔法球被创建后的初始化(init)。2.魔法球初始化至发生碰撞被销毁的整个过程(running).3.发生碰撞后。相对应的,每个魔法词条需要重写init(),running(),afterCollision()这三个方法,这三个方法分别在魔法球初始化时被调用一次,在运行或者说存在的整个过程中被循环调用,在发生碰撞销毁前被调用一次。 ​ 举个例子,假如一个魔法球的词条是【白板】【弹射】【伤害】,它们的优先级是0 ,1 ,1,(优先级的值越低越先执行),则在魔法球被创建时,会依次调用【白板】【弹射】【伤害】的init()方法,【白板】的init()方法会将魔法球的初始坐标设定为玩家朝向的正前方某个位置上,而【弹射】【伤害】,init()方法为空,不会产生任何影响;之后,魔法球进入RUNNING,运行期,会不断依照优先级调用【白板】【弹射】【伤害】的running()方法,【弹射】的running()方法每次调用会改变魔法球的坐标,使它朝正前方移动,而其它两个词条running()方法为空,最后,当魔法球发生碰撞后,会依次调用三个词条的afterCollison()方法,【伤害】的afterCollison方法会对发生碰撞的实体进行类型判断,如果是生物类实体,将执行扣血操作,而其它两个词条的afterCollison()同样为空,不会产生影响。这就是词条的整个运行过程,如果我们再加一个【爆炸】词条,那么魔法球在碰撞后会发生爆炸并且扣血,如果再加入一个【缓慢】词条,那么在扣血的同时造成缓慢效果,移动速度大减。 #### 3.怎么实现地图的序列化和持久化?怎么将地图数据通过文件的方式保存下来? ​ 最开始的地图就是通过一个类实现的,里面有大量的实体对象和Image对象,后来就想把它序列化成文件保存,这样如果想更换游戏的地图只需要替换相应的数据文件就可以了,而不需要更改程序的源代码。一开始是打算通过对象流的方式将整个地图对象序列化,但是很快发现,想要序列化,那么类必须实现Serializable接口,一些基本的数据类型还有String类可以直接序列化,但是其他的类就必须实现这个接口才行。我们自己写的Entity类还好,值需要实现这个接口就行了,但是实体类中有很多成员比如Image类就没有实现Serializable接口,所以无法序列化。这就导致了图像对象是无法通过对象流进行序列化的,那究竟怎么解决呢? ​ Json文件+Java反射。为什么会联想到使用Json文件解决这一问题呢?主要是忽然想到Mysql数据库存储的所有数据都是字符串,包括图像数据也是存储的图像的字符串编码,而Json本质也是一种字符串的数据存储方式。 ​ 基本数据类型都可以直接写在Json文件中,那图像呢?写编码显示不太合适,但是可以写图像对应的图片文件的名字,然后Java读取到它的名字后再读取对应的图片文件转化成Image对象,包括其它的不可序列化对象也一样,先通过Json配置好对象的基本信息,然后读取Json文件中的配置信息,再通过反射调用类的构造器构造相应的对象。 #### 4.多线程开发中发现出现了较高的内存开销,占满CPU,原因是什么?怎么解决? ​ 多线程占满CPU主要是因为部分线程在运行过程中存在死循环导致的,解决这个问题值需要在死循环中添加 ``` Thread.sleep(1); ``` 即可解决,原先一秒钟一个线程的死循环可能运行上百万次,直接吃满CPU,现在每秒基本只会运行1000次。 #### 5.swing提供的图像绘制功能只能在原有图像的基础上进行覆盖,旧图像会影响新的图像,怎么解决? ​ 多图层解决。将整个游戏画面的绘制分成背景层,装饰物层,实体层,实体覆盖层大概这几个不同的绘制层,依次进行绘制,绘制完背景层后会直接覆盖掉原有的图像,然后再新的背景层上一次绘制装饰物层图像,实体图像,以及能够覆盖实体的图像。 #### 6.怪物如何自动找到玩家?怪物的寻路算法怎么实现? 关于寻路实现,主要采用的是A*算法。首先通过代理怪物类,由代理类控制怪物的各种行为,比如说寻路,攻击等。然后每次寻路时对怪物当前地图进行数据抽象,建立二维数组,障碍物标记为-1,无障碍物可通过标记为0,接着通过A star算法计算导航的路径,最后对计算的路径映射到地图上的实际坐标和路线,通过实体代理类完成怪物的移动。 以下是主要实现代码: ```java public NavigationNode AStarSearch(NavigationNode start, NavigationNode end) { //把第一个开始的结点加入到Open表中 this.Open.add(start); //把出现过的结点加入到Exist表中 this.Exist.add(start); //主循环 while (Open.size() > 0) { //取优先队列顶部元素并且把这个元素从Open表中删除 NavigationNode currentNode = Open.poll(); //将这个结点加入到Close表中 Close.add(currentNode); //对当前结点进行扩展,得到一个四周结点的数组 ArrayList neighbourNode = extendCurrentNode(currentNode); //对这个结点遍历,看是否有目标结点出现 //没有出现目标结点再看是否出现过 for (NavigationNode node : neighbourNode) { //找到目标结点就返回 if (node.x == end.x && node.y == end.y) { node.initNode(currentNode,end); return node; } //没出现过的结点加入到Open表中并且设置父节点 if (!isExist(node)) { node.initNode(currentNode, end); Open.add(node); Exist.add(node); } } } //如果遍历完所有出现的结点都没有找到最终的结点,返回null return null; } ``` #### 7.如何借助swing绘制动画,或者说绘制gif动图。 首先,我们知道swing绘制图像一般都是 通过drawImage()实现的,而它接收的参数是Image,但是我们知道Image一般是接收的单一图像。而ImageIcon可以接收动图gif,ImageIcon的getImage()方法则会返回gif的当前帧图像,通过这个方法,多次调用drawImage方法则可以实现动图的展示了。 ### 3.3未来更新计划 - 选用专业游戏引擎如godot,unity对项目进行重构 - 利用java的netty框架开发游戏服务器,实现简单的多人混战。 - 添加数据库,实现账号的登录注册管理,持久化用户游戏数据 - 添加聊天、好友和组队功能,增强游戏社交性 - 丰富游戏背景故事 - 开发更多有趣的魔法词条,以供玩家进行搭配,创造出更丰富的玩法 - 开发更多地图 - 编写并开放游戏相关接口,让玩家可以自己编写词条和地图(就像Minecraft的mod),建立游戏社区.