基于PubNub的类agar.io游戏卡顿延迟高的优化方案咨询
解决你的.io游戏帧率过低与网络延迟问题
嘿,我仔细啃了你的agar.io风格游戏代码,发现几个核心问题直接导致了帧率暴跌(300ms一帧)和网络延迟高的情况,咱们逐个击破:
一、帧率过低的核心原因与修复
1. 每帧重置Canvas尺寸,纯纯的性能浪费
你现在在render()里每帧都执行c.width=innerWidth和c.height=innerHeight,这会强制Canvas销毁旧的渲染上下文、清空所有内容,是顶级性能杀手。只需要在窗口大小改变时调整一次就行:
// 把Canvas尺寸初始化移到onSignIn里,再加个resize监听 onSignIn=(googleUser)=>{ c=document.createElement("canvas") document.body.style.margin=0 document.body.appendChild(c) ctx=c.getContext("2d") // 初始化尺寸 resizeCanvas() // 监听窗口变化 window.addEventListener('resize', resizeCanvas) // ... 其他代码 ... } function resizeCanvas() { c.width = innerWidth c.height = innerHeight }
2. 每帧新建Image元素,内存泄漏+重绘卡顿
遍历用户绘制头像时,你每次都document.createElement("img"),这会不断生成新DOM元素,既占内存,又会因为重复加载解码拖慢渲染。必须缓存已加载的图片:
// 全局搞个图片缓存容器 const imageCache = {} // 绘制用户时先查缓存: Object.values(obj).forEach(user => { // 缓存里没有才新建 if(!imageCache[user.imgURL]){ let img = document.createElement("img") img.src = user.imgURL imageCache[user.imgURL] = img } // 用缓存好的图绘制 ctx.drawImage(imageCache[user.imgURL], user.x - x + innerWidth/2, user.y - y + innerHeight/2, 32, 32) // ... 边框绘制代码 ... })
3. 网格绘制重复无优化
每帧都嵌套循环画整个网格太蠢了,用离屏Canvas预先画好网格,之后每帧直接把离屏Canvas贴到主Canvas上,能省超多绘制指令:
// 初始化离屏Canvas存网格 const gridCanvas = document.createElement('canvas') const gridCtx = gridCanvas.getContext('2d') function drawGrid() { gridCanvas.width = innerWidth + 64 gridCanvas.height = innerHeight + 64 gridCtx.lineWidth=4 gridCtx.strokeStyle="rgba(200,200,200,1)" for(let x2=0; x2<gridCanvas.width; x2+=32){ for(let y2=0; y2<gridCanvas.height; y2+=32){ gridCtx.strokeRect(x2,y2,32,32) } } } // 初始化和resize时各画一次 onSignIn=(googleUser)=>{ // ... 其他代码 ... drawGrid() window.addEventListener('resize', () => { resizeCanvas() drawGrid() }) } // render里直接贴缓存好的网格: render=()=>{ requestAnimationFrame(render) ctx.clearRect(0,0,c.width,c.height) // 计算视角偏移,贴网格 const offsetX = Math.floor(x/32)*32 - x const offsetY = Math.floor(y/32)*32 - y ctx.drawImage(gridCanvas, offsetX, offsetY) // ... 其他绘制代码 ... }
二、网络延迟高的核心原因与修复
1. 每帧发坐标,网络直接炸了
用requestAnimationFrame每帧都调用pubnub.publish,相当于每秒发60次坐标,服务器和客户端都扛不住,延迟不高才怪。做节流控制,比如每100ms发一次,或者坐标变多了再发:
// 全局记录上次发送时间和坐标 let lastSendTime = 0 let lastX, lastY render=()=>{ requestAnimationFrame(render) // ... 移动逻辑 ... // 节流规则:每100ms发一次,或者坐标变了8px以上再发 const now = Date.now() const deltaX = Math.abs(x - lastX) const deltaY = Math.abs(y - lastY) if(now - lastSendTime > 100 || (deltaX > 8 && deltaY > 8)){ pubnub.publish({ channel:"hello_world", message:{ content:ID, username:username, imgURL:imgURL, x:x,y:y } }) lastSendTime = now lastX = x lastY = y } // ... 绘制代码 ... }
2. 不清理离线用户,对象越变越大
你只在收到消息时加用户到obj,但用户离线后没移除,导致obj里的无效用户越来越多,遍历和绘制开销直线上升。监听PubNub的presence事件,删掉离线用户:
pubnub.addListener({ // ... 其他监听 ... presence:(presenceEvent)=>{ if(presenceEvent.action === 'leave' || presenceEvent.action === 'timeout'){ delete obj[presenceEvent.uuid] } } })
三、其他小优化
- 把键盘事件改成规范的监听方式,别覆盖全局事件:
const k = {} window.addEventListener('keydown', (e) => k[e.keyCode] = true) window.addEventListener('keyup', (e) => k[e.keyCode] = false)
- 遍历用户用
Object.values(obj)代替for...in,避免遍历原型链上的垃圾属性:
Object.values(obj).forEach(user => { // ... 绘制用户代码 ... })
优化后的完整代码(关键修改已标注)
<link rel="icon" type="image/png" href="https://i.ibb.co/sHNyD0b/tecnocomunist-star.png"> <script src="https://cdn.pubnub.com/sdk/javascript/pubnub.4.27.4.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.js"></script> <script src="https://www.gstatic.com/firebasejs/4.9.0/firebase.js"></script> <script src="https://apis.google.com/js/platform.js" async defer></script> <meta name="google-signin-client_id" content="464161591015-669suu0sat7n9c1lq2g6b3mn174g5sej.apps.googleusercontent.com"> <div style="position: absolute; left: 0px; top: 0px"class="g-signin2" data-onsuccess="onSignIn"></div> <script> const k = {} const imageCache = {} let lastSendTime = 0 let lastX, lastY let obj = {} let c, ctx, profile, ID, username, imgURL, x, y const gridCanvas = document.createElement('canvas') const gridCtx = gridCanvas.getContext('2d') // 规范键盘事件监听 window.addEventListener('keydown', (e) => k[e.keyCode] = true) window.addEventListener('keyup', (e) => k[e.keyCode] = false) function resizeCanvas() { c.width = innerWidth c.height = innerHeight } function drawGrid() { gridCanvas.width = innerWidth + 64 gridCanvas.height = innerHeight + 64 gridCtx.clearRect(0, 0, gridCanvas.width, gridCanvas.height) gridCtx.lineWidth=4 gridCtx.strokeStyle="rgba(200,200,200,1)" for(let x2=0; x2<gridCanvas.width; x2+=32){ for(let y2=0; y2<gridCanvas.height; y2+=32){ gridCtx.strokeRect(x2,y2,32,32) } } } onSignIn=(googleUser)=>{ c=document.createElement("canvas") document.body.style.margin=0 document.body.appendChild(c) ctx=c.getContext("2d") profile=googleUser.getBasicProfile() ID=profile.getId() username=profile.getName() imgURL=profile.getImageUrl() x=innerWidth/2 y=innerHeight/2 lastX = x lastY = y // 初始化Canvas和网格 resizeCanvas() drawGrid() window.addEventListener('resize', () => { resizeCanvas() drawGrid() }) render() } render=()=>{ requestAnimationFrame(render) ctx.clearRect(0,0,c.width,c.height) // 移动逻辑 if(k[37]){x-=5} if(k[38]){y-=5} if(k[39]){x+=5} if(k[40]){y+=5} // 节流发送坐标 const now = Date.now() const deltaX = Math.abs(x - lastX) const deltaY = Math.abs(y - lastY) if(now - lastSendTime > 100 || (deltaX > 8 && deltaY > 8)){ pubnub.publish({ channel:"hello_world", message:{ content:ID, username:username, imgURL:imgURL, x:x,y:y } }) lastSendTime = now lastX = x lastY = y } // 绘制缓存的网格 const offsetX = Math.floor(x/32)*32 - x const offsetY = Math.floor(y/32)*32 - y ctx.drawImage(gridCanvas, offsetX, offsetY) // 绘制用户 Object.values(obj).forEach(user => { if(!imageCache[user.imgURL]){ let img = document.createElement("img") img.src = user.imgURL imageCache[user.imgURL] = img } const drawX = user.x - x + innerWidth/2 const drawY = user.y - y + innerHeight/2 ctx.drawImage(imageCache[user.imgURL], drawX, drawY, 32, 32) ctx.lineWidth=4 ctx.strokeStyle="rgba(0,0,0,1)" ctx.strokeRect(drawX, drawY, 32, 32) }) } pubnub = new PubNub({ publishKey:'demo', subscribeKey:'demo', uuid:"myUniqueUUID" }) pubnub.addListener({ status:(statusEvent)=>{ if(statusEvent.category==="PNConnectedCategory"){ // 已在onSignIn启动render,这里可省略 } }, message:(msg)=>{ obj[msg.message.content]={username:msg.message.username,imgURL:msg.message.imgURL,x:msg.message.x,y:msg.message.y} }, presence:(presenceEvent)=>{ if(presenceEvent.action === 'leave' || presenceEvent.action === 'timeout'){ delete obj[presenceEvent.uuid] } } }) pubnub.subscribe({ channels:['hello_world'] }) </script>
这些改完后,帧率应该能回到正常的60fps左右,网络延迟也会大幅降低。如果还有问题,可以再排查下PubNub的配置,或者考虑用WebAssembly做更复杂的计算优化~
内容的提问来源于stack exchange,提问作者user9861020




