回望整个大学的编程生涯,我们开发的所有程序几乎只能实现本地操作,不仅操作和显示都只能在本地实现,信息和数据都存放在本地。但在这续期学习过计算机网络这么课程之后,我们掌握了基础的网络知识,学习并实践了socket编程。因此,秉承着学以致用的精神,我们希望将这些网络技术运用在Java的编程开发中。
坦克大战就是我们脑子里第一个冒出来的想法。本项目使用JavaFX实现坦克大战的基本游戏部分,并通过C/S的网络模式实现坦克大战的联机功能。
同时,为了增强游戏的完整性,我们引入大厅的概念,玩家需要登录和注册进入大厅,并需要创建房间以开始游戏。通过大厅和房间来管理玩家和游戏会话,为玩家提供了更方便的联机体验。
本项目主要分成了大厅和游戏部分。
- 大厅部分:注册登陆 -> 选择房间 -> 进游戏
- 游戏部分:开始游戏 -> 游戏结束
姓名 | 工作任务 |
---|---|
Wu (组长) | 游戏部分 |
Hu | 游戏部分 |
Qiao | 大厅部分 |
Yang | 大厅部分 |
本项目使用Java语言进行开发。
- **项目管理:**本项目使用Gradle对代码结构、资源和依赖进行管理,减少了依赖文件混乱的问题。
- **代码管理:**本项目使用Github进行团队编程开发,采用多分支模式并行开发,大幅度提高了开发效率。
-
目前的代码已在GitHub上开源:https://github.com/Fucloud233/tank-war-online
-
- **代码测试:**本项目使用JUint进行单元测试,将项目核心代码和测试代码解耦,使代码结构清晰且方便测试。
- **类与对象:**用于封装数据和行为的抽象概念,管理各种实现功能所需的对象,如坦克、子弹。
- **超类与继承:**通过继承,子类可以重用超类的代码,减少重复编写代码的工作量,提高代码的可维护性和扩展性,如在通信部分中,坦克发射子弹信息体和移动信息体继承于一个父信息类。
- **接口及其实现:**接口定义了一组方法的规范,而其实现则是具体的方法实现。接口可以实现多态和解耦合,使得代码更加灵活和可扩展。如在游戏开发中,我们使用接口来定义游戏对象的行为规范,如移动、死亡等。
- **异常处理:**异常是指程序在运行过程中出现的错误,可以通过异常处理来捕获和处理异常,使程序能够正常运行。在游戏开发中,我们使用异常处理来更好地控制游戏的流程和逻辑。
- **多线程:**多线程可以提高程序的效率和响应速度。在游戏开发中,我们使用多线程来处理游戏中的各种任务和事件,如网络通信、碰撞检测、界面刷新。
- **文件存储:**在游戏中使用了使用文件存储地图信息。
- **网络编程:**在本项目中使用了网络编程来进行数据交换和通信,具体实现了如多人在线游戏和聊天等功能
- **Java图形界面:**本次开发中,大厅和游戏部分均使用Javafx来渲染,实现了交互式界面以及游戏本身。
- **Java JDBC:**我们使用Java JDBC连接远程服务器数据库来管理用户信息。
本项目使用的核心知识点主要为Java的网络编程、通过JavaFX进行GUI编程和面向对象思想的运用。
- **网路编程:**本项目使用Java的net库进行网络编程。在本项目的第一版中使用了多线程NIO的IO模式,在后续的版本迭代中改用了多路复用的IO模式,并解决了TCP连接中的粘包问题,实现了高效的服务端处理能力。
- **GUI编程:**本项目使用JavaFX开发GUI,使用Stage实现大厅部分的多窗口切换,并使用Canvas组件实现游戏画面的绘制,相比于Pane组件,实现更高效且更低消耗的画面显示。
- **通信逻辑:**本项目的游戏部分通过Json进行通信,并使用Jackson库对Json进行序列化和反序列,通过Json与对象的绑定,大幅度提高了Json序列化和反序列化的编程效率。
- **多线程:**本项目的游戏部分使用多线程同处理画面渲染、游戏控制、消息接收三个任务,保证游戏机制的正确性和性能。
- **面向对象:**本项目贯彻了OOP继承、封装和多态的思想,增强代码复用、维护性和可扩展性。
- **数据库:**本项目通过了JDBC和远程MySQL数据库来实现持久化数据存储的问题。
- 服务端的多路复用,消息收发和处理,以及游戏的实时性考虑。
- 大厅部分的多窗口切换的逻辑问题。
- 游戏机制的正确性的实现,例如玩家操控、坦克移动和碰撞等。
"坦克大战联机版"是一个Java项目,包含大厅、游戏和服务器三个部分。
项目旨在提供一个联机对战的坦克大战游戏体验。
- 大厅部分能为玩家提供一个交互性强的游戏入口。
- 游戏部分为玩家提供完整的、有趣的游戏体验。
- 服务器需保证大厅和游戏部分的网络交互。
除了基本的功能性需求以外,还存在一部分非功能性需求:
- 可用性需求:游戏UI简洁明了,易于使用,游戏操作简单直观,在最简化操作机制的基础上保证游戏性。
- 可靠性需求:服务器应该具有高可靠性,能够保证大厅和游戏的正常运行和稳定性。
- 可维护性需求:游戏需要有良好的代码结构,易于阅读和修改,能够方便地进行调试和维护。
- 可扩展性需求:游戏需要具备良好的可扩展性,能够方便地添加新的游戏模式、地图、坦克、武器等内容。
大厅部分:
- 大厅是玩家进入游戏的入口和交互界面。
- 提供用户注册和登录功能,使玩家可以创建个人账户并登录游戏。
- 玩家可以选择加入已有游戏房间或创建新的游戏房间。
- 提供一个交流平台,使玩家能够与其他玩家进行聊天和准备游戏。
游戏部分:
- 游戏是项目的核心部分,玩家通过控制坦克进行对战。
- 提供地图、障碍物等游戏元素,增加游戏的多样性和挑战性。
- 玩家可以通过操作坦克移动和发射子弹与其他玩家的坦克进行对抗。
服务器部分:
- 服务器负责处理玩家之间的网络通信和游戏逻辑。
- 维护游戏房间和玩家通信,确保游戏的同步和一致性。
- 接收玩家的操作指令,并将更新广播给其他玩家,实现实时对战效果。
- 管理房间和处理游戏结束等逻辑。
语言 | Java19 |
---|---|
开发环境 | Window10 |
开发工具 | IDEA |
项目管理 | Gradle |
依赖库 | JavaFX, Jackson, JUint |
LoginWindow
登录****界面
- 用户在登录界面输入自己的账号和密码,注意不能重复登录。登陆后进入大厅页面。
- 若账号或者密码出现错误,会有相应的提示信息。
- 如果还未注册信息,可以点击注册进入注册窗口。
注册****界面
- 用户注册时,要设置自己的用户名、账号和密码。
- 注意由于用户名和账号都是唯一标识,因此均不能够重复。若重复,会有提示信息指示重新设置用户名或者账号。
- 注册成功后,自动回到登陆界面。
Client
- 当用户需要在游戏的大厅系统中创建游戏房间时,可以使用房间创建窗口(CreateRoomWindow)。这个窗口使用JavaFX库构建,并提供了一组用于设置房间属性的GUI组件。
- 在房间创建窗口中,用户可以输入房间名并选择房间容纳的玩家数量。此外,还可以通过单选按钮决定是否需要设置房间密码。如果需要密码,将显示一个密码输入框供用户输入。当用户点击"创建房间"按钮时,系统会验证输入的数据是否完整。如果房间名为空或者需要密码但未输入密码,则系统会显示警告信息要求用户提供完整的信息。验证通过后,系统将使用通信模块(Communicate)向服务器发送房间创建请求,并传递相应的参数,如房主用户名、账号、房间名、玩家数量和密码(如果需要)。
- 如果房间创建成功,系统将关闭房间创建窗口,并打开游戏等待窗口(GameWaitWindow)。在游戏等待窗口中,用户将成为房间的拥有者,并可以等待其他玩家加入房间。房间创建窗口的关闭功能也可供用户随时使用。
- 用户需要选择对应列表项中的房间,否则进行提示"您还没有选择房间"。进入房间前需要判断房间的状态:已开始游戏、人数已满则均无法进入选中房间。满足要求则可以进入房间。如果房主设置了密码,则会弹出选择房间窗口。
- 当用户需要输入游戏房间的密码时,可以使用选择房间窗口(SelectRoomWindow)进行操作。这个窗口是使用JavaFX库构建的,并提供了一个密码输入框和确认按钮,用于验证用户的房间密码。
- 在选择房间窗口中,用户将看到一个密码输入框和一个标签,标签上显示着"房间密码"的文本。密码输入框用于用户输入房间密码。此窗口还包含一个水平布局(HBox),用于将标签和密码输入框水平排列在一起。在水平布局的底部,有一个确认按钮。
- 当用户在密码输入框中输入完毕后,可以点击"确认"按钮。点击事件会触发系统将密码和房间号作为参数,使用通信模块(Communicate)将这些信息发送给服务器进行验证。服务器验证通过后,方可进入房间。
- 每次有用户登录或退出时,聊天框内会有相应记录。
- 用户可以选择给所有人发消息,也可以选择给指定的用户发消息,以方便进行群聊或私聊,但用户不能自言自语,即只给自己发消息。
WaitGameWindow
- 聊天:显示游戏房间的聊天框和聊天内容,让玩家可以进行实时的文字交流。提供发送聊天消息的输入框和发送按钮,方便玩家发送消息给其他玩家。允许玩家选择聊天对象,可以选择发送给全部人或者指定的玩家,以便进行私聊或群聊。
- 显示当前房间内的玩家列表,包括玩家的序号、昵称和状态,以便玩家了解房间内其他玩家的信息。
- 提供开始游戏/准备按钮,房主可以开始游戏,其他玩家可以准备,从而控制游戏的开始。一个人无法开始游戏,并且房间中存在未准备的用户也无法开始游戏。
- 提供退出房间按钮,玩家可以退出当前房间,返回游戏大厅或其他界面。房主若退出房间则会解散该房间。
游戏机制和游戏客户端需要分开介绍
游戏机制偏向于游戏的设计和逻辑,游戏客户端则更多的是实现结果和技术细节、
游戏机制是相对固定的和不变的,游戏客户端的实现依赖于游戏机制,并且不同的人实现也都可能有所不同,是相对多样和变化的
更合适的应该先介绍游戏是什么(游戏机制),再介绍怎么实现(游戏客户端)
坦克大战虽然是各位耳熟能详的电子游戏,但在这里我们还是详细介绍一下设定的游戏机制。下面我将分别从游戏实体、控制逻辑和判胜机制三个方面详细介绍。
- 游戏部分是整个项目的核心,我们贯彻了OOP继承、封装和多态的思想,定义了游戏的实体。
- 与此同时,为了实现联机功能,我们同样定义了游戏的信息体。
- 我们对游戏整体的机制进行了充分的考量与设计。
首先介绍游戏中有什么
游戏部分主要以下实体。
实体名称 | 属性 | 行为 |
---|---|---|
坦克 | 玩家编号、子弹数量、速度、存活 | 转向、移动、碰撞发射子弹 |
子弹 | 玩家编号、射程、初始位置、速度、存活 | 移动碰撞摧毁坦克和建筑物 |
建筑方块 | 种类、名字、脆弱性(是否能被摧毁) | 无 |
地图 | 坦克出生点、建筑方块的坐标和种类 | 无 |
- 地图方块:游戏地图包含多种方块,包括不可击碎的"石头"、子弹可以穿过的"草丛"和子弹可以击碎的"木头"。玩家可以根据地图的不同方块进行合理的走位和策略,以获取胜利。
- 子弹机制:为了游戏的平衡性,限制了子弹的最远距离。并且每个玩家在场上只能有一颗子弹存在,以避免连续不间断地发射子弹的问题。
- 玩家数量:游戏支持2、3、4人的玩家数量,玩家可以选择合适的人数进行游戏。
整体设计上,我们采用使用面向对象的思想,将**实体(Entity)**作为基类进行建模,包并通过继承和多态性实现共享和扩展功能。
再介绍这游戏怎么玩
在游戏部分玩家能够控制的操作主要为移动和发射。
坦克的移动由"WASD"键,指令逻辑分为两个部分:按下+松开。
- 按下方向键后,游戏会根据方向键设置坦克方向,
- 而松开方向键后,坦克会被设置为停止状态。
值得一提的是,为了使坦克移动更加灵敏,我们的按下逻辑是:即使已有方向键处于按下状态,再按下一个方向键,坦克会立即转向。以此,玩家可以进行更精确的移动,提高了游戏性。
游戏会在方向键的按下和松开时使用网络通信发送坦克状态,通过服务端转发给各个玩家,实现不同玩家的联机同步。
坦克的移动根据目前坦克状态来进行,若当前坦克处于移动状态,则逻辑线程会在一次循环中根据方向为坦克移动配置的距离(即坦克速度)。若坦克处于静止状态,则不作处理。
使用边界绘制方法,来检测坦克与其他元素(坦克、地图方块)判断是否碰撞。在移动前,线程会记录当前坦克状态(包括位置,方向)。若发生碰撞,则线程会恢复坦克状态,并设置坦克为静止状态。
子弹的发射由按下"J"键触发。
按下发射键时,游戏会根据当前坦克位置和方向添加一颗子弹,并将该子弹创建信息通过服务端转发给各个玩家,实现不同玩家的联机同步。
当子弹已经死亡(超过最大距离/击中物体),则移除子弹。
子弹通过边界绘制,来与其他元素(坦克、地图方块)判断是否碰撞。发生碰撞后,子弹会被设置为死亡(失效)。
若与方块发生碰撞,则如果方块是可击碎的,则设置方块死亡。若与坦克发射碰撞,则设置坦克死亡,并将该坦克死亡信息通过服务端转发给各个玩家,实现不同玩家的联机同步。
最后介绍游戏怎么赢
作为一款游戏,合理、完整、有趣的游戏机制是不可或缺的,以下是游戏机制的要点:
场和局的概念:游戏中区分了场和局的概念。一场游戏可以包含多个局,而默认局数为2。当所有局数完成后,将显示一场的积分榜,展示玩家赢得的局数。玩家可以在游戏结束后通过聊天框协商是否要再来一场,增加游戏的灵活性和玩家的成就感。
- 项目中游戏的客户端会同时运行三个进程,分别用于游戏逻辑处理、游戏绘制、网络通信。
- 为了提高游戏的功能性,我们设计了侧边状态栏,以显示游戏状态,提高游戏可玩性。
客户端首先完成:初始化连接、初始化显示、初始化游戏控制后,将负责以下3部分的任务,每个任务都有一个独立的线程进行控制。
介绍通过JavaFX如何满足前面的游戏机制(表层的、可见的)
效果见3.2.2.3附图
当游戏客户端启动并初始化显示时,侧边状态栏就会被添加到页面中。
侧边状态栏元素有:
- 坦克样式:用于区分玩家的自己的坦克样式
- 游戏状态:包括局数、玩家
- 计分板:即玩家胜场数
这些元素会根据游戏状态的改变而更新显示。
显示胜者(按得分顺序),平局显示
介绍游戏客户端背后的技术细节(内在的,不可见的)
- 绘制线程:以60Hz的刷新率绘制游戏画面(坦克、子弹和方块)
- 逻辑线程:负责处理包括坦克移动、碰撞检测和子弹发射等游戏逻辑
- 接收线程:网络通信任务负责接收从服务器发送的消息,并根据消息类型进行相应的处理
- 消息类型会在服务端的通信协议中重点介绍
服务端是本项目的最核心最重要的部分,它联系了所有玩家的客户端,起到了重要状态的记录和消息转发功能。下面我将从存储单元、通信协议、处理逻辑和多路复用三个方面对本项目的服务端进行详细的介绍。
网络编程中我们处理的是socket,但是为了让这些抽象的socket具有意义,我们定义了两类实体以方便我们处理大厅、房间和游戏的业务逻辑。
在本项目中,玩家是最基本的组成单元。在实际编码过程中,我们以对应的socket为key,玩家对象为value建立HashMap,来对他们进行绑定,以更快地通过socket对象查找到玩家对象。
玩家对象包含以下属性:
- 基本属性(昵称、账号、密码)
- 玩家状态
- 所在房间:记录玩家当前所在的房间对象。
基本属性中的昵称和账号是不可重复的,他们用于唯一标识一个玩家。同时,这些基本属性会持久化地保存在数据库中,用于玩家登陆和注册的校验。此外,玩家登录后,昵称和账号会用作于提示显示,比如积分榜的显示和房号的生成。
玩家状态是用于标记玩家所在的位置,它通过枚举类型存储,包括以下5种状态。服务端会根据玩家的当前状态对其发送的消息进行使用不同的处理函数进行处理。通过状态分类处理的方式,可以减少不同业务逻辑处理的耦合度,减少错误的发生率且提高处理效率。
Null
未登录状态Lobby
处于大厅Ready
\NoReady
处于房间(是否准备)Playing
处于游戏中
玩家是本项目中最基本的单元,多名玩家就组成了房间。以房间为单位,服务端就能很清晰地划分需要处理的玩家范围,更高效地执行消息转发的功能。房间包含以下属性:
- 基本属性(房间ID、房间名,最大玩家数量,密码(可选))
- 玩家集合:记录加入房间的玩家对象
- 房间状态:记录房间是否开始游戏
- 游戏对象:记录房间当前所开始游戏的信息
基本属性中的房间ID是由房主账号决定。由于玩家账号是唯一的,所以房间ID也是唯一的。其中最大玩家数量会限制玩家进入的数量,设置范围为2-4人。
房间对象中不仅包含了玩家对象,还包含了玩家对应的socket对象。我们仍使用HashMap来存储socket和玩家对象的映射关系。但是为了维护玩家进入房间的顺序,也就是为房间内的每个玩家分配一个编号,我们额外使用了Vector来记录这些用户进入房间的顺序。显然,Vector中第一个玩家则是该房间的房主
房间与游戏的粒度是相同的,二者本可以融合在一起,但房间和游戏处理的信息截然不同,因此我定义游戏对象,储存游戏中的一些关键信息,以扩展房间对象的功能。它包含以下属性
- 所有玩家、剩余玩家:记录存活玩家数量,以判断此轮游戏是否结束
- 总轮数、当前轮数:记录轮数,以判断游戏是否结束
- 玩家分数:记录每个玩家当前的得分情况
- 地图编号:记录当前使用的地图编号
在游戏中,由于前面维护了玩家的顺序信息,此时玩家对象的属性对游戏处理没有帮助,因此只需要记录socket信息。其中“剩余玩家”存储当前存活的玩家编号,“所有玩家”则用于在新一轮游戏中刷新“剩余玩家”。
为了保证客户端和服务端能够正常通信,我们确立了基本的通信协议。值得注意的是,由于大厅部分和游戏部分是由不同的人员开发,因此通信协议有所不同。
我们的消息由两部分组成,定长头部和消息内容。需要使用额外的定长头部,主要是解决TCP连接所产生的的粘包问题。
定长头部我们使用长度为3的十进制数字( 为编码方便,头部仍使用ASCII),因此我们的消息体长度最高能达到999个字符,已经能覆盖我们日常的通信需求。
大厅部分是指包含登陆注册、大厅和房间在内的所有的功能。大厅部分的通信协议是以信息流的方式定义,消息中所包含的信息按顺序进行拼接,并使用符号|
进行分割。
其中,消息的类型位于首位,服务端和客户端在接收到消息之后,会对消息类型进行识别,然后按照需要读取后面的信息,再进行不同的业务处理。
Select room|roomId
游戏部分的通信主要包括玩家对坦克下发的指令信息,以及一些与游戏相关的重要状态信息。与大厅部分不同的是,游戏部分使用了Json作为游戏的通信格式。
在该部分的通信中,我们定义了一系列与Message有关的消息对象,其包括最基础的两个属性:消息类型和发送者ID。 并为了满足更多的通信需求,我们还以此继承了更多的消息对象,比如说移动消息、发射(子弹)消息等。下面是一个简单的移动消息的例子
{"type": "Move", "id": 0, "x": 25, "y": 50, "dir": "LEFT"}
在项目中,我们使用Jackson库对Json进行解析和使用。通过定义前面的消息对象我,我们实现Json绑定就能很迅速和便捷的实现Json和对象之间的转换。这也就是我们使用Json来实现消息通信的重要原因之一。
消息类型 | 对象名 | 说明 | 包含属性 |
---|---|---|---|
初始化消息 | InitMsg | 当所有玩家就位且Socket连接正常后,服务器向客户端发送初始化游戏的消息。 | 地图ID、玩家数量、总轮数 |
移动消息 | MoveMsg | 客户端在移动操作时,向服务器发送移动消息,服务器接收后会广播给除发送者以外的所有客户端。 | 方向、当前位置 |
发射消息 | ShootMsg | 类似于移动消息,用于表示玩家的发射操作。 | 方向、初始位置 |
死亡消息 | DeadMsg | 玩家发送给服务器的死亡消息,告知服务器该玩家已经死亡。服务器根据死亡人数判断是否结束游戏。 | 死亡玩家ID |
重置消息 | ResetMsg | 当前局结束后,服务器广播重置游戏消息,并记录本局的胜利玩家ID。 | 胜者ID、当前轮数 |
结束消息 | OverMsg | 所有游戏局数完成后,服务器广播游戏结束消息,获取每个玩家的积分,进入结算页面。 | 玩家积分 |
在本项目开发初期,服务采用了多线程BIO模式,服务端对于每一个客户端socket都需要单独使用一个线程。但在后续的开发过程中,我们发现线程之间处理阻碍我们后续业务逻辑的设计与开发,就将服务端改为了多路复用的IO模式。
本系统中,用户所在状态可以大致分为3种情况:大厅(包括登陆/注册)、房间和游戏。服务端会维护一个socket与用户的哈希表(users),以此来记录对应用户的当前状态。同时,当用户的状态发生转移时,服务端也会随之修改。
由于这3种状态所需要处理的业务相对独立,所以我们以上面的状态为粒度创建了对应的处理对象(Handler)。服务端只要监听到来自客户端的消息,就会把对应的socket传给Handler,它就会根据消息中的具体信息进行具体的业务处理。
不记录状态,转发
多路复用
Junit测试,并行测试
本小组的java项目实现了一个基本的多人在线坦克大战游戏,但对于游戏玩法和游戏界面等仍需要进行拓展和增强,使用户体验感更好。以下就一些方向进行未来展望。
- 游戏界面和用户体验改进:优化游戏界面设计,使其更加美观、直观和用户友好。房间内部增加踢人、邀请等功能。游戏内部考虑添加音效和动画效果来提升游戏的沉浸感和乐趣。
- 游戏玩法增强:考虑添加更多的游戏元素和玩法机制,如道具系统、技能系统、不同类型的坦克等。这些增强将增加游戏的深度和可玩性。
- 增加游戏模式:除了单一的战斗模式,考虑添加其他游戏模式,如团队合作模式、生存模式、竞速模式等。这将给玩家提供更多选择和挑战。
- 社交功能:添加社交功能,如好友系统、公告板等,使玩家能够更好地互动和交流。
- 游戏数据分析:实现数据分析功能,收集玩家的游戏数据,并进行统计和分析,从中获取洞察和优化游戏。并且可以设置游戏历史,供用户进行查看。
- 网络优化和服务器扩展:确保游戏在网络方面的稳定性和流畅性,使用负载均衡和分布式服务器架构来支持更多在线玩家。
总之,本项目可以通过不断完善和添加功能来进行改进,逐渐打造成为一个更丰富、更吸引人的游戏。
项目展示内容以及注意事项
- 登录&注册&设置服务器ip/port
- 创建房间&大厅房间状态
- 所有玩家进入大厅后再创建房间
- 大厅和房间内的聊天
- 对自己&对他人&对所有人
- 游戏准备与取消准备
- 进入游戏后的对战&重置&胜利结算
- 移动动作慢一些(防止瞬移)
- 坦克出生点离远一些
- 回到房间后的玩家状态
- 退出房间和解散房间后的房间状态