流星蝴蝶剑怎么网上联机,流星蝴蝶剑如何局域网联机对战

首页 > 实用技巧 > 作者:YD1662023-05-29 14:35:43

image.png

延时同步处理

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

流星蝴蝶剑怎么网上联机,流星蝴蝶剑如何局域网联机对战(9)

image.png

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

预测

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

流星蝴蝶剑怎么网上联机,流星蝴蝶剑如何局域网联机对战(10)

image.png

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

和解

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

流星蝴蝶剑怎么网上联机,流星蝴蝶剑如何局域网联机对战(11)

image.png

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

插值

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

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

五、同步策略主要实现[2]

// index.tsx type Action = { actionId: string; actionType: -1 | 1; ts: number; }; const GameDemo = () => { const [socket, setSocket] = useState(io()); const [playerList, setPlayerList] = useState<Player[]>([]); const [serverPlayerList, setServerPlayerList] = useState<Player[]>([]); const [query, setQuery] = useUrlState({ port: 3101, host: "localhost" }); const curPlayer = useRef(new Player({ id: nanoid(), speed: 5 })); const btnTimer = useRef<number>(0); const actionList = useRef<Action[]>([]); const prePlayerList = useRef<Player[]>([]); useEffect(() => { initSocket(); }, []); const initSocket = () => { const { host, port } = query; console.error(host, port); const socket = io(`ws://${host}:${port}`); socket.id = curPlayer.current.id; setSocket(socket); socket.on("connect", () => { // 创建玩家 socket.emit("create-player", { id: curPlayer.current.id }); }); socket.on("create-player-done", ({ playerList }) => { setPlayerList(playerList); const curPlayerIndex = (playerList as Player[]).findIndex( (player) => player.id === curPlayer.current.id ); curPlayer.current.socketId = playerList[curPlayerIndex].socketId; }); socket.on("player-disconnect", ({ id, playerList }) => { setPlayerList(playerList); }); socket.on("interval-update", ({ state }) => { curPlayer.current.state = state; }); socket.on( "update-state", ({ playerList, actionId: _actionId, }: { playerList: Player[]; actionId: string; ts: number; }) => { setPlayerList(playerList); const player = playerList.find((p) => curPlayer.current.id === p.id); if (player) { // 和解 if (player.reconciliation && _actionId) { const actionIndex = actionList.current.findIndex( (action) => action.actionId === _actionId ); // 偏移量计算 let pivot = 0; // 过滤掉状态之前的操作, 留下预测操作 for (let i = actionIndex; i < actionList.current.length; i ) { pivot = actionList.current[i].actionType; } const newPlayerState = cloneDeep(player); // 计算和解后的位置 newPlayerState.state.x = pivot * player.speed; curPlayer.current = newPlayerState; } else { curPlayer.current = player; } } playerList.forEach((player) => { // 其他玩家 if (player.interpolation && player.id !== curPlayer.current.id) { // 插值 const prePlayerIndex = prePlayerList.current.findIndex( (p) => player.id === p.id ); // 第一次记录 if (prePlayerIndex === -1) { prePlayerList.current.push(player); } else { // 如果已经有过去的状态 const thumbEl = document.getElementById(`thumb-${player.id}`); if (thumbEl) { const prePos = { x: prePlayerList.current[prePlayerIndex].state.x, }; new TWEEN.Tween(prePos) .to({ x: player.state.x }, 100) .onUpdate(() => { thumbEl.style.setProperty( "transform", `translateX(${prePos.x}px)` ); console.error("onUpdate", 2, prePos.x); }) .start(); } prePlayerList.current[prePlayerIndex] = player; } } }); } ); // 服务端无延迟返回状态 socket.on("update-real-state", ({ playerList }) => { setServerPlayerList(playerList); }); }; // 玩家操作 (输入) // 向左移动 const handleLeft = () => { const { id, predict, speed, reconciliation } = curPlayer.current; // 和解 if (reconciliation) { const actionId = uuidv4(); actionList.current.push({ actionId, actionType: -1, ts: Date.now() }); socket.emit("handle-left", { id, actionId }); } else { socket.emit("handle-left", { id }); } // 预测 if (predict) { curPlayer.current.state.x -= speed; } btnTimer.current = window.requestAnimationFrame(handleLeft); TWEEN.update(); }; // 向右移动 const handleRight = (time?: number) => { const { id, predict, speed, reconciliation } = curPlayer.current; // 和解 if (reconciliation) { const actionId = uuidv4(); actionList.current.push({ actionId, actionType: 1, ts: Date.now() }); socket.emit("handle-right", { id, actionId }); } else { socket.emit("handle-right", { id }); } // 预测 if (predict) { curPlayer.current.state.x = speed; } // socket.emit("handle-right", { id }); btnTimer.current = window.requestAnimationFrame(handleRight); TWEEN.update(); }; return ( <div> <div> 当前用户 <div>{curPlayer.current.id}</div> 在线用户 {playerList.map((player) => { return ( <div key={player.id} style={{ display: "flex", justifyContent: "space-around" }} > <div>{player.id}</div> <div>{moment(player.enterRoomTS).format("HH:mm:ss")}</div> </div> ); })} </div> {playerList.map((player, index) => { const mySelf = player.id === curPlayer.current.id; const disabled = !mySelf; return ( <div className="player-wrapper" key={player.id}> <div style={{ display: "flex", justifyContent: "space-evenly" }}> <div style={{ color: mySelf ? "red" : "black" }}>{player.id}</div> <div> 预测 <input disabled={disabled} type="checkbox" checked={player.predict} onChange={() => { socket.emit("predict-change", { id: curPlayer.current.id, predict: !player.predict, }); }} ></input> </div> <div> 和解 <input disabled={disabled} type="checkbox" checked={player.reconciliation} onChange={() => { socket.emit("reconciliation-change", { id: curPlayer.current.id, reconciliation: !player.reconciliation, }); }} ></input> </div> <div> 插值 <input // disabled={!disabled} disabled={true} type="checkbox" checked={player.interpolation} onChange={() => { socket.emit("interpolation-change", { id: player.id, interpolation: !player.interpolation, }); }} ></input> </div> </div> <div>Client</div> {mySelf ? ( <div className="track"> <div id={`thumb-${player.id}`} className="left" style={{ backgroundColor: teamColor[player.state.team], transform: `translateX(${ // 是否预测 curPlayer.current.predict ? curPlayer.current.state.x : player.state.x }px)`, }} > 自己 </div> </div> ) : ( <div className="track"> <div id={`thumb-${player.id}`} className="left" style={ // 是否插值 player.interpolation ? { backgroundColor: teamColor[player.state.team], } : { backgroundColor: teamColor[player.state.team], transform: `translateX(${player.state.x}px)`, } } > 别人 </div> </div> )} <div>Server</div> {serverPlayerList.length && ( <div className="server-track"> <div className="left" style={{ backgroundColor: teamColor[player.state.team], transform: `translateX(${ serverPlayerList[index]?.state?.x ?? 0 }px)`, }} ></div> </div> )} <div> delay: <input type="number" min={1} max={3000} onChange={(e) => { const val = parseInt(e.target.value); socket.emit("delay-change", { delay: val, id: curPlayer.current.id, }); }} value={player.delay} disabled={disabled} ></input> speed: <input onChange={(e) => { const val = e.target.value === "" ? 0 : parseInt(e.target.value); socket.emit("speed-change", { speed: val, id: curPlayer.current.id, }); }} value={player.speed} disabled={disabled} ></input> </div> <button onMouseDown={() => { window.requestAnimationFrame(handleLeft); }} onMouseUp={() => { cancelAnimationFrame(btnTimer.current); }} disabled={disabled} > 左 </button> <button onMouseDown={() => { window.requestAnimationFrame(handleRight); }} onMouseUp={() => { cancelAnimationFrame(btnTimer.current); }} disabled={disabled} > 右 </button> </div> ); })} </div> ); }; export default memo(GameDemo); 六、结束语

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

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

七、参考参考资料

[1]

极度简陋的聊天室 Demo (React node): https://github.com/SmaIIstars/react-demo/tree/master/src/pages/socket/chat-room

[2]

同步策略主要实现: https://github.com/SmaIIstars/react-demo/tree/master/src/pages/socket/game-demo

[3]

大佬King: https://juejin.cn/user/3272618092799501

[4]

他的动图: https://juejin.cn/post/7041560950897377293

[5]

动图里面的效果: https://github.com/SmaIIstars/react-demo/tree/master/src/pages/socket/game-demo

[6]

如何设计大型游戏服务器架构?-今日头条: https://www.toutiao.com/article/6768682173030466051/

[7]

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

[8]

如何做一款网络联机的游戏? - 知乎: https://www.zhihu.com/question/275075420

- END -

上一页123末页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.