浅谈多人游戏原理和简单实现。

  • 😜           :是江迪呀
  • ✒️本文关键词websocket网络原理多人游戏
  • ☀️每日   一言这世上有两种东西无法直视,一是太阳,二是人心!

在这里插入图片描述

一、我的游戏史

我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿是我挨得最狠的,以至于现在回想起来,屁股还条件反射的隐隐作痛。

后来我骗我妈说我要学习英语、练习打字以后成为祖国的栋梁之才!让她给我买台小霸王学习机(游戏机),在我一哭二闹三上吊胡搅蛮缠的攻势下,我妈妥协了。就此我接触到了FC游戏。现在还能记得我和朋友玩激龟快打,满屋子的小朋友在看的场景。经常有家长在我家门口喊他家小孩吃饭。那时候我们县城里面有商店卖游戏卡,小卡一张5块钱,一张传奇卡25-40块不等(所谓传奇卡就是角色扮演,带有存档的游戏),每天放学都要去商店去看看,有没有新的游戏卡,买不起,就看下封面过过瘾。我记得我省吃俭用一个多月,买了两张卡:《哪吒传奇》和《重装机兵》那是真的上瘾,没日没夜的玩。

再然后我接触到了手机游戏,记得那时候有个软件叫做冒泡游戏(我心目的中的Stream),里面好多游戏,太吸引我了。一个游戏一般都是几百KB,最大也就是几MB,不过那时候流量很贵,1块钱1MB,并且!一个月只有30Mb。我姑父是收手机的,我在他那里搞到了一部半智能手机,牌子我现在还记得:诺基亚N70,那时候我打开游戏就会显示一杯冒着热气的咖啡,我很喜欢这个图标,因为看见它意味着我的游戏快加载完成了,没想到,十几年后我们会再次相遇,哈哈哈哈。我当时玩了一款网游叫做:《幻想三国》,第一回接触网游简直惊呆了,里面好多人都是其他玩的家,这太有趣了。并且我能在我的手机上看到其他玩家,能够看到他们的行为动作,这太神奇了!!!我也一直思考这到底是怎么实现的!

最后是电脑游戏,单机:《侠盗飞车》、《植物大战僵尸》、《虐杀原型》;网游:《DNF》、《CF》、《LOL》、《梦幻西游》我都玩过。

不过那个疑问一直没有解决,也一值留在我心中 —— 在网络游戏中,是如何实时更新其他玩家行为的呢?

二、解惑

在我进入大学后,我选择了软件开发专业,真巧!再次遇到了那个冒着热气的咖啡图标,这时我才知道它叫做——Java。我很认真的去学,希望有一天能够做一款游戏!

参加工作后,我并没有如愿以偿,我成为了一名Java开发程序员,但是我在日常的开发的都是web应用,接触到大多是HTTP请求,它是种请求-响应协议模式。这个问题也还是想不明白,难道每当其他玩家做一个动作都需要发送一次HTTP请求?然后响应给其他玩家。这样未免效率也太低了吧,如果一个服务器中有几千几万人,那么服务器得承受多大压力呀!一定不是这样的!!!

直到我遇到了Websocket,这是一种长连接,而HTTP是一种短连接,顿时这个问题我就想明白了。在此二者的区别我就不过多赘述了,请看我的另一篇文章:WebSocket详解以及应用

知道了这个知识后,我终于能够大致明白了网络游戏的基本原理。原来网络游戏是由客户端服务器端组成的,客户端就是我们下载到电脑或者手机上的应用,而服务器端就是把其他玩家连接起来的中转站,还有一点需要说明的是,网络游戏是分房间的,这个房间就相当于一台服务器。首先,在玩家登陆客户端并选择房间建立长连接后,A玩家做出移动的动作,随即会把这个动作指令上传给服务器,然后服务器再将指令广播到房间中的其他玩家的客户端来操作A的角色,这样就可以实现实时更新其他玩家行为。

在这里插入图片描述

三、简单实现

客户端服务端在处理指令时,方法必须是配套的。比如说,有新的玩家连接到服务器,那么服务器就应当向其它客户端广播创建一个新角色的指令,客户端在接收到该指令后,执行客户端创建角色的方法。
为了方便演示,这里需要定义两个HTML来表示两个不同的客户端不同的玩家,这两套客户端代码除了玩家的信息不一样,其它完全一致!!!

3.1 客户端实现步骤

我在这里客户端使用HTML+JQ实现

客户端——1代码:

