| name | galacean-mobile |
| description | Galacean Engine 移动端游戏开发指南,包含适配、优化和最佳实践 |
Galacean 移动端游戏开发
1. 视口配置(关键)
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
完整 HTML 头部配置:
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="format-detection" content="telephone=no, date=no, address=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#1a1a2e">
</head>
2. CSS 移动端适配
* {
-webkit-tap-highlight-color: transparent;
touch-action: none;
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
}
html, body {
overscroll-behavior: none;
-webkit-overflow-scrolling: none;
width: 100%;
height: 100%;
overflow: hidden;
}
#ui {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
.responsive-text {
font-size: clamp(14px, 4vw, 18px);
}
3. 移动端检测
const isMobile = /iPhone|iPad|iPod|Android|webOS|BlackBerry|Windows Phone/i.test(navigator.userAgent);
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isPortrait = window.innerHeight > window.innerWidth;
4. 相机适配
基础适配
function setupMobileCamera(camera: Camera): void {
const aspect = window.innerWidth / window.innerHeight;
if (aspect < 1) {
camera.orthographicSize = 9;
} else {
camera.orthographicSize = 9 / aspect;
}
camera.aspectRatio = aspect;
}
window.addEventListener('orientationchange', () => {
setTimeout(() => {
setupMobileCamera(camera);
}, 100);
});
基于内容尺寸的精确适配(推荐)
根据游戏内容实际尺寸和视窗大小动态计算相机视野,确保内容完整显示:
const BOARD_WIDTH = 6;
const BOARD_HEIGHT = 6;
const PADDING = 0.9;
function calculateOrthoSize(): number {
const aspect = window.innerWidth / window.innerHeight;
if (aspect < 1) {
const heightBasedSize = BOARD_HEIGHT / 2 / PADDING;
const widthBasedSize = BOARD_WIDTH / 2 / aspect / PADDING;
return Math.max(heightBasedSize, widthBasedSize);
} else {
return BOARD_HEIGHT / 2 / PADDING;
}
}
const camera = cameraEntity.addComponent(Camera);
camera.isOrthographic = true;
camera.orthographicSize = calculateOrthoSize();
camera.aspectRatio = window.innerWidth / window.innerHeight;
window.addEventListener('resize', () => {
camera.orthographicSize = calculateOrthoSize();
camera.aspectRatio = window.innerWidth / window.innerHeight;
});
关键点:
- 必须设置
camera.isOrthographic = true,否则 orthographicSize 不会生效
- 竖屏时窄屏幕设备(如手机)需要同时检查宽高,防止内容被截断
PADDING 系数控制四周留白,建议 0.85-0.95
5. 触摸事件处理
禁止默认行为
document.addEventListener('touchmove', (e) => {
e.preventDefault();
}, { passive: false });
let lastTouchEnd = 0;
document.addEventListener('touchend', (e) => {
const now = Date.now();
if (now - lastTouchEnd <= 300) {
e.preventDefault();
}
lastTouchEnd = now;
}, { passive: false });
document.addEventListener('contextmenu', (e) => e.preventDefault());
触摸反馈
class MobileButton extends Script {
private onTouchStart(): void {
this.entity.transform.setScale(0.95, 0.95, 1);
}
private onTouchEnd(): void {
this.entity.transform.setScale(1, 1, 1);
}
onAwake(): void {
const canvas = document.getElementById('canvas');
canvas?.addEventListener('touchstart', () => this.onTouchStart(), { passive: true });
canvas?.addEventListener('touchend', () => this.onTouchEnd(), { passive: true });
}
}
6. 性能优化
DPR 限制
const dpr = Math.min(window.devicePixelRatio || 1, 2);
engine.canvas.resizeByClientSize(dpr);
动画优化
const animationDuration = isMobile ? 0.2 : 0.3;
const MAX_CONCURRENT_ANIMATIONS = isMobile ? 10 : 20;
7. 横竖屏适配
CSS 媒体查询
@media screen and (orientation: portrait) {
#game-container {
width: 100vw;
height: 100vh;
}
}
@media screen and (orientation: landscape) {
#game-container {
width: 100vw;
height: 100vh;
}
}
@media screen and (orientation: landscape) and (max-height: 500px) {
#orientation-hint {
display: flex;
}
#game-container {
display: none;
}
}
横竖屏切换处理
class OrientationManager extends Script {
onAwake(): void {
this.checkOrientation();
window.addEventListener('orientationchange', () => {
setTimeout(() => this.checkOrientation(), 100);
});
}
private checkOrientation(): void {
const isPortrait = window.innerHeight > window.innerWidth;
const hint = document.getElementById('orientation-hint');
if (!isPortrait && window.innerHeight < 500) {
hint?.style.setProperty('display', 'flex');
} else {
hint?.style.setProperty('display', 'none');
}
}
}
8. UI 适配
响应式布局
function adaptUIForMobile(): void {
const width = window.innerWidth;
const isMobile = width < 768;
const scoreElement = document.getElementById('score');
if (scoreElement) {
scoreElement.style.fontSize = isMobile ? '16px' : '22px';
scoreElement.style.padding = isMobile ? '8px 16px' : '10px 30px';
}
}
触摸友好的按钮
.mobile-button {
min-width: 44px;
min-height: 44px;
padding: 12px 24px;
margin: 8px;
transition: transform 0.1s;
}
.mobile-button:active {
transform: scale(0.95);
}
9. 常见移动端问题
问题1:点击延迟
解决:设置正确的 viewport
<meta name="viewport" content="width=device-width, user-scalable=no">
问题2:iOS 橡皮筋效果
解决:
body {
overflow: hidden;
position: fixed;
width: 100%;
height: 100%;
}
问题3:刘海屏适配
解决:
#ui {
padding-top: max(10px, env(safe-area-inset-top));
padding-bottom: max(10px, env(safe-area-inset-bottom));
}
问题4:虚拟键盘
解决:
const initialHeight = window.innerHeight;
window.addEventListener('resize', () => {
const keyboardHeight = initialHeight - window.innerHeight;
if (keyboardHeight > 150) {
}
});
10. 测试清单
渲染系统职责分离
问题: 混用Canvas和HTML渲染UI导致定位混乱
原则: Canvas渲染游戏,HTML渲染UI,避免跨系统
private nextPreviewEntity: Entity;
private hudElement: HTMLElement;
private board: Entity[][];
private nextCells: HTMLDivElement[];
updatePreview() { cell.style.background = color; }
触摸控制布局
推荐: 左侧方向键,右侧功能键,分组间距24px防误触
#controls { display: flex; justify-content: space-between; gap: 24px; }
.control-group { display: grid; gap: 8px; }