首页 游戏指南正文内容

联机游戏原理入门即入土-入门篇

admin 2026-02-04 167

本文来自字节教育-成人与创新前端团队,已授权ELab发布。

一、背景

联机游戏是指多个客户端共同参与的游戏,这里主要有以下三种方式

玩家主机的P2P联机模式,比如流星蝴蝶剑、以及破解游戏(盗版)

玩家进入公共服务器进行游戏,玩家资料由服务器储存的网络游戏,比如星际争霸、魔兽等

可以在单人模式中开启局域网来与他人进行多人游戏,但仅限于连接同一局域网的玩家使用

二、服务器架构历史

大多数联机游戏采用的是CS架构,使用独立设备作为主机与玩家进行交互通信

client/server架构

第一代架构(一个服):

这种模式,将所有玩家的请求发送到同一个线程中进行处理,主线程每隔一段时间对所有对象进行更新.适合一些回合制以及运算量小的游戏

第二代架构(分服):

后来随着玩家越来越多,第一代架构已经不堪重负,于是就产生了第二种架构---分服,这样对玩家进行分流,让玩家在不同的服务器上玩,不同服之间就像不同的平行世界

第三代架构(世界服):

虽然第二代架构已经可以满足玩家增长的需求(人满了就再开个服),但是又出现了玩家开始想跨服玩或者时间长了,单服务器上没有多少活跃玩家,所以又出现了世界服模型

基础三层架构

这种设计将网关、和数据存储进行分离,数据使用同一个数据服务器,不同游戏服务器的数据交换由网关进行交换

进阶三层架构

在基础三层架构的基础上再进行拆分,将不同的功能进行抽离独立,提高性能

无缝地图架构

在进阶三层架构中,地图的切换总是需要loading(DNF),为了解决这个问题,在无缝地图架构中,由一组节点(Node)服务器来管理地图区域,这个组就是NodeMaster,它来进行整体管理,如果还有更大的就再又更大的WorldMaster来进行管理

玩家在地图上进行移动其实就是在Node服务器间进行移动,比如从A----B,需要由NodeMaster把数据从NodeA复制到NodeB后,再移除NodeA的数据

三、通信

联机最大特点便是多玩家之间的交互,保证每个玩家的数据和显示一致是必不可少的步骤,在介绍同步方案之前,我们先来了解一下如何实现两端的通信

长连接通信()
//clientimportReact,{memo,useEffect,useState,useRef}from"react";import{io}from"";import{nanoid}from"nanoid";import"./";consthost="192.168.0.108",port=3101;constChatRoom=()={const[socket,setSocket]=useState(io());const[message,setMessage]=useState("");const[content,setContent]=useState{id:string;message:string;type?:string;}[]([]);const[userList,setUserList]=useStatestring[]([]);constuserInfo=useRef({id:"",enterRoomTS:0});constroomState=useRef({content:[]as{id:string;message:string;type?:string;}[],});useEffect(()={//初始化SocketinitSocket();//初始化用户信息={id:nanoid(),enterRoomTS:(),};},[]);useEffect(()={=content;},[content]);constinitSocket=()={constsocket=io(`ws://${host}:${port}`);setSocket(socket);//建立连接("connect",()={("连接成功");//用户加入("adduser",);});//用户加入聊天室("userjoined",({id,userList})={constnewContent=[];({id,message:`${id}加入`,type:"tip"});setContent(newContent);setUserList(userList);});//新消息("newmessage",({id,message})={constnewContent=[];({id,message});setContent(newContent);});//用户离开聊天室("userleave",function({id,userList}){constnewContent=[];({id,message:`${id}离开`,type:"tip"});setContent(newContent);setUserList(userList);});};consthandleEnterS:=(e)={if(==="Enter"){//客户端发送新消息("newmessage",{id:,message,});setMessage("");();}};consthandleButtonS=()={//客户端发送新消息("newmessage",{id:,message,});setMessage("");};consthandleChange:=(e)={constval=??"";setMessage(val);};consthandleQuit=()={//断开连接();};return(div///div);};exportdefaultmemo(ChatRoom);
//serverimport{Server}from"";consthost="192.168.0.108",port=3101;constio=newServer(port,{cors:true});constsessionList=[];("connection",(socket)={("socketconnectedsuccessful");//用户进入聊天室("adduser",({id})={=id;if(!(id)){(id);}(`${id}已加入房间,房间人数:${}`);((sessionList));("userjoined",{id,userList:sessionList});});//发送的新消息("newmessage",({id,message})={("newmessage",{id,message});});("disconnect",()={((),1);("userleave",{id:,userList:sessionList,});});});
四、同步策略