(1)创建画布

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Canvas Game</title><style>canvas {border: 1px solid black;}</style><script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<canvas id="gameCanvas" width="800" height="800"></canvas>
</body>
</html>

(2)设置1s60帧更新页面

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function clearCanvas() {ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function gameLoop() {clearCanvas();players.forEach(player => {player.draw();});}setInterval(gameLoop, 1000 / 60);
//清除画布方法
function clearCanvas() {ctx.clearRect(0, 0, canvas.width, canvas.height);}

(3)连接游戏服务器并处理指令

这里使用websocket链接游戏服务器

 //连接服务器
const websocket = new WebSocket("ws://192.168.31.136:7070/websocket?userId=" + userId + "&userName=" + userName);//向服务器发送消息function sendMessage(userId,keyCode){const messageData = {playerId: userId,keyCode: keyCode};websocket.send(JSON.stringify(messageData));}//接收服务器消息,并根据不同的指令,做出不同的动作websocket.onmessage = event => {const data = JSON.parse(event.data);// 处理服务器发送过来的消息console.log('Received message:', data);//创建游戏对象if(data.type == 1){console.log("玩家信息:" +  data.players.length)for (let i = 0; i < data.players.length; i++) {console.log("玩家id:"+playerOfIds);createPlayer(data.players[i].playerId,data.players[i].pointX, data.players[i].pointY, data.players[i].color);}}//销毁游戏对象if(data.type == 2){console.log("玩家信息:" +  data.players.length)for (let i = 0; i < data.players.length; i++) {destroyPlayer(data.players[i].playerId)}}//移动游戏对象if(data.type == 3){console.log("移动;玩家信息:" +  data.players.length)for (let i = 0; i < data.players.length; i++) {players.filter(player => player.id === data.players[i].playerId)[0].move(data.players[i].keyCode)}}};

(4)创建玩家对象

//存放游戏对象
let players = [];
//playerId在此写死,正常情况下应该是用户登录获取的
const userId = "1"; // 用户的 id
const userName = "逆风笑"; // 用户的名称
//玩家对象
class Player {constructor(id,x, y, color) {this.id = id;this.x = x;this.y = y;this.size = 30;this.color = color;}//绘制游戏角色方法draw() {ctx.fillStyle = this.color;ctx.fillRect(this.x, this.y, this.size, this.size);}//游戏角色移动方法	move(keyCode) {switch (keyCode) {case 37: // Leftthis.x = Math.max(0, this.x - 10);break;case 38: // Upthis.y = Math.max(0, this.y - 10);break;case 39: // Rightthis.x = Math.min(canvas.width - this.size, this.x + 10);break;case 40: // Downthis.y = Math.min(canvas.height - this.size, this.y + 10);break;}this.draw();}}

(5)客户端创建角色方法

//创建游戏对象方法
function createPlayer(id,x, y, color) {const player = new Player(id,x, y, color);players.push(player);playerOfIds.push(id);return player;
}

(6)客户端销毁角色方法

在玩家推出客户端后,其它玩家的客户端应当销毁对应的角色。

//角色销毁
function destroyPlayer(playId){players = players.filter(player => player.id !== playId);
}

客户端——2代码:

客户端2的代码只有玩家信息不一致:

  const userId = "2"; // 用户的 idconst userName = "逆风哭"; // 用户的名称

3.2 服务器端

服务器端使用Java+websocket来实现!

(1)引入依赖:

 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.1.2.RELEASE</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId><version>2.3.7.RELEASE</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.11</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.75</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.16.16</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.6.3</version></dependency>

(2)创建服务器

@Component
@ServerEndpoint("/websocket")
@Slf4j
public class Server {/*** 服务器玩家池* 解释:这里使用 ConcurrentHashMap为了保证线程安全,不会出现同一个玩家存在多条记录问题* 使用 static fina修饰 是为了保证 playerPool 全局唯一*/private static final ConcurrentHashMap<String, Server> playerPool = new ConcurrentHashMap<>();/*** 存储玩家信息*/private static final ConcurrentHashMap<String, Player> playerInfo = new ConcurrentHashMap<>();/*** 已经被创建了的玩家id*/private static ConcurrentHashMap<String, Server> createdPlayer = new ConcurrentHashMap<>();private Session session;private Player player;/*** 连接成功后调用的方法*/@OnOpenpublic void webSocketOpen(Session session) throws IOException {Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();String userId = requestParameterMap.get("userId").get(0);String userName = requestParameterMap.get("userName").get(0);this.session = session;if (!playerPool.containsKey(userId)) {int locationX = getLocation(151);int locationY = getLocation(151);String color = PlayerColorEnum.getValueByCode(getLocation(1) + 1);Player newPlayer = new Player(userId, userName, locationX, locationY,color,null);playerPool.put(userId, this);this.player = newPlayer;//存放玩家信息playerInfo.put(userId,newPlayer);}log.info("玩家:{}|{}连接了服务器", userId, userName);// 创建游戏对象this.createPlayer(userId);}/*** 接收到消息调用的方法*/@OnMessagepublic void onMessage(String message, Session session) throws IOException, InterruptedException {log.info("用户:{},消息{}:",this.player.getPlayerId(),message);PlayerDTO playerDTO = new PlayerDTO();Player player = JSONObject.parseObject(message, Player.class);List<Player> players = new ArrayList<>();players.add(player);playerDTO.setPlayers(players);playerDTO.setType(OperationType.MOVE_OBJECT.getCode());String returnMessage = JSONObject.toJSONString(playerDTO);//广播所有玩家for (String key : playerPool.keySet()) {synchronized (session){String playerId = playerPool.get(key).player.getPlayerId();if(!playerId.equals(this.player.getPlayerId())){playerPool.get(key).session.getBasicRemote().sendText(returnMessage);}}}}/*** 关闭连接调用方法*/@OnClosepublic void onClose() throws IOException {String playerId = this.player.getPlayerId();log.info("玩家{}退出!", playerId);Player playerBaseInfo = playerInfo.get(playerId);//移除玩家for (String key : playerPool.keySet()) {playerPool.remove(playerId);playerInfo.remove(playerId);createdPlayer.remove(playerId);}//通知客户端销毁对象destroyPlayer(playerBaseInfo);}/*** 出现错误时调用的方法*/@OnErrorpublic void onError(Throwable error) {log.info("服务器错误,玩家id:{},原因:{}",this.player.getPlayerId(),error.getMessage());}/*** 获取随即位置* @param seed* @return*/private int getLocation(Integer seed){Random random = new Random();return random.nextInt(seed);}
}

websocket配置:

@Configuration
public class ServerConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter(){return new ServerEndpointExporter();}
}

(3)创建玩家对象

玩家对象:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {/*** 玩家id*/private String playerId;/*** 玩家名称*/private String playerName;/*** 玩家生成的x坐标*/private Integer pointX;/*** 玩家生成的y坐标*/private Integer pointY;/*** 玩家生成颜色*/private String color;/*** 玩家动作指令*/private Integer keyCode;
}

创建玩家对象返回给客户端DTO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerDTO {private Integer type;private List<Player> players;
}

玩家移动指令返回给客户端DTO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerMoveDTO {private Integer type;private List<Player> players;
}

(4)动作指令

public enum OperationType {CREATE_OBJECT(1,"创建游戏对象"),DESTROY_OBJECT(2,"销毁游戏对象"),MOVE_OBJECT(3,"移动游戏对象"),;private Integer code;private String value;OperationType(Integer code, String value) {this.code = code;this.value = value;}public Integer getCode() {return code;}public String getValue() {return value;}
}

(5)创建对象方法

  /*** 创建对象方法* @param playerId* @throws IOException*/private void createPlayer(String playerId) throws IOException {if (!createdPlayer.containsKey(playerId)) {List<Player> players = new ArrayList<>();for (String key : playerInfo.keySet()) {Player playerBaseInfo = playerInfo.get(key);players.add(playerBaseInfo);}PlayerDTO playerDTO = new PlayerDTO();playerDTO.setType(OperationType.CREATE_OBJECT.getCode());playerDTO.setPlayers(players);String syncInfo = JSONObject.toJSONString(playerDTO);for (String key :playerPool.keySet()) {playerPool.get(key).session.getBasicRemote().sendText(syncInfo);}// 存放createdPlayer.put(playerId, this);}}

(6)销毁对象方法

   /*** 销毁对象方法* @param playerBaseInfo* @throws IOException*/private void destroyPlayer(Player playerBaseInfo) throws IOException {PlayerDTO playerDTO = new PlayerDTO();playerDTO.setType(OperationType.DESTROY_OBJECT.getCode());List<Player> players = new ArrayList<>();players.add(playerBaseInfo);playerDTO.setPlayers(players);String syncInfo = JSONObject.toJSONString(playerDTO);for (String key :playerPool.keySet()) {playerPool.get(key).session.getBasicRemote().sendText(syncInfo);}}

四、演示

4.1 客户端1登陆服务器

在这里插入图片描述

4.2 客户端2登陆服务器

在这里插入图片描述

4.3 客户端2移动

在这里插入图片描述

4.4 客户端1移动

在这里插入图片描述

4.5 客户端1退出

在这里插入图片描述
完结撒花

完整代码传送门

五、总结

以上就是我对网络游戏如何实现玩家实时同步的理解与实现,我实现后心里也释然了,哈哈哈,真的好有趣!!!
我希望大家也是,不要失去好奇心,遇到自己感兴趣的事情,一定要多思考呀~

后来随着我经验的不断积累,我又去了解了一下Java作为游戏服务器的相关内容,发现Netty更适合做这个并且更容易入门,比如《我的世界》一些现有的服务器就是使用Netty实现的。有空也实现下,玩玩~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/63244.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

nginx反向代理 负载均衡

目录 1.反向代理介绍&#xff1a; 2.七层代理和四层代理&#xff1a; 2.1 七层代理&#xff1a; 2.2 四层代理&#xff1a; 3.反向代理web服务器&#xff1a; 3.1 代理服务器配置&#xff1a; 3.2 服务器配置 &#xff1a; 3.3 客户端访问&#xff1a; 3.4 代理不同端口&am…

VMware虚拟机的安装以及安装CentOS系统

VMware虚拟机是一款非常受欢迎的虚拟化软件&#xff0c;它可以模拟一台计算机运行在另一台计算机上&#xff0c;从而实现在一台物理机器上运行多个虚拟机的目的。在本文中&#xff0c;我们将会介绍如何安装VMware虚拟机并在其中安装CentOS操作系统。 文章目录 &#x1f4c0;VMw…

微机原理 || 8253接口芯片知识点+4道经典例题+手写解题过程

&#xff08;二&#xff09;经典考题&#xff1a; 【例1】: 设8253端口地址为300H&#xff5e;303H&#xff0c;要求计数器2工作在方式5&#xff0c;二进制计数&#xff0c;CLK22MHz&#xff0c;OUT21KHz。试按上述要求完成8253的初始化。 【例2】&#xff1a;选择计数器0工…

说说FLINK细粒度滑动窗口如何处理

分析&回答 Flink的窗口机制是其底层核心之一&#xff0c;也是高效流处理的关键。Flink窗口分配的基类是WindowAssigner抽象类&#xff0c;下面的类图示出了Flink能够提供的所有窗口类型。 Flink窗口分为滚动&#xff08;tumbling&#xff09;、滑动&#xff08;sliding&am…

【LeetCode算法系列题解】第21~25题

CONTENTS LeetCode 21. 合并两个有序链表&#xff08;简单&#xff09;LeetCode 22. 括号生成&#xff08;中等&#xff09;LeetCode 23. 合并K个升序链表&#xff08;困难&#xff09;LeetCode 24. 两两交换链表中的节点&#xff08;中等&#xff09;LeetCode 25. K 个一组翻转…

YOLO数据集划分(训练集、验证集、测试集)

1.将训练集、验证集、测试集按照7:2:1随机划分 1.项目准备 1.在项目下新建一个py文件&#xff0c;名字就叫做splitDataset1.py 2.将自己需要划分的原数据集就放在项目文件夹下面 以我的为例&#xff0c;我的原数据集名字叫做hatDataXml 里面的JPEGImages装的是图片 Annota…

【深度学习】ChatGPT

本文基于Andrej Karpathy(OpenAI 联合创始人&#xff0c;曾担任特斯拉的人工智能和自动驾驶视觉主管)在Microsoft Build 2023上的演讲整理而成&#xff08;完整的视频在文末&#xff0c;直接拖到文章底部&#xff09;&#xff0c;主要分为2大部分&#xff1a; 1.如何训练GPT(可…

亚马逊的邮箱可以更改吗,修改亚马逊账户邮箱的方法

亚马逊的邮箱可以更改吗&#xff1f; 可以更改&#xff0c;但更改主户邮箱需要电话对身份进行深度验证。如果需要修改&#xff0c;可以先开case向客服说明情况&#xff0c;然后根据客服的指导步骤来操作即可。 修改亚马逊账户邮箱的方法 1、登录您的账户&#xff1a;打开亚马…

如何有效防止服务器被攻击?

随着互联网的快速发展&#xff0c;服务器安全问题日益引起人们的关注。近期&#xff0c;全球范围内频繁发生的服务器攻击事件引发了广泛关注。为了保护企业和个人的数据安全&#xff0c;有效防止服务器被攻击已成为迫在眉睫的任务。 首先&#xff0c;及时更新服务器的操作系统和…

C++|观察者模式

观察者模式&#xff1a; 定义对象间的一种一对多&#xff08;变化&#xff09;的依赖关系&#xff0c;以便当一个 对象(Subject)的状态发生改变时&#xff0c;所有依赖于它的对象都 得到通知并自动更新 动机&#xff1a; 在软件构建过程中&#xff0c;我们需要为某些对象建立…

数学之美 — 1

为什么你会想和他人共享那些美丽的事物呢&#xff1f;因为这会让他&#xff08;她&#xff09;感到愉悦&#xff0c;也能让你在分享的过程中重新欣赏一次事物的美。 ——David Blackwell 1、感官之美&#xff0c;对于那些有规律的事物&#xff0c;你可以利用自己的视觉、触觉、…

python自动化测试- 自动化框架及工具

1 概述 手续的关于测试的方法论&#xff0c;都是建立在之前的文章里面提到的观点&#xff1a; 功能测试不建议做自动化接口测试性价比最高接口测试可以做自动化 后面所谈到的 测试自动化 也将围绕着 接口自动化 来介绍。 本系列选择的测试语言是 python 脚本语言。由于其…

C# NetTopologySuite+ProjNet 任意图形类型坐标转换

添加引用&#xff1a;NetTopologySuite、ProjNet、ProjNet.SRID Program.cs文件&#xff1a; using ProjNet.CoordinateSystems; using ProjNet.CoordinateSystems.Transformations; using ProjNet.SRID; using System; using System.Collections.Generic; using System.Linq;…

python通过docker打包执行

背景 正常情况下,python脚本执行需要安装有python环境,那python环境虽然也可以通过移植的方法来安装,那总归是比较麻烦的,下面通过docker打包的方式来执行python脚本 1、安装python镜像 准备两个文件即可,dockerfile、requirements.txt两个文件的内容分别如下 同目录下…

泼辣修图Ploarr5.11.7电脑最新简体中文版下载

泼辣修图专业版是一款强大的专业修图软件&#xff0c;拥有上百款调色工具还有丰富的图层素材&#xff0c; 更有智能的人像修饰面板&#xff0c;具备物体识别的智能蒙板&#xff0c;高效的滤镜管理系统和强大的文字工具&#xff0c;支持批量处理。一切围绕摄影&#xff0c;无论是…

只考一门数据结构!安徽工程大学计算机考研

安徽工程大学 考研难度&#xff08;☆&#xff09; 内容&#xff1a;23考情概况&#xff08;拟录取和复试分析&#xff09;、院校概况、23专业目录、23复试详情、各专业考情分析、各科目考情分析。 正文992字&#xff0c;预计阅读&#xff1a;3分钟 2023考情概况 安徽工程大…

three.js(六):自适应设备分辨率

自适应设备分辨率 当今大多数的PC端和移动端显示器都是HD-DPI显示器。HD-DPI 是High Definition-Dots Per Inch 的简称&#xff0c;意思是高分辨率显示器。不同设备的显示器的分辨率是不一样的。 以上图中的iPhone6/7/8 为例&#xff1a;375*667 代表的手机的屏幕的物理尺寸&a…

海康机器人工业相机 Win10+Qt+Cmake 开发环境搭建

文章目录 一. Qt搭建海康机器人工业相机开发环境 一. Qt搭建海康机器人工业相机开发环境 参考这个链接安装好MVS客户端 Qt新建一个c项目 cmakeList中添加海康机器人的库&#xff0c;如下&#xff1a; cmake_minimum_required(VERSION 3.5)project(HIKRobotCameraTest LANG…

Go framework-go-zero

一、Go Go天然适配云原生&#xff0c;而云原生时代已经到来&#xff0c;各个应用组件基础设施等都应该积极的去拥抱云原生。 不要让框架束缚开发。 1、go-zero介绍 go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性&#xff0c;…

[C/C++]天天酷跑超详细教程-中篇

个人主页&#xff1a;北海 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;C/C&#x1f91d;希望作者的文章能对你有所帮助&#xff0c;有不足的地方请在评论区留言指正&#xff0c;大家一起学习交流&#xff01;&#x1f9…