Vue3 + PlayCanvas 实战:3D 地图自由巡视闯关游戏开发
在前作基础之上,我们进一步拓展了开发难度,实现一款具备多关卡地图、角色移动、摄像机控制、小地图及闯关重置等核心功能的 3D 地图自由巡视游戏。本文将完整覆盖从基础交互到游戏化场景的开发全流程。
环境准备
- OS: Windows 11
- Browser: Chrome / Edge
- Node: v24.14.0
- NPM: 11.9.0
- Vue: 3.5.25
- Vite: 7.3.1
功能规划
本次开发需实现以下核心逻辑:
- 基础交互:方向键(↑↓←→)控制角色移动,WASD 键控制摄像机旋转;
- 视觉定制:自定义场景背景、地面、障碍物颜色(障碍物灰色、背景白色、每关地面差异化);
- 关卡系统:至少 3 张差异化地图,到达终点后按回车键进入下一关;
- 辅助功能:小地图实时显示角色、起点、终点、障碍物位置;
- 碰撞检测:角色与障碍物、地图边界碰撞,防止穿模;
- 通关判定:角色到达终点触发通关提示,支持回车重置关卡。
核心逻辑实现
脚本部分是游戏的核心,包含 PlayCanvas 初始化、关卡配置、角色移动、摄像机控制、碰撞检测、小地图绘制、通关判定等关键逻辑。
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import * as pc from 'playcanvas'
// 游戏状态管理
const currentLevel = ref(1) // 当前关卡
const isWon = ref(false) // 是否通关
let app = null // PlayCanvas 应用实例
let player = null // 玩家实体
let startPoint = null // 起点实体
let endPoint = null // 终点实体
let cameraEntity =
globalEnterHandler =
moveSpeed =
cameraRotateSpeed =
cameraYaw =
cameraPitch = -
cameraDistance =
minimapCanvas =
minimapCtx =
minimapScale =
obstacles = []
levelConfigs = [
{
: ,
: pc.(-, , -),
: pc.(, , ),
: ,
: [, , ],
: [, , ],
: [, , ]
},
{
: ,
: pc.(-, , ),
: pc.(, , ),
: ,
: [, , ],
: [, , ],
: [, , ]
},
{
: ,
: pc.(, , -),
: pc.(, , ),
: ,
: [, , ],
: [, , ],
: [, , ]
}
]
= () => {
(isWon.) {
.(, currentLevel.)
currentLevel. = currentLevel. % levelConfigs. +
isWon. =
()
.().()
}
}
() {
{
()
canvas = .()
(!canvas) {
.()
}
canvas.()
canvas.(, canvas.())
()
graphicsDevice = pc.(canvas, {
: ,
:
})
graphicsDevice. = .
app = pc.(canvas, {
: graphicsDevice,
: pc.(canvas),
: pc.(),
: ,
:
})
app.()
(!app.) app. = pc.(app.)
app.(pc.)
app.(pc.)
()
app.(, update)
app..(pc., goToNextLevel)
app..(pc., goToNextLevel)
app..(, goToNextLevel)
app..(, goToNextLevel)
globalEnterHandler = {
(e. === || e. === ) {
e.()
()
}
}
.(, globalEnterHandler)
.(, {
(app && app.) {
app.(., .)
app..(., .)
}
})
app.(., .)
} (error) {
.(, error)
}
}
() {
minimapCanvas = .()
minimapCtx = minimapCanvas.()
}
() {
(!minimapCtx || !player || !startPoint || !endPoint)
minimapCtx.(, , minimapCanvas., minimapCanvas.)
minimapCtx. =
minimapCtx.(, , minimapCanvas., minimapCanvas.)
centerX = minimapCanvas. /
centerY = minimapCanvas. /
minimapCtx. =
obstacles.( {
x = centerX + obs.. * minimapScale
y = centerY + obs.. * minimapScale
w = obs.. * minimapScale
h = obs.. * minimapScale
minimapCtx.(x - w / , y - h / , w, h)
})
minimapCtx. =
startX = centerX + startPoint.(). * minimapScale
startY = centerY + startPoint.(). * minimapScale
minimapCtx.()
minimapCtx.(startX, startY, , , . * )
minimapCtx.()
minimapCtx. =
endX = centerX + endPoint.(). * minimapScale
endY = centerY + endPoint.(). * minimapScale
minimapCtx.()
minimapCtx.(endX, endY, , , . * )
minimapCtx.()
minimapCtx. =
playerX = centerX + player.(). * minimapScale
playerY = centerY + player.(). * minimapScale
minimapCtx.()
minimapCtx.(playerX, playerY, , , . * )
minimapCtx.()
minimapCtx. =
minimapCtx. =
minimapCtx.()
minimapCtx.(playerX, playerY)
minimapCtx.(
playerX + .(player.(). * . / ) * ,
playerY + .(player.(). * . / ) *
)
minimapCtx.()
}
() {
(!player || obstacles. === )
playerRadius =
( i = ; i < obstacles.; i++) {
obs = obstacles[i]
obsMinX = obs.. - obs.. /
obsMaxX = obs.. + obs.. /
obsMinZ = obs.. - obs.. /
obsMaxZ = obs.. + obs.. /
playerMinX = nextPos. - playerRadius
playerMaxX = nextPos. + playerRadius
playerMinZ = nextPos. - playerRadius
playerMaxZ = nextPos. + playerRadius
xOverlap = playerMaxX > obsMinX && playerMinX < obsMaxX
zOverlap = playerMaxZ > obsMinZ && playerMinZ < obsMaxZ
(xOverlap && zOverlap)
}
currentConfig = levelConfigs[currentLevel. - ]
mapHalfSize = currentConfig. /
(.(nextPos.) > mapHalfSize - || .(nextPos.) > mapHalfSize - ) {
}
}
() {
obstacles = []
(app && app. && app... > ) {
children = [...app..]
children.( child.())
}
config = levelConfigs[currentLevel. - ] || levelConfigs[]
app.. = pc.(...config.)
ground = pc.()
ground.(, { : , : })
ground.(config., , config.)
ground.(, , )
ground.. = (...config.)
app..(ground)
( i = ; i < config.; i++) {
posX, posZ
validPos =
(!validPos) {
posX = (-config. / + , config. / - )
posZ = (-config. / + , config. / - )
distToStart = .(posX - config.., posZ - config..)
distToEnd = .(posX - config.., posZ - config..)
(distToStart > && distToEnd > ) validPos =
}
scaleX = (, )
scaleZ = (, )
obstacle = pc.()
obstacle.(, { : , : })
obstacle.(posX, scaleX / , posZ)
obstacle.(scaleX, scaleX, scaleZ)
obstacle.. = (...config.)
app..(obstacle)
obstacles.({
: { : posX, : posZ },
: { : scaleX, : scaleZ }
})
}
startPoint = pc.()
startPoint.(, { : , : })
startPoint.(config.)
startPoint.(, , )
startPoint.. = (, , )
app..(startPoint)
endPoint = pc.()
endPoint.(, { : , : })
endPoint.(config.)
endPoint.(, , )
endPoint.. = (, , )
app..(endPoint)
player = pc.()
player.(, { : , : })
player.(config.)
player.(, , )
player.. = (, , )
app..(player)
cameraEntity = pc.()
cameraEntity.(, {
: pc.(...config.),
: pc.,
: pc.
})
()
cameraEntity.(player.())
app..(cameraEntity)
()
}
() {
(!cameraEntity || !player)
yawRad = cameraYaw * . /
pitchRad = cameraPitch * . /
x = .(yawRad) * .(pitchRad) * cameraDistance
z = .(yawRad) * .(pitchRad) * cameraDistance
y = .(pitchRad) * cameraDistance +
playerPos = player.()
cameraEntity.(playerPos. + x, playerPos. + y, playerPos. + z)
cameraEntity.(playerPos)
}
() {
.() * (max - min) + min
}
() {
material = pc.()
material..(r, g, b)
material..(r * , g * , b * )
material.()
material
}
() {
(isWon. || !app || !player || !endPoint || !cameraEntity)
keyboard = app.
(keyboard.(pc.)) cameraYaw += cameraRotateSpeed * dt
(keyboard.(pc.)) cameraYaw -= cameraRotateSpeed * dt
(keyboard.(pc.)) {
cameraPitch += cameraRotateSpeed * dt
cameraPitch = .(cameraPitch, )
}
(keyboard.(pc.)) {
cameraPitch -= cameraRotateSpeed * dt
cameraPitch = .(cameraPitch, -)
}
()
moveStep = moveSpeed * dt
currentPos = player.()
newPos = pc.(currentPos., currentPos., currentPos.)
(keyboard.(pc.)) newPos. -= moveStep
(keyboard.(pc.)) newPos. += moveStep
(keyboard.(pc.)) newPos. -= moveStep
(keyboard.(pc.)) newPos. += moveStep
(!(newPos)) player.(newPos)
()
distanceToEnd = .(
player.(). - endPoint.().,
player.(). - endPoint.().
)
(distanceToEnd < ) {
isWon. =
.(, isWon.)
}
}
( () => {
()
})
( {
(app) {
app.(, update)
(app.) {
app..(pc., goToNextLevel)
app..(, goToNextLevel)
}
.(, {})
app.()
app =
}
(globalEnterHandler) {
.(, globalEnterHandler)
globalEnterHandler =
}
})
</script>