现在大多游戏常用的两种同步技术方向分别是:帧同步和状态同步

帧同步

帧同步的方式服务端很简单,只承担了操作转发的操作,你给我了什么,我就通知其他人你怎么了,具体的执行是各个客户端拿到操作后自己执行

状态同步

状态同步是客户端将操作告诉服务端,然后服务端拿着操作进行计算,最后把结果返给各个客户端,然后客户端根据新数据进行渲染即可

延时同步处理

我们先看看不处理延时的情况:

网络延时是无法避免的,但我们可以通过一些方法让玩家感受不到延时,主要有以下三个步骤

预测

先说明预测不是预判,也需要玩家进行操作,只是客户端不再等待服务端的返回,先自行计算操作展示给玩家,等服务端状态返回后再次渲染:

虽然在客户端通过预测的方式提前模拟了玩家的操作,但是服务端返回的状态始终是之前的状态,所以我们会发现有状态回退的现象发生

和解

预测能让客户端流畅的运行,如果我们在此基础上再做一层处理是否能够避免状态回退的方式呢?如果我们在收到服务端的延迟状态的时候,在这个延迟基础上再进行预测就可以避免回退啦!看看下面的流程:

我们把服务端返回老状态作为基础状态,然后再筛选出这个老状态之后的操作进行预测,这样就可以避免客户端回退的现象发生

插值

我们通过之前的预测、和解两个步骤,已经可以实现客户端无延迟且不卡顿的效果,但是联机游戏是多玩家交互,自己虽然不卡了,但是在别的玩家那里却没有办法做预测和和解,所以在其他玩家的视角中,我们仍然是一卡一卡的

我们这时候使用一些过渡动画,让移动变得丝滑起来,虽然本质上接受到的实际状态还是一卡一卡的,但是至少看起来不卡

