我正在为我正在开发的游戏开发第三人称角色控制。到目前为止,我对结果感到满意。角色控件有很多简洁的功能,例如:如果一个物体在相机前面,它会向前移动,所以你仍然可以看到角色,但是当我将它旋转到一边然后将我的玩家转开时,相机会严重卡顿从中。我在 JSFiddle 上上传了一个测试:http: //jsfiddle.net/nA8SV/我只在 chrome 中测试过这个,由于某种原因,结果部分没有得到按键,直到你点击与画布相邻的空白区域框架。
[我也开始使用“--disable-web-security”来忽略跨源]
但是,一旦您单击页面,按键就会起作用。控件是轨道控件的修改版本。因此,您可以左键单击并旋转视图。此外,您可以使用 wasd 键四处移动,当您移动/旋转时,相机视图应返回到玩家身后。
对于在 JSFiddle 上工作非常困难的问题,我深表歉意。(但旋转错误正在发生,所以它至少重现了这一点。)
基本上我试图让我的相机旋转回到我的角色后面,所以我有一些代码可以修复第 250 行的旋转,但是当角色移动时相机会卡住。
以下是我的理论,我认为相机的整体抖动与玩家轻微反弹的物理模拟有关,但我不确定如何解决这个问题,任何帮助将不胜感激。
这是完整的代码,但我会推荐 JSFiddle 链接,我更容易看到它工作。
THREE.PlayerControls = function (anchor, scene, player, camera, domElement) {
this.walking = false;
this.occ = false;
this.scene = scene;
this.occLastZoom = 0;
this.jumpRelease = true;
this.jumping = false;
this.falling = false;
this.moving = false;
this.turning = false;
this.anchor = anchor;
this.player = player;
this.camera = camera;
this.camera.position.set(0, 8.25, -20);
this.domElement = (domElement !== undefined) ? domElement : document;
this.anchor.add(this.camera);
// API
this.enabled = true;
this.center = new THREE.Vector3(0, 4, 0);
this.userZoom = true;
this.userZoomSpeed = 2.0;
this.userRotate = true;
this.userRotateSpeed = 1.0;
this.minPolarAngle = 0; // radians
this.maxPolarAngle = Math.PI; // radians
this.minDistance = 2;
this.maxDistance = 30;
this.keys = {
LEFT: 65,
STRAFFLEFT: 81,
UP: 87,
RIGHT: 68,
STRAFFRIGHT: 69,
DOWN: 83,
JUMP: 32,
SLASH: 191
};
// internals
var scope = this;
var EPS = 0.000001;
var PIXELS_PER_ROUND = 1800;
var rotateStart = new THREE.Vector2();
var rotateEnd = new THREE.Vector2();
var rotateDelta = new THREE.Vector2();
var zoomStart = new THREE.Vector2();
var zoomEnd = new THREE.Vector2();
var zoomDelta = new THREE.Vector2();
var phiDelta = 0;
var thetaDelta = 0;
var scale = 1;
var lastPosition = new THREE.Vector3();
var STATE = {
NONE: -1,
ROTATE: 0,
ZOOM: 1
};
var state = STATE.NONE;
var key_state = [];
// events
var changeEvent = {
type: 'change'
};
this.rotateLeft = function (angle) {
thetaDelta -= angle;
};
this.rotateRight = function (angle) {
thetaDelta += angle;
};
this.rotateUp = function (angle) {
phiDelta -= angle;
};
this.rotateDown = function (angle) {
phiDelta += angle;
};
this.zoomIn = function (zoomScale) {
if (zoomScale === undefined) {
zoomScale = getZoomScale();
}
scale /= zoomScale;
};
this.zoomOut = function (zoomScale) {
if (zoomScale === undefined) {
zoomScale = getZoomScale();
}
scale *= zoomScale;
};
this.update = function (delta) {
// detect falling
if (this.scene.children.length > 0) {
var originPoint = this.anchor.position.clone();
var ray = new THREE.Raycaster(originPoint, new THREE.Vector3(0, -1, 0));
var collisionResults = ray.intersectObjects(this.scene.children.filter(function (child) {
return child.occ;
}));
if (collisionResults.length > 0) {
if (collisionResults[0].distance < 1.25 && this.falling) {
this.falling = false;
this.jumping = false;
} else if (collisionResults[0].distance > 2 + (this.jumping ? 1 : 0) && !this.falling) {
this.falling = true;
}
}
}
// handle movement
if (!this.falling) {
if (key_state.indexOf(this.keys.JUMP) > -1 && this.jumpRelease && !this.jumping) {
// jump
var lv = this.anchor.getLinearVelocity();
this.anchor.setLinearVelocity(new THREE.Vector3(lv.x, 15, lv.z));
this.jumpRelease = false;
this.jumping = true;
//jump
} else if (!this.jumping) {
// move
if (key_state.indexOf(this.keys.UP) > -1) {
var rotation_matrix = new THREE.Matrix4().extractRotation(this.anchor.matrix);
var speed = this.walking ? 2.5 : 10;
var force_vector;
// straffing?
if (key_state.indexOf(this.keys.STRAFFLEFT) > -1 && key_state.indexOf(this.keys.STRAFFRIGHT) < 0) {
force_vector = new THREE.Vector3((2 * speed / 3), 0, (2 * speed / 3)).applyMatrix4(rotation_matrix);
this.player.rotation.set(0, Math.PI / 4, 0);
} else if (key_state.indexOf(this.keys.STRAFFRIGHT) > -1) {
force_vector = new THREE.Vector3((-2 * speed / 3), 0, (2 * speed / 3)).applyMatrix4(rotation_matrix);
this.player.rotation.set(0, -Math.PI / 4, 0);
} else {
force_vector = new THREE.Vector3(0, 0, speed).applyMatrix4(rotation_matrix);
this.player.rotation.set(0, 0, 0);
}
this.anchor.setLinearVelocity(force_vector);
this.moving = true;
// forward
} else if (key_state.indexOf(this.keys.DOWN) > -1) {
var rotation_matrix = new THREE.Matrix4().extractRotation(this.anchor.matrix);
var speed = this.walking ? -2.5 : -5;
var force_vector;
// straffing?
if (key_state.indexOf(this.keys.STRAFFLEFT) > -1 && key_state.indexOf(this.keys.STRAFFRIGHT) < 0) {
force_vector = new THREE.Vector3((-2 * speed / 3), 0, (2 * speed / 3)).applyMatrix4(rotation_matrix);
this.player.rotation.set(0, -Math.PI / 4, 0);
} else if (key_state.indexOf(this.keys.STRAFFRIGHT) > -1) {
force_vector = new THREE.Vector3((2 * speed / 3), 0, (2 * speed / 3)).applyMatrix4(rotation_matrix);
this.player.rotation.set(0, Math.PI / 4, 0);
} else {
force_vector = new THREE.Vector3(0, 0, speed).applyMatrix4(rotation_matrix);
this.player.rotation.set(0, 0, 0);
}
this.anchor.setLinearVelocity(force_vector);
this.moving = true;
//back
} else if (key_state.indexOf(this.keys.STRAFFLEFT) > -1) {
var rotation_matrix = new THREE.Matrix4().extractRotation(this.anchor.matrix);
var speed = this.walking ? 2.5 : 10;
var force_vector = new THREE.Vector3(speed, 0, 0).applyMatrix4(rotation_matrix);
this.player.rotation.set(0, Math.PI / 2, 0);
this.anchor.setLinearVelocity(force_vector);
this.moving = true;
//straff
} else if (key_state.indexOf(this.keys.STRAFFRIGHT) > -1) {
var rotation_matrix = new THREE.Matrix4().extractRotation(this.anchor.matrix);
var speed = this.walking ? 2.5 : 10;
var force_vector = new THREE.Vector3(-speed, 0, 0).applyMatrix4(rotation_matrix);
this.player.rotation.set(0, -Math.PI / 2, 0);
this.anchor.setLinearVelocity(force_vector);
this.moving = true;
//straff
} else if (this.moving) {
this.player.rotation.set(0, 0, 0);
this.anchor.setLinearVelocity(new THREE.Vector3(0, 0, 0));
this.moving = false;
}
//turn
if (key_state.indexOf(this.keys.LEFT) > -1 && key_state.indexOf(this.keys.RIGHT) < 0) {
this.anchor.setAngularVelocity(new THREE.Vector3(0, 1.5, 0));
this.turning = true;
//turning
} else if (key_state.indexOf(this.keys.RIGHT) > -1) {
this.anchor.setAngularVelocity(new THREE.Vector3(0, -1.5, 0));
this.turning = true;
//turning
} else if (this.turning) {
this.anchor.setAngularVelocity(new THREE.Vector3(0, 0, 0));
this.turning = false;
}
//idle
}
if (key_state.indexOf(this.keys.JUMP) == -1) {
this.jumpRelease = true;
}
} else {
//falling
}
var position = this.camera.position;
var offset = position.clone().sub(this.center);
// angle from z-axis around y-axis
var theta = Math.atan2(offset.x, offset.z);
// angle from y-axis
var phi = Math.atan2(Math.sqrt(offset.x * offset.x + offset.z * offset.z), offset.y);
theta += thetaDelta;
phi += phiDelta;
if ((this.moving || this.turning) && state != STATE.ROTATE) {
// fix camera rotation
if (theta < 0) theta -= Math.max(delta, (-1 * Math.PI) + theta);
else theta += Math.min(delta, Math.PI - theta);
// fix pitch (should be an option or it could get anoying)
//phi = 9*Math.PI/24;
}
// restrict phi to be between desired limits
phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, phi));
// restrict phi to be betwee EPS and PI-EPS
phi = Math.max(EPS, Math.min(Math.PI - EPS, phi));
var radius;
if (this.occ) {
this.occLastZoom = Math.max(this.minDistance, Math.min(this.maxDistance, this.occLastZoom * scale));
radius = this.occLastZoom;
} else {
radius = offset.length() * scale;
}
// restrict radius to be between desired limits
radius = Math.max(this.minDistance, Math.min(this.maxDistance, radius));
// check for objects infront of camera
var projector = new THREE.Projector();
var vector = new THREE.Vector3(0, 0, 1);
projector.unprojectVector(vector, camera);
var point = new THREE.Vector3(this.anchor.position.x + this.center.x, this.anchor.position.y + this.center.y, this.anchor.position.z + this.center.z);
var vec = camera.position.clone().sub(vector).normalize()
var checkray = new THREE.Raycaster(point, vec, this.minDistance, this.maxDistance);
var checkcollisionResults = checkray.intersectObjects(this.scene.children.filter(function (child) {
return child.occ;
}));
if (checkcollisionResults.length > 0) {
var min = radius;
for (var i = 0; i < checkcollisionResults.length; i++) {
if (min > checkcollisionResults[i].distance) min = checkcollisionResults[i].distance;
}
if (min < radius) {
if (!this.occ) {
this.occ = true;
this.occLastZoom = radius;
}
radius = min;
} else {
this.occ = false;
}
}
offset.x = radius * Math.sin(phi) * Math.sin(theta);
offset.y = radius * Math.cos(phi);
offset.z = radius * Math.sin(phi) * Math.cos(theta);
if (radius < 5) {
this.player.material.opacity = Math.max(0, radius / 5.0);
this.center.y = 4 + ((5 - radius) / 2.5);
} else {
if (this.player.material.opacity != 1.0) {
this.player.material.opacity = 1.0;
this.center.y = 4;
}
}
position.copy(this.center).add(offset);
this.camera.lookAt(this.center);
thetaDelta = 0;
phiDelta = 0;
scale = 1;
if (lastPosition.distanceTo(this.camera.position) > 0) {
this.dispatchEvent(changeEvent);
lastPosition.copy(this.camera.position);
}
};
function roundTothree(num) {
return +(Math.round(num + "e+3") + "e-3");
}
function getZoomScale() {
return Math.pow(0.95, scope.userZoomSpeed);
}
function onMouseDown(event) {
if (scope.enabled === false) return;
if (scope.userRotate === false) return;
event.preventDefault();
if (state === STATE.NONE) {
if (event.button === 0) state = STATE.ROTATE;
}
if (state === STATE.ROTATE) {
rotateStart.set(event.clientX, event.clientY);
}
document.addEventListener('mousemove', onMouseMove, false);
document.addEventListener('mouseup', onMouseUp, false);
}
function onMouseMove(event) {
if (scope.enabled === false) return;
event.preventDefault();
if (state === STATE.ROTATE) {
rotateEnd.set(event.clientX, event.clientY);
rotateDelta.subVectors(rotateEnd, rotateStart);
scope.rotateLeft(2 * Math.PI * rotateDelta.x / PIXELS_PER_ROUND * scope.userRotateSpeed);
scope.rotateUp(2 * Math.PI * rotateDelta.y / PIXELS_PER_ROUND * scope.userRotateSpeed);
rotateStart.copy(rotateEnd);
} else if (state === STATE.ZOOM) {
zoomEnd.set(event.clientX, event.clientY);
zoomDelta.subVectors(zoomEnd, zoomStart);
if (zoomDelta.y > 0) {
scope.zoomIn();
} else {
scope.zoomOut();
}
zoomStart.copy(zoomEnd);
}
}
function onMouseUp(event) {
if (scope.enabled === false) return;
if (scope.userRotate === false) return;
document.removeEventListener('mousemove', onMouseMove, false);
document.removeEventListener('mouseup', onMouseUp, false);
state = STATE.NONE;
}
function onMouseWheel(event) {
if (scope.enabled === false) return;
if (scope.userZoom === false) return;
var delta = 0;
if (event.wheelDelta) { // WebKit / Opera / Explorer 9
delta = event.wheelDelta;
} else if (event.detail) { // Firefox
delta = -event.detail;
}
if (delta > 0) {
scope.zoomOut();
} else {
scope.zoomIn();
}
}
function onKeyDown(event) {
console.log('onKeyDown')
if (scope.enabled === false) return;
switch (event.keyCode) {
case scope.keys.UP:
var index = key_state.indexOf(scope.keys.UP);
if (index == -1) key_state.push(scope.keys.UP);
break;
case scope.keys.DOWN:
var index = key_state.indexOf(scope.keys.DOWN);
if (index == -1) key_state.push(scope.keys.DOWN);
break;
case scope.keys.LEFT:
var index = key_state.indexOf(scope.keys.LEFT);
if (index == -1) key_state.push(scope.keys.LEFT);
break;
case scope.keys.STRAFFLEFT:
var index = key_state.indexOf(scope.keys.STRAFFLEFT);
if (index == -1) key_state.push(scope.keys.STRAFFLEFT);
break;
case scope.keys.RIGHT:
var index = key_state.indexOf(scope.keys.RIGHT);
if (index == -1) key_state.push(scope.keys.RIGHT);
break;
case scope.keys.STRAFFRIGHT:
var index = key_state.indexOf(scope.keys.STRAFFRIGHT);
if (index == -1) key_state.push(scope.keys.STRAFFRIGHT);
break;
case scope.keys.JUMP:
var index = key_state.indexOf(scope.keys.JUMP);
if (index == -1) key_state.push(scope.keys.JUMP);
break;
}
}
function onKeyUp(event) {
switch (event.keyCode) {
case scope.keys.UP:
var index = key_state.indexOf(scope.keys.UP);
if (index > -1) key_state.splice(index, 1);
break;
case scope.keys.DOWN:
var index = key_state.indexOf(scope.keys.DOWN);
if (index > -1) key_state.splice(index, 1);
break;
case scope.keys.LEFT:
var index = key_state.indexOf(scope.keys.LEFT);
if (index > -1) key_state.splice(index, 1);
break;
case scope.keys.STRAFFLEFT:
var index = key_state.indexOf(scope.keys.STRAFFLEFT);
if (index > -1) key_state.splice(index, 1);
break;
case scope.keys.RIGHT:
var index = key_state.indexOf(scope.keys.RIGHT);
if (index > -1) key_state.splice(index, 1);
break;
case scope.keys.STRAFFRIGHT:
var index = key_state.indexOf(scope.keys.STRAFFRIGHT);
if (index > -1) key_state.splice(index, 1);
break;
case scope.keys.JUMP:
var index = key_state.indexOf(scope.keys.JUMP);
if (index > -1) key_state.splice(index, 1);
break;
case scope.keys.SLASH:
scope.walking = !scope.walking;
break;
}
}
this.domElement.addEventListener('contextmenu', function (event) {
event.preventDefault();
}, false);
this.domElement.addEventListener('mousedown', onMouseDown, false);
this.domElement.addEventListener('mousewheel', onMouseWheel, false);
this.domElement.addEventListener('DOMMouseScroll', onMouseWheel, false); // firefox
window.addEventListener('keydown', onKeyDown, false);
window.addEventListener('keyup', onKeyUp, false);
};
THREE.PlayerControls.prototype = Object.create(THREE.EventDispatcher.prototype);
// end player controlls
Physijs.scripts.worker = 'https://rawgithub.com/chandlerprall/Physijs/master/physijs_worker.js';
Physijs.scripts.ammo = 'http://chandlerprall.github.io/Physijs/examples/js/ammo.js';
// standard global variables
var container, scene, camera, renderer, controls;
//var keyboard = new THREEx.KeyboardState();
var clock = new THREE.Clock();
// MAIN //
window.onload = function() {
console.log('loaded')
// SCENE //
scene = new Physijs.Scene();
scene.setGravity(new THREE.Vector3(0, -32, 0));
scene.addEventListener(
'update',
function () {
scene.simulate();
});
// CAMERA //
var SCREEN_WIDTH = window.innerWidth,
SCREEN_HEIGHT = window.innerHeight;
var VIEW_ANGLE = 45,
ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT,
NEAR = 1,
FAR = 1000;
camera = new THREE.PerspectiveCamera(VIEW_ANGLE, ASPECT, NEAR, FAR);
// RENDERER //
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.shadowMapEnabled = true;
// to antialias the shadow
renderer.shadowMapSoft = true;
renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
container = document.getElementById('container');
container.appendChild(renderer.domElement);
// EVENTS //
//THREEx.WindowResize(renderer, camera);
// LIGHT //
var hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);
hemiLight.color.setHSL(0.6, 1, 0.6);
hemiLight.groundColor.setHSL(0.095, 1, 0.75);
hemiLight.position.set(0, 500, 0);
scene.add(hemiLight);
var light = new THREE.DirectionalLight(0xffffff, 1);
light.color.setHSL(0.1, 1, 0.95);
light.position.set(-1, 1.75, 1);
light.position.multiplyScalar(50);
light.castShadow = true;
light.shadowMapWidth = 2048;
light.shadowMapHeight = 2048;
light.shadowDarkness = 0.5;
var d = 50;
light.shadowCameraLeft = -d;
light.shadowCameraRight = d;
light.shadowCameraTop = d;
light.shadowCameraBottom = -d;
light.shadowCameraFar = 3500;
light.shadowBias = -0.0001;
light.shadowDarkness = 0.35;
scene.add(light);
// GEOMETRY //
var checkerboard = new THREE.ImageUtils.loadTexture('http://www.cns.nyu.edu/lcv/texture/artificial-periodic/checkerboard.o.jpg');
checkerboard.wrapS = checkerboard.wrapT = THREE.RepeatWrapping;
checkerboard.repeat.set(4, 4);
var checkerboard2 = new THREE.ImageUtils.loadTexture('http://www.cns.nyu.edu/lcv/texture/artificial-periodic/checkerboard.o.jpg');
var cubeMaterial = Physijs.createMaterial(
new THREE.MeshLambertMaterial({
map: checkerboard2
}),
1.0, // high friction
0.0 // low restitution
);
var cubeGeometry = new THREE.CubeGeometry(10, 5, 10, 1, 1, 1);
var cube = new Physijs.BoxMesh(
cubeGeometry,
cubeMaterial,
1);
cube.position.set(-10, 1, -10);
cube.castShadow = true;
cube.receiveShadow = true;
cube.occ = true;
scene.add(cube);
var cubeMaterial2 = Physijs.createMaterial(
new THREE.MeshLambertMaterial({
map: checkerboard2
}),
1.0, // high friction
0.0 // low restitution
);
var cubeGeometry2 = new THREE.CubeGeometry(10, 5, 10, 1, 1, 1);
var cube2 = new Physijs.BoxMesh(
cubeGeometry2,
cubeMaterial2,
1);
cube2.position.set(-10, 7, -1);
cube2.castShadow = true;
cube2.receiveShadow = true;
cube2.occ = true;
scene.add(cube2);
var cubeMaterial3 = Physijs.createMaterial(
new THREE.MeshLambertMaterial({
map: checkerboard2
}),
1.0, // high friction
0.0 // low restitution
);
var cubeGeometry3 = new THREE.CubeGeometry(10, 5, 10, 1, 1, 1);
var cube3 = new Physijs.BoxMesh(
cubeGeometry3,
cubeMaterial3,
1);
cube3.position.set(-10, 13, 8);
cube3.castShadow = true;
cube3.receiveShadow = true;
cube3.occ = true;
scene.add(cube3);
var cone = new Physijs.ConeMesh(
new THREE.CylinderGeometry(0, 5, 4, 30, 30, true),
Physijs.createMaterial(
new THREE.MeshLambertMaterial({
map: checkerboard2
}),
1.0, // high friction
0.0 // low restitution
),
0);
cone.position.set(0, 2, 0);
scene.castShadow = true;
scene.receiveShadow = true;
cone.occ = true;
scene.add(cone);
// FLOOR //
var floorMaterial = new THREE.MeshLambertMaterial({
map: checkerboard
});
var floorGeometry = new THREE.PlaneGeometry(100, 100, 1, 1);
var floor = new Physijs.PlaneMesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.castShadow = false;
floor.receiveShadow = true;
floor.occ = true;
scene.add(floor);
// SKY //
var skyBoxGeometry = new THREE.CubeGeometry( 1000, 1000, 1000 );
var skyBox = new THREE.Mesh(skyBoxGeometry, new THREE.MeshLambertMaterial({
color: '#3333bb'
}));
scene.add(skyBox);
// fog must be added to scene before first render
scene.fog = new THREE.FogExp2(0x999999, 0.001);
var bounding = new Physijs.SphereMesh(
new THREE.SphereGeometry(0.75, 4, 4),
Physijs.createMaterial(
new THREE.MeshBasicMaterial({
color: '#ff0000'
}),
1.0, // high friction
0.0 // low restitution
),
0.1);
var player = new THREE.Mesh(
new THREE.CubeGeometry(1, 6, 1, 1, 1, 1),
new THREE.MeshLambertMaterial({
color: '#00ff00'
}),
1);
player.position.set(0, 3, 0);
bounding.position.set(10, 0.75, -10);
bounding.add(player);
scene.add(bounding);
bounding.setAngularFactor(new THREE.Vector3(0, 0, 0));
controls = new THREE.PlayerControls(bounding, scene, player, camera, renderer.domElement);
// animation loop / game loop
scene.simulate();
animate();
};
function animate() {
requestAnimationFrame(animate);
render();
update();
}
function update() {
// delta = change in time since last call (in seconds)
var delta = clock.getDelta();
THREE.AnimationHandler.update(delta);
if (controls) controls.update(delta);
}
function render() {
renderer.render(scene, camera);
}
谢谢!!!