第一人称自由漫游控制组件
功能介绍
通过第一人称自由漫游控制组件,能够更方便地进行自由漫游。实现通过键盘移动、鼠标控制方向的类第一人称游戏操作。
不妨通过代码示例在 Vue 中尝试一下:
在线演示
点击 在线链接 以查看在线演示。
组件代码示例
默认路径为 components/WanderMode/index.vue
vue
<template>
<div class="wander-mode">
<div class="wander-controls" v-show="showControls">
<div class="control-info">
<h3>第一人称漫游模式</h3>
<div v-if="!isPointerLocked" class="start-tip">
<p><strong>点击屏幕激活漫游控制</strong></p>
<p><small>鼠标将被锁定在窗口中心</small></p>
</div>
<div class="controls-guide">
<p><strong>鼠标:</strong>控制镜头方向</p>
<p><strong>WASD:</strong>前后左右移动</p>
<p><strong>Space:</strong>上升</p>
<p><strong>Shift:</strong>下降</p>
<p><strong>ESC:</strong>退出漫游/解锁鼠标</p>
</div>
</div>
<div class="speed-control">
<label>移动速度:</label>
<input
type="range"
min="1"
max="100"
v-model="moveSpeed"
class="speed-slider"
/>
<span>{{ moveSpeed }}</span>
</div>
<button @click="exitWanderMode" class="exit-btn">退出漫游</button>
</div>
<!-- 简化的状态显示 -->
<div class="wander-status" v-show="isWanderMode && !showControls">
<span class="status-text">漫游模式</span>
<button @click="toggleControls" class="toggle-btn">?</button>
</div>
<!-- 准星 -->
<div class="crosshair" v-if="isWanderMode">
<div class="crosshair-horizontal"></div>
<div class="crosshair-vertical"></div>
</div>
</div>
</template>
<style scoped>
.wander-mode {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
}
.wander-controls {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 10px;
pointer-events: auto;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.control-info h3 {
margin: 0 0 15px 0;
color: #4caf50;
font-size: 18px;
}
.start-tip {
background: rgba(76, 175, 80, 0.2);
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
border: 1px solid #4caf50;
}
.start-tip p {
margin: 0;
color: #4caf50;
font-weight: bold;
text-align: center;
}
.controls-guide p {
margin: 8px 0;
font-size: 14px;
line-height: 1.4;
}
.speed-control {
margin: 15px 0;
display: flex;
align-items: center;
gap: 10px;
}
.speed-control label {
font-size: 14px;
min-width: 80px;
}
.speed-slider {
flex: 1;
min-width: 100px;
}
.speed-control span {
min-width: 30px;
text-align: center;
font-weight: bold;
}
.exit-btn {
background: #f44336;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.exit-btn:hover {
background: #d32f2f;
}
.crosshair {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
}
.crosshair-horizontal,
.crosshair-vertical {
position: absolute;
background: rgba(255, 255, 255, 0.8);
border-radius: 1px;
}
.crosshair-horizontal {
width: 20px;
height: 2px;
top: 50%;
transform: translateY(-50%);
}
.crosshair-vertical {
width: 2px;
height: 20px;
left: 50%;
transform: translateX(-50%);
}
.wander-status {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 15px;
border-radius: 5px;
pointer-events: auto;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
gap: 10px;
}
.status-text {
font-size: 14px;
color: #4caf50;
}
.toggle-btn {
background: #4caf50;
color: white;
border: none;
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.toggle-btn:hover {
background: #45a049;
}
</style>
<script>
import * as Cesium from 'cesium'
export default {
name: 'WanderMode',
data () {
return {
isWanderMode: false,
moveSpeed: 10,
isPointerLocked: false,
showControls: false, // 默认隐藏详细控件
// 移动状态
moveState: {
forward: false,
backward: false,
left: false,
right: false,
up: false,
down: false
},
// 鼠标控制
mouseSensitivity: 0.002,
lastMouseX: 0,
lastMouseY: 0,
// 相机状态
cameraHeading: 0,
cameraPitch: 0,
// 事件监听器引用
keydownHandler: null,
keyupHandler: null,
mousemoveHandler: null,
clickHandler: null,
globalClickHandler: null,
globalMouseDownHandler: null,
globalMouseUpHandler: null,
globalPointerDownHandler: null,
globalPointerUpHandler: null,
// 动画循环
animationId: null
}
},
mounted () {
this.setupEventListeners()
},
beforeUnmount () {
this.cleanup()
},
methods: {
/**
* 开始漫游模式
*/
startWanderMode () {
if (this.isWanderMode) return
this.isWanderMode = true
// 保存当前相机状态
const camera = window.viewer.camera
this.cameraHeading = camera.heading
this.cameraPitch = camera.pitch
// 禁用默认相机控制
window.viewer.scene.screenSpaceCameraController.enableRotate = false
window.viewer.scene.screenSpaceCameraController.enableTranslate = false
window.viewer.scene.screenSpaceCameraController.enableZoom = false
window.viewer.scene.screenSpaceCameraController.enableTilt = false
window.viewer.scene.screenSpaceCameraController.enableLook = false
// 隐藏鼠标光标
// window.viewer.canvas.style.cursor = 'none'
// 设置指针锁定(可选)
this.setupPointerLock()
// 开始移动循环
this.startMovementLoop()
this.$emit('wander-started')
},
/**
* 退出漫游模式
*/
exitWanderMode () {
if (!this.isWanderMode) return
this.showControls = false;
this.isWanderMode = false
this.exitPointerLock()
// 恢复默认相机控制
window.viewer.scene.screenSpaceCameraController.enableRotate = true
window.viewer.scene.screenSpaceCameraController.enableTranslate = true
window.viewer.scene.screenSpaceCameraController.enableZoom = true
window.viewer.scene.screenSpaceCameraController.enableTilt = true
window.viewer.scene.screenSpaceCameraController.enableLook = true
// 恢复鼠标光标
window.viewer.canvas.style.cursor = 'default'
// 停止移动循环
this.stopMovementLoop()
// 重置移动状态
this.resetMoveState()
this.$emit('wander-ended')
},
/**
* 设置指针锁定
*/
setupPointerLock () {
const canvas = window.viewer.canvas
// 使用Pointer Lock API锁定鼠标
this.clickHandler = (event) => {
if (this.isWanderMode && !this.isPointerLocked) {
// 请求指针锁定
canvas.requestPointerLock = canvas.requestPointerLock ||
canvas.mozRequestPointerLock ||
canvas.webkitRequestPointerLock
if (canvas.requestPointerLock) {
canvas.requestPointerLock()
}
}
}
canvas.addEventListener('click', this.clickHandler)
// 添加多种鼠标事件监听器,用于在指针锁定状态下阻止所有鼠标事件
this.globalClickHandler = (event) => {
if (this.isPointerLocked) {
event.preventDefault()
event.stopPropagation()
return false
}
}
this.globalMouseDownHandler = (event) => {
if (this.isPointerLocked) {
event.preventDefault()
event.stopPropagation()
return false
}
}
this.globalMouseUpHandler = (event) => {
if (this.isPointerLocked) {
event.preventDefault()
event.stopPropagation()
return false
}
}
this.globalPointerDownHandler = (event) => {
if (this.isPointerLocked) {
event.preventDefault()
event.stopPropagation()
return false
}
}
this.globalPointerUpHandler = (event) => {
if (this.isPointerLocked) {
event.preventDefault()
event.stopPropagation()
return false
}
}
// 使用捕获阶段确保优先处理
document.addEventListener('click', this.globalClickHandler, true)
document.addEventListener('mousedown', this.globalMouseDownHandler, true)
document.addEventListener('mouseup', this.globalMouseUpHandler, true)
document.addEventListener('pointerdown', this.globalPointerDownHandler, true)
document.addEventListener('pointerup', this.globalPointerUpHandler, true)
// 监听指针锁定状态变化
document.addEventListener('pointerlockchange', this.onPointerLockChange.bind(this))
document.addEventListener('pointerlockerror', this.onPointerLockError.bind(this))
},
/**
* 退出指针锁定
*/
exitPointerLock () {
try {
if (document.pointerLockElement && document.exitPointerLock) {
document.exitPointerLock()
}
} catch (error) {
console.warn('Failed to exit pointer lock:', error)
}
this.isPointerLocked = false
},
/**
* 指针锁定状态变化处理
*/
onPointerLockChange () {
const canvas = window.viewer.canvas
this.isPointerLocked = document.pointerLockElement === canvas ||
document.mozPointerLockElement === canvas ||
document.webkitPointerLockElement === canvas
if (!this.isPointerLocked && this.isWanderMode) {
// 指针锁定丢失时显示提示,但不自动退出漫游模式
console.log('指针锁定已丢失,点击屏幕重新激活')
}
},
/**
* 指针锁定错误处理
*/
onPointerLockError () {
console.warn('Pointer lock failed')
this.exitWanderMode()
},
/**
* 设置事件监听器
*/
setupEventListeners () {
// 键盘事件
this.keydownHandler = (event) => {
if (!this.isWanderMode) return
switch (event.code) {
case 'KeyW':
this.moveState.forward = true
break
case 'KeyS':
this.moveState.backward = true
break
case 'KeyA':
this.moveState.left = true
break
case 'KeyD':
this.moveState.right = true
break
case 'Space':
event.preventDefault()
this.moveState.up = true
break
case 'ShiftLeft':
case 'ShiftRight':
this.moveState.down = true
break
case 'Escape':
if (this.isPointerLocked) {
// 如果指针锁定状态,先解锁鼠标
this.exitPointerLock()
} else {
// 如果没有指针锁定,退出漫游模式
this.exitWanderMode()
}
break
}
}
this.keyupHandler = (event) => {
if (!this.isWanderMode) return
switch (event.code) {
case 'KeyW':
this.moveState.forward = false
break
case 'KeyS':
this.moveState.backward = false
break
case 'KeyA':
this.moveState.left = false
break
case 'KeyD':
this.moveState.right = false
break
case 'Space':
this.moveState.up = false
break
case 'ShiftLeft':
case 'ShiftRight':
this.moveState.down = false
break
}
}
// 鼠标移动事件
this.mousemoveHandler = (event) => {
if (!this.isWanderMode) return
// 只有在指针锁定状态下才处理鼠标移动
if (!this.isPointerLocked) return
// 使用movementX/Y获取鼠标移动距离
const deltaX = event.movementX || 0
const deltaY = event.movementY || 0
// 更新相机方向(修复反转问题)
this.cameraHeading += deltaX * this.mouseSensitivity
this.cameraPitch -= deltaY * this.mouseSensitivity // 上下方向反转
// 限制俯仰角
this.cameraPitch = Math.max(-Cesium.Math.PI_OVER_TWO,
Math.min(Cesium.Math.PI_OVER_TWO, this.cameraPitch))
// 应用相机方向
this.updateCameraOrientation()
}
document.addEventListener('keydown', this.keydownHandler)
document.addEventListener('keyup', this.keyupHandler)
document.addEventListener('mousemove', this.mousemoveHandler)
},
/**
* 更新相机方向
*/
updateCameraOrientation () {
const camera = window.viewer.camera
camera.setView({
destination: camera.position,
orientation: {
heading: this.cameraHeading,
pitch: this.cameraPitch,
roll: 0.0
}
})
},
/**
* 开始移动循环
*/
startMovementLoop () {
let lastTime = 0
const move = (currentTime) => {
if (!this.isWanderMode) return
// 限制帧率,提高性能
if (currentTime - lastTime >= 16) { // 约60fps
this.updateCameraPosition()
lastTime = currentTime
}
this.animationId = requestAnimationFrame(move)
}
this.animationId = requestAnimationFrame(move)
},
/**
* 停止移动循环
*/
stopMovementLoop () {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
},
/**
* 更新相机位置
*/
updateCameraPosition () {
// 检查是否有任何移动输入
const hasMovement = this.moveState.forward || this.moveState.backward ||
this.moveState.left || this.moveState.right ||
this.moveState.up || this.moveState.down
if (!hasMovement) return // 没有移动输入时直接返回
const camera = window.viewer.camera
const speed = this.moveSpeed * 0.1
// 计算移动方向
let moveVector = new Cesium.Cartesian3(0, 0, 0)
// W/S 前后移动(修复方向)
if (this.moveState.forward || this.moveState.backward) {
const direction = this.moveState.forward ? 1 : -1
const forward = Cesium.Cartesian3.clone(camera.direction)
Cesium.Cartesian3.normalize(forward, forward)
const forwardMovement = Cesium.Cartesian3.multiplyByScalar(forward, speed * direction, new Cesium.Cartesian3())
moveVector = Cesium.Cartesian3.add(moveVector, forwardMovement, moveVector)
}
// A/D 左右移动
if (this.moveState.left || this.moveState.right) {
const direction = this.moveState.right ? 1 : -1
const right = Cesium.Cartesian3.clone(camera.right)
Cesium.Cartesian3.normalize(right, right)
const rightMovement = Cesium.Cartesian3.multiplyByScalar(right, speed * direction, new Cesium.Cartesian3())
moveVector = Cesium.Cartesian3.add(moveVector, rightMovement, moveVector)
}
if (this.moveState.up || this.moveState.down) {
const direction = this.moveState.up ? 1 : -1
const up = Cesium.Cartesian3.clone(camera.up)
Cesium.Cartesian3.normalize(up, up)
const upMovement = Cesium.Cartesian3.multiplyByScalar(up, speed * direction, new Cesium.Cartesian3())
moveVector = Cesium.Cartesian3.add(moveVector, upMovement, moveVector)
}
// 应用移动
if (!Cesium.Cartesian3.equals(moveVector, Cesium.Cartesian3.ZERO)) {
const newPosition = Cesium.Cartesian3.add(camera.position, moveVector, new Cesium.Cartesian3())
// 检查新位置是否在地面以下
const cartographic = Cesium.Cartographic.fromCartesian(newPosition)
const height = window.viewer.scene.globe.getHeight(cartographic) || 0
// 确保相机不会低于地面
if (cartographic.height < height + 1.8) { // 1.8米是人物高度
cartographic.height = height + 1.8
const adjustedPosition = Cesium.Cartesian3.fromCartographic(cartographic)
camera.setView({
destination: adjustedPosition,
orientation: {
heading: this.cameraHeading,
pitch: this.cameraPitch,
roll: 0.0
}
})
} else {
camera.setView({
destination: newPosition,
orientation: {
heading: this.cameraHeading,
pitch: this.cameraPitch,
roll: 0.0
}
})
}
}
},
/**
* 重置移动状态
*/
resetMoveState () {
this.moveState = {
forward: false,
backward: false,
left: false,
right: false,
up: false,
down: false
}
},
/**
* 切换控件显示
*/
toggleControls () {
this.showControls = !this.showControls
},
/**
* 清理事件监听器
*/
cleanup () {
if (this.keydownHandler) {
document.removeEventListener('keydown', this.keydownHandler)
}
if (this.keyupHandler) {
document.removeEventListener('keyup', this.keyupHandler)
}
if (this.mousemoveHandler) {
document.removeEventListener('mousemove', this.mousemoveHandler)
}
if (this.clickHandler) {
window.viewer.canvas.removeEventListener('click', this.clickHandler)
}
document.removeEventListener('pointerlockchange', this.onPointerLockChange.bind(this))
document.removeEventListener('pointerlockerror', this.onPointerLockError.bind(this))
this.stopMovementLoop()
if (this.isWanderMode) {
this.exitWanderMode()
}
}
}
}
</script>调用代码示例
vue
<template>
<div id="unicoreContainer">
<!-- 第一人称自由漫游控制组件窗口卡片开始 -->
<wdSet ref="wdSetId"></wdSet>
<!-- 第一人称自由漫游控制组件窗口卡片结束 -->
</div>
</template>
<script>
import { UniCore } from 'unicore-sdk'
import { config } from 'unicore-sdk/unicore.config'
import 'unicore-sdk/Widgets/widgets.css'
import wdSet from '@/components/WanderMode/index'; // 第一人称自由漫游控制组件
export default {
components: {
wdSet
},
// 生命周期 - 挂载完成(可以访问DOM元素)
mounted () {
this.init();
},
// 方法集合
methods: {
/**
* 通用图形引擎初始化
*/
init () {
// 初始化UniCore
// 目前采用Cesium的地形&底图数据,这里配置Cesium的token
let accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxNjEwMzI4My01MjBmLTQzYzktOGZiMS0wMDRhZjE0N2IyMGIiLCJpZCI6MTc1NzkyLCJpYXQiOjE3MTM3NzQ3OTh9.zU-R4MNvHr8rvn1v28PQfDImyutnpPF2lmEgGeSPckQ";
// 初始化unicore
let uniCore = new UniCore(config, accessToken);
uniCore.init("unicoreContainer");
window.uniCore = uniCore;
let viewer = uniCore.viewer;
// 视角初始化
uniCore.position.buildingPosition(viewer, [113.12380548015745, 28.250758831850005, 700], -20, -45, 1);
this.$refs.wdSetId.showControls = true;
if (this.$refs.wdSetId) {
this.$refs.wdSetId.startWanderMode();
}
}
}
}
</script>
<style scoped>
#unicoreContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: black;
}
</style>调用代码示例中的关键代码
暂无,只需直接引入组件即可。