五、同步策略主要实现[2]
//={actionId:string;actionType:-1|1;ts:number;};constGameDemo=()={const[socket,setSocket]=useState(io());const[playerList,setPlayerList]=useStatePlayer[]([]);const[serverPlayerList,setServerPlayerList]=useStatePlayer[]([]);const[query,setQuery]=useUrlState({port:3101,host:"localhost"});constcurPlayer=useRef(newPlayer({id:nanoid(),speed:5}));constbtnTimer=useRefnumber(0);constactionList=useRefAction[]([]);constprePlayerList=useRefPlayer[]([]);useEffect(()={initSocket();},[]);constinitSocket=()={const{host,port}=query;(host,port);constsocket=io(`ws://${host}:${port}`);=;setSocket(socket);("connect",()={//创建玩家("create-player",{id:});});("create-player-done",({playerList})={setPlayerList(playerList);constcurPlayerIndex=(playerListasPlayer[]).findIndex((player)====);=playerList[curPlayerIndex].socketId;});("player-disconnect",({id,playerList})={setPlayerList(playerList);});("interval-update",({state})={=state;});("update-state",({playerList,actionId:_actionId,}:{playerList:Player[];actionId:string;ts:number;})={setPlayerList(playerList);constplayer=((p)====);if(player){//和解if(_actionId){constactionIndex=((action)====_actionId);//偏移量计算letpivot=0;//过滤掉状态之前的操作,留下预测操作for(leti=actionIndex;;i++){pivot+=[i].actionType;}constnewPlayerState=cloneDeep(player);//计算和解后的位置+=pivot*;=newPlayerState;}else{=player;}}((player)={//其他玩家if(!==){//插值constprePlayerIndex=((p)====);//第一次记录if(prePlayerIndex===-1){(player);}else{//如果已经有过去的状态constthumbEl=(`thumb-${}`);if(thumbEl){constprePos={x:[prePlayerIndex].,};(prePos).to({x:},100).onUpdate(()={("transform",`translateX(${}px)`);("onUpdate",2,);}).start();}[prePlayerIndex]=player;}}});});//服务端无延迟返回状态("update-real-state",({playerList})={setServerPlayerList(playerList);});};//玩家操作(输入)//向左移动consthandleLeft=()={const{id,predict,speed,reconciliation}=;//和解if(reconciliation){constactionId=uuidv4();({actionId,actionType:-1,ts:()});("handle-left",{id,actionId});}else{("handle-left",{id});}//预测if(predict){=speed;}=(handleLeft);();};//向右移动consthandleRight=(time?:number)={const{id,predict,speed,reconciliation}=;//和解if(reconciliation){constactionId=uuidv4();({actionId,actionType:1,ts:()});("handle-right",{id,actionId});}else{("handle-right",{id});}//预测if(predict){+=speed;}//("handle-right",{id});=(handleRight);();};return(divdiv当前用户div{}/div在线用户{((player)={return(divkey={}style={{display:"flex",justifyContent:"space-around"}}div{}/divdiv{moment().format("HH:mm:ss")}/div/div);})}/div{((player,index)={constmySelf====;constdisabled=!mySelf;return(divclassName="player-wrapper"key={}divstyle={{display:"flex",justifyContent:"space-evenly"}}divstyle={{color:mySelf?"red":"black"}}{}/divdiv预测inputdisabled={disabled}type="checkbox"checked={}onChange={()={("predict-change",{id:,predict:!,});}}/input/divdiv和解inputdisabled={disabled}type="checkbox"checked={}onChange={()={("reconciliation-change",{id:,reconciliation:!,});}}/input/divdiv插值input//disabled={!disabled}disabled={true}type="checkbox"checked={}onChange={()={("interpolation-change",{id:,interpolation:!,});}}/input/div/divdivClient/div{mySelf?(divclassName="track"divid={`thumb-${}`}className="left"style={{backgroundColor:teamColor[],transform:`translateX(${//是否预测?:}px)`,}}自己/div/div):(divclassName="track"divid={`thumb-${}`}className="left"style={//是否插值?{backgroundColor:teamColor[],}:{backgroundColor:teamColor[],transform:`translateX(${}px)`,}}别人/div/div)}divServer/div{(divclassName="server-track"divclassName="left"style={{backgroundColor:teamColor[],transform:`translateX(${serverPlayerList[index]?.state?.x??0}px)`,}}/div/div)}divdelay:inputtype="number"min={1}max={3000}onChange={(e)={constval=parseInt();("delay-change",{delay:val,id:,});}}value={}disabled={disabled}/inputspeed:inputonChange={(e)={constval====""?0:parseInt();("speed-change",{speed:val,id:,});}}value={}disabled={disabled}/input/divbuttononMouseDown={()={(handleLeft);}}onMouseUp={()={cancelAnimationFrame();}}disabled={disabled}左/buttonbuttononMouseDown={()={(handleRight);}}onMouseUp={()={cancelAnimationFrame();}}disabled={disabled}右/button/div);})}/div);};exportdefaultmemo(GameDemo);
六、结束语

首先感谢在学习过程中给我提供帮助的大佬King[3].我先模仿着他的动图[4]和讲解的思路自己实现了一版动图里面的效果[5],我发现我的效果总是比较卡顿,于是我拿到了动图demo的代码进行学习,原来只是一个纯前端的演示效果,所以与我使用socket的效果有所不同.

为什么说标题是入门即入土?网络联机游戏的原理还有很多很多,通信和同步测量只是基础中的基础,在学习的过程中才发现,联机游戏的领域还很大,这对我来说是一个很大的挑战.

七、参考

2天做了个多人实时对战,200ms延迟竟然也能丝滑流畅?-掘金[7]

如何做一款网络联机的游戏?-知乎[8]

参考资料

[1]

极度简陋的聊天室Demo(React+node):

[2]

同步策略主要实现:

[3]

大佬King:

[4]

他的动图:

[5]

动图里面的效果:

[6]

[7]

2天做了个多人实时对战,200ms延迟竟然也能丝滑流畅?-掘金:

[8]

如何做一款网络联机的游戏?-知乎:

-END-

文章目录