移动端友好的网页游戏
移动端是最大的游戏平台。一些小调整就能让你的网页游戏在手机和平板上跑得很好。
1)视口设置
阻止缩放,并保证正常的尺寸适配:
html
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">2)全屏 canvas
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
touch-action: none; /* 阻止浏览器手势 */
}
#game {
display: block;
width: 100%;
height: 100%;
}3)触摸事件处理
js
const canvas = document.getElementById('game')
// 阻止默认触摸行为
canvas.addEventListener('touchstart', (e) => e.preventDefault(), { passive: false })
canvas.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false })
// 跟踪触点
const touches = new Map()
canvas.addEventListener('touchstart', (e) => {
for (const touch of e.changedTouches) {
touches.set(touch.identifier, {
x: touch.clientX,
y: touch.clientY,
startX: touch.clientX,
startY: touch.clientY,
})
}
})
canvas.addEventListener('touchmove', (e) => {
for (const touch of e.changedTouches) {
const t = touches.get(touch.identifier)
if (t) {
t.x = touch.clientX
t.y = touch.clientY
}
}
})
canvas.addEventListener('touchend', (e) => {
for (const touch of e.changedTouches) {
touches.delete(touch.identifier)
}
})4)虚拟摇杆
js
class VirtualJoystick {
constructor(x, y, radius) {
this.baseX = x
this.baseY = y
this.radius = radius
this.knobRadius = radius * 0.4
this.active = false
this.touchId = null
this.dx = 0
this.dy = 0
}
handleTouchStart(touch) {
const dist = Math.hypot(touch.clientX - this.baseX, touch.clientY - this.baseY)
if (dist < this.radius) {
this.active = true
this.touchId = touch.identifier
this.updatePosition(touch.clientX, touch.clientY)
}
}
handleTouchMove(touch) {
if (touch.identifier === this.touchId) {
this.updatePosition(touch.clientX, touch.clientY)
}
}
handleTouchEnd(touch) {
if (touch.identifier === this.touchId) {
this.active = false
this.touchId = null
this.dx = 0
this.dy = 0
}
}
updatePosition(x, y) {
let dx = x - this.baseX
let dy = y - this.baseY
const dist = Math.hypot(dx, dy)
if (dist > this.radius) {
dx = (dx / dist) * this.radius
dy = (dy / dist) * this.radius
}
this.dx = dx / this.radius
this.dy = dy / this.radius
}
draw(ctx) {
// 底盘
ctx.beginPath()
ctx.arc(this.baseX, this.baseY, this.radius, 0, Math.PI * 2)
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'
ctx.fill()
// 摇杆头
const knobX = this.baseX + this.dx * this.radius
const knobY = this.baseY + this.dy * this.radius
ctx.beginPath()
ctx.arc(knobX, knobY, this.knobRadius, 0, Math.PI * 2)
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'
ctx.fill()
}
getInput() {
return { x: this.dx, y: this.dy }
}
}5)触摸按钮
js
class TouchButton {
constructor(x, y, radius, label) {
this.x = x
this.y = y
this.radius = radius
this.label = label
this.pressed = false
this.touchId = null
}
contains(px, py) {
return Math.hypot(px - this.x, py - this.y) < this.radius
}
handleTouchStart(touch) {
if (this.contains(touch.clientX, touch.clientY)) {
this.pressed = true
this.touchId = touch.identifier
}
}
handleTouchEnd(touch) {
if (touch.identifier === this.touchId) {
this.pressed = false
this.touchId = null
}
}
draw(ctx) {
ctx.beginPath()
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
ctx.fillStyle = this.pressed ? 'rgba(255, 255, 255, 0.5)' : 'rgba(255, 255, 255, 0.2)'
ctx.fill()
ctx.font = 'bold 24px sans-serif'
ctx.fillStyle = '#fff'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(this.label, this.x, this.y)
}
}6)屏幕方向处理
js
function checkOrientation() {
const isLandscape = window.innerWidth > window.innerHeight
if (!isLandscape) {
showRotatePrompt()
} else {
hideRotatePrompt()
resizeCanvas()
}
}
window.addEventListener('resize', checkOrientation)
window.addEventListener('orientationchange', checkOrientation)
function showRotatePrompt() {
document.getElementById('rotate-prompt').style.display = 'flex'
}
function hideRotatePrompt() {
document.getElementById('rotate-prompt').style.display = 'none'
}7)性能优化
js
// 在低端设备降低分辨率
function getDeviceScale() {
const dpr = window.devicePixelRatio || 1
const isLowEnd = navigator.hardwareConcurrency <= 2
return isLowEnd ? Math.min(dpr, 1.5) : Math.min(dpr, 2)
}
// 限流昂贵操作
let lastHeavyUpdate = 0
function maybeDoHeavyWork(timestamp) {
if (timestamp - lastHeavyUpdate > 100) { // 重操作 10 fps
doExpensiveCalculations()
lastHeavyUpdate = timestamp
}
}
// 使用更低质量的资源
function getAssetQuality() {
const memory = navigator.deviceMemory || 4
if (memory <= 2) return 'low'
if (memory <= 4) return 'medium'
return 'high'
}8)电量考虑
js
// 检查电量并降低特效
async function checkBattery() {
if ('getBattery' in navigator) {
const battery = await navigator.getBattery()
if (battery.level < 0.2 && !battery.charging) {
enableBatterySaver()
}
battery.addEventListener('levelchange', () => {
if (battery.level < 0.2 && !battery.charging) {
enableBatterySaver()
}
})
}
}
function enableBatterySaver() {
game.particleCount = Math.floor(game.particleCount * 0.5)
game.targetFps = 30
console.log('Battery saver enabled')
}9)Fullscreen API
js
async function requestFullscreen() {
const elem = document.documentElement
try {
if (elem.requestFullscreen) {
await elem.requestFullscreen()
} else if (elem.webkitRequestFullscreen) {
await elem.webkitRequestFullscreen()
}
// 锁定方向
if (screen.orientation?.lock) {
await screen.orientation.lock('landscape')
}
} catch (err) {
console.warn('Fullscreen failed:', err)
}
}
// 在用户交互中调用
document.getElementById('play-btn').addEventListener('click', () => {
requestFullscreen()
startGame()
})10)测试清单
发布前:
- 触控响应——在真实设备上测,不要只用模拟器
- 各种屏幕尺寸——手机、平板、iPad Pro
- 两个方向——或锁定一个方向
- 低端设备——在 2-3 年前的手机上测
- 慢速网络——用 3G 模拟来测
- iOS Safari——音频、全屏有各种怪癖
- Android Chrome——最常见的浏览器
- 中断——电话、通知
- 内存压力——后台标签页切回来还能恢复吗?
js
// 触摸可视化调试
function drawTouchDebug(ctx) {
for (const [id, touch] of touches) {
ctx.beginPath()
ctx.arc(touch.x, touch.y, 30, 0, Math.PI * 2)
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
ctx.fill()
ctx.fillStyle = '#fff'
ctx.fillText(id.toString(), touch.x, touch.y)
}
}相关阅读
- 游戏输入处理
- 响应式游戏画布
- 离线游戏的 PWA
- 游戏的 Web Audio API —— 处理 iOS 音频限制
- 发布一个快速加载的网页游戏 —— 为移动网络优化加载时间
- Iframe 嵌入游戏 —— 移动端嵌入相关注意事项
外部资源
- MDN: Touch events —— 多点触控处理
- MDN: Screen Orientation API —— 锁定方向
- MDN: Fullscreen API —— 在移动端进入全屏
- web.dev: Responsive design —— 适用于游戏 UI 的响应式模式