image.png
延时同步处理我们先看看不处理延时的情况:
image.png
网络延时是无法避免的, 但我们可以通过一些方法让玩家感受不到延时, 主要有以下三个步骤
预测先说明预测不是预判, 也需要玩家进行操作, 只是 客户端 不再等待 服务端 的返回, 先自行计算操作展示给玩家, 等 服务端 状态返回后再次渲染:
image.png
虽然在客户端通过预测的方式提前模拟了玩家的操作, 但是服务端返回的状态始终是之前的状态, 所以我们会发现有状态回退的现象发生
和解预测能让客户端流畅的运行, 如果我们在此基础上再做一层处理是否能够避免状态回退的方式呢? 如果我们在收到服务端的延迟状态的时候, 在这个延迟基础上再进行预测就可以避免回退啦! 看看下面的流程:
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 的效果有所不同.
为什么说标题是入门即入土? 网络联机游戏的原理还有很多很多, 通信和同步测量只是基础中的基础, 在学习的过程中才发现, 联机游戏的领域还很大, 这对我来说是一个很大的挑战.
七、参考- 如何设计大型游戏服务器架构?-今日头条[6]
- 2 天做了个多人实时对战,200ms 延迟竟然也能丝滑流畅? - 掘金[7]
- 如何做一款网络联机的游戏? - 知乎[8]
[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 -