You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于PubNub的类agar.io游戏卡顿延迟高的优化方案咨询

解决你的.io游戏帧率过低与网络延迟问题

嘿,我仔细啃了你的agar.io风格游戏代码,发现几个核心问题直接导致了帧率暴跌(300ms一帧)和网络延迟高的情况,咱们逐个击破:

一、帧率过低的核心原因与修复

1. 每帧重置Canvas尺寸,纯纯的性能浪费

你现在在render()里每帧都执行c.width=innerWidthc.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

火山引擎 最新活动