crude implementation of a character controller
This commit is contained in:
parent
8350249591
commit
19ea3d22ab
|
@ -0,0 +1,183 @@
|
|||
import {
|
||||
Vector3,
|
||||
Vector2,
|
||||
PerspectiveCamera,
|
||||
Object3D,
|
||||
Quaternion,
|
||||
} from 'three';
|
||||
import { clamp, degToRad } from 'three/src/math/MathUtils.js';
|
||||
|
||||
export class ThirdPersonCamera {
|
||||
private currentPosition = new Vector3();
|
||||
private currentLookAt = new Vector3();
|
||||
private offsetFromPlayer = new Vector3();
|
||||
private mousePos = new Vector2();
|
||||
private prevMousePos = new Vector2();
|
||||
|
||||
private angleAroundPlayer = 180;
|
||||
private pitch = -45;
|
||||
private distance = 6;
|
||||
|
||||
private panning = false;
|
||||
private pinching = false;
|
||||
private locked = false;
|
||||
private previousPinchLength = 0;
|
||||
|
||||
private _moveFn?: (x: number, y: number) => void;
|
||||
|
||||
constructor(
|
||||
private camera: PerspectiveCamera,
|
||||
private target: Object3D,
|
||||
private eventTarget: HTMLElement
|
||||
) {}
|
||||
|
||||
private dragEvent = (x: number, y: number) => {
|
||||
this.prevMousePos.copy(this.mousePos);
|
||||
this.mousePos = this.mousePos.fromArray([x, y]);
|
||||
const offset = this.prevMousePos.clone().sub(this.mousePos);
|
||||
|
||||
if (this.panning) {
|
||||
if (this.locked && this._moveFn) {
|
||||
this._moveFn(offset.x, offset.y);
|
||||
} else {
|
||||
this.angleAroundPlayer =
|
||||
this.angleAroundPlayer + ((offset.x * 0.3) % 360);
|
||||
}
|
||||
|
||||
this.pitch = clamp(this.pitch + offset.y * 0.3, -90, 90);
|
||||
this.calculateCameraOffset();
|
||||
}
|
||||
};
|
||||
|
||||
events: Record<string, (...arg: any[]) => void> = {
|
||||
contextmenu: (e: MouseEvent) => e.preventDefault(),
|
||||
mousedown: (e: MouseEvent) => {
|
||||
if (e.button === 2) {
|
||||
this.panning = true;
|
||||
}
|
||||
},
|
||||
mouseup: (e: MouseEvent) => {
|
||||
const wasPanning = this.panning === true;
|
||||
if (e.button === 2) {
|
||||
this.panning = false;
|
||||
}
|
||||
|
||||
if (wasPanning && this.locked && this._moveFn && !this.panning) {
|
||||
this._moveFn(0, 0);
|
||||
}
|
||||
},
|
||||
mouseleave: (e: MouseEvent) => {
|
||||
const wasPanning = this.panning === true;
|
||||
this.panning = false;
|
||||
if (wasPanning && this.locked && this._moveFn && !this.panning) {
|
||||
this._moveFn(0, 0);
|
||||
}
|
||||
},
|
||||
mousemove: (e: MouseEvent) => this.dragEvent(e.clientX, e.clientY),
|
||||
wheel: (e: WheelEvent) => {
|
||||
e.deltaY < 0 ? (this.distance /= 1.2) : (this.distance *= 1.2);
|
||||
this.distance = clamp(this.distance, 3, 20);
|
||||
this.calculateCameraOffset();
|
||||
},
|
||||
// mobile
|
||||
touchstart: (ev: TouchEvent) => {
|
||||
ev.preventDefault();
|
||||
const touch = ev.touches[0] || ev.changedTouches[0];
|
||||
this.mousePos.fromArray([touch.pageX, touch.pageY]);
|
||||
this.panning = true;
|
||||
|
||||
if (ev.touches.length === 2) {
|
||||
this.pinching = true;
|
||||
}
|
||||
},
|
||||
touchmove: (ev: TouchEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (ev.touches.length === 2 && this.pinching) {
|
||||
const pinchLength = Math.hypot(
|
||||
ev.touches[0].pageX - ev.touches[1].pageX,
|
||||
ev.touches[0].pageY - ev.touches[1].pageY
|
||||
);
|
||||
|
||||
if (this.previousPinchLength) {
|
||||
const delta = pinchLength / this.previousPinchLength;
|
||||
delta > 0 ? (this.distance *= delta) : (this.distance /= delta);
|
||||
this.distance = clamp(this.distance, 3, 20);
|
||||
this.calculateCameraOffset();
|
||||
}
|
||||
|
||||
this.previousPinchLength = pinchLength;
|
||||
} else if (this.panning) {
|
||||
this.dragEvent(ev.touches[0].clientX, ev.touches[0].clientY);
|
||||
}
|
||||
},
|
||||
touchend: (ev: TouchEvent) => {
|
||||
const wasPanning = this.panning === true;
|
||||
this.pinching = false;
|
||||
this.panning = false;
|
||||
this.previousPinchLength = 0;
|
||||
|
||||
if (wasPanning && this.locked && this._moveFn && !this.panning) {
|
||||
this._moveFn(0, 0);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
initialize() {
|
||||
Object.keys(this.events).forEach((key) => {
|
||||
this.eventTarget.addEventListener(key, this.events[key]);
|
||||
});
|
||||
this.calculateCameraOffset();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
Object.keys(this.events).forEach((key) => {
|
||||
this.eventTarget.removeEventListener(key, this.events[key]);
|
||||
});
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
const offset = this.getTargetOffset();
|
||||
const lookAt = this.getTargetLookAt();
|
||||
|
||||
// https://www.youtube.com/watch?v=UuNPHOJ_V5o
|
||||
const factor = 1.0 - Math.pow(0.001, dt);
|
||||
|
||||
this.currentPosition.lerp(offset, factor);
|
||||
this.currentLookAt.lerp(lookAt, factor);
|
||||
|
||||
this.camera.position.copy(this.currentPosition);
|
||||
this.camera.lookAt(this.currentLookAt);
|
||||
}
|
||||
|
||||
public setLock(isLocked: boolean) {
|
||||
this.locked = isLocked;
|
||||
}
|
||||
|
||||
public registerAltMoveFunction(fn: (x: number, y: number) => void) {
|
||||
this._moveFn = fn;
|
||||
}
|
||||
|
||||
private calculateCameraOffset() {
|
||||
const hdist = this.distance * Math.cos(degToRad(this.pitch));
|
||||
const vdist = this.distance * Math.sin(degToRad(this.pitch));
|
||||
|
||||
this.offsetFromPlayer.set(
|
||||
hdist * Math.sin(degToRad(this.angleAroundPlayer)),
|
||||
-vdist,
|
||||
hdist * Math.cos(degToRad(this.angleAroundPlayer))
|
||||
);
|
||||
}
|
||||
|
||||
private getTargetOffset(): Vector3 {
|
||||
const offset = this.offsetFromPlayer.clone();
|
||||
offset.add(this.target.position);
|
||||
return offset;
|
||||
}
|
||||
|
||||
private getTargetLookAt(): Vector3 {
|
||||
const offset = new Vector3(0, 4.75, 0);
|
||||
offset.add(this.target.position);
|
||||
return offset;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,8 @@ import {
|
|||
import { GameEvents } from '../types/events';
|
||||
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { Vector3 } from 'three';
|
||||
import { Object3D, Vector3 } from 'three';
|
||||
import { ThirdPersonCamera } from './camera';
|
||||
|
||||
/**
|
||||
* Gameplay manager.
|
||||
|
@ -22,7 +23,7 @@ export class GameplayComponent extends EngineComponent {
|
|||
|
||||
public characters: Humanoid[] = [];
|
||||
|
||||
public controls!: OrbitControls;
|
||||
public controls!: ThirdPersonCamera;
|
||||
public character!: Humanoid;
|
||||
public movementSpeed = 16;
|
||||
public movement = {
|
||||
|
@ -32,31 +33,38 @@ export class GameplayComponent extends EngineComponent {
|
|||
right: 0,
|
||||
};
|
||||
|
||||
private move = new Vector3();
|
||||
private look = new Vector3();
|
||||
|
||||
override initialize(): void {
|
||||
this.world = this.renderer.scene.getObjectByName('World') as World;
|
||||
this.cleanUpEvents = this.bindEvents();
|
||||
this.controls = new OrbitControls(
|
||||
this.renderer.camera,
|
||||
this.renderer.renderer.domElement
|
||||
);
|
||||
this.controls.enablePan = false;
|
||||
|
||||
this.loadCharacter('Diamond');
|
||||
}
|
||||
|
||||
override update(delta: number): void {
|
||||
this.controls.update();
|
||||
this.controls?.update(delta);
|
||||
for (const character of this.characters) {
|
||||
character.tick(delta);
|
||||
}
|
||||
this.character?.setVelocity(
|
||||
new Vector3(
|
||||
this.movement.left + -this.movement.right,
|
||||
0,
|
||||
this.movement.forward + -this.movement.backward
|
||||
)
|
||||
);
|
||||
this.character?.getWorldPosition(this.controls.target);
|
||||
|
||||
this.move.set(0, 0, 0);
|
||||
this.look.setFromMatrixColumn(this.renderer.camera.matrix, 0);
|
||||
this.look.multiplyScalar(this.movement.forward + -this.movement.backward);
|
||||
this.look.crossVectors(this.renderer.camera.up, this.look);
|
||||
this.move.add(this.look);
|
||||
|
||||
this.look.setFromMatrixColumn(this.renderer.camera.matrix, 0);
|
||||
this.look.multiplyScalar(this.movement.right + -this.movement.left);
|
||||
this.move.add(this.look);
|
||||
|
||||
this.look.copy(this.move).normalize();
|
||||
this.character?.localToWorld(this.look);
|
||||
|
||||
this.character?.setVelocity(this.move);
|
||||
if (this.move.length()) {
|
||||
this.character?.setLook(this.move.clone().normalize());
|
||||
}
|
||||
}
|
||||
|
||||
override cleanUp(): void {
|
||||
|
@ -71,6 +79,13 @@ export class GameplayComponent extends EngineComponent {
|
|||
this.characters.push(ctrl);
|
||||
ctrl.initialize();
|
||||
this.character = ctrl;
|
||||
|
||||
this.controls = new ThirdPersonCamera(
|
||||
this.renderer.camera,
|
||||
char,
|
||||
this.renderer.renderer.domElement
|
||||
);
|
||||
this.controls.initialize();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
|
|
|
@ -30,6 +30,9 @@ export class Humanoid extends GameObject implements Ticking {
|
|||
private _health = 100;
|
||||
private _maxHealth = 100;
|
||||
private _velocity = new Vector3(0, 0, 0);
|
||||
private _lookAt = new Vector3(0, 0, 1);
|
||||
private _currentLookAt = new Vector3(0, 0, 1);
|
||||
private _animState = 0;
|
||||
public static bodyPartNames = [
|
||||
'Head',
|
||||
'Torso',
|
||||
|
@ -51,6 +54,8 @@ export class Humanoid extends GameObject implements Ticking {
|
|||
public static bodyAnimationNames = ['Idle', 'Walk'];
|
||||
|
||||
private mixer!: AnimationMixer;
|
||||
private idleAction!: AnimationAction;
|
||||
private walkAction!: AnimationAction;
|
||||
|
||||
@EditorProperty({ type: Number })
|
||||
get health() {
|
||||
|
@ -104,19 +109,43 @@ export class Humanoid extends GameObject implements Ticking {
|
|||
this.mixer = new AnimationMixer(this.parent);
|
||||
this.ready = true;
|
||||
|
||||
const clip = AnimationClip.findByName(this.parent.animations, 'Idle');
|
||||
const action = this.mixer.clipAction(clip);
|
||||
action.play();
|
||||
const idleClip = AnimationClip.findByName(this.parent.animations, 'Idle');
|
||||
const walkClip = AnimationClip.findByName(this.parent.animations, 'Walk');
|
||||
this.idleAction = this.mixer.clipAction(idleClip);
|
||||
this.walkAction = this.mixer.clipAction(walkClip);
|
||||
this.idleAction.play();
|
||||
}
|
||||
|
||||
setVelocity(velocity: Vector3) {
|
||||
this._velocity.copy(velocity);
|
||||
|
||||
this.setWalkAnimationState(!!this._velocity.length() ? 1 : 0);
|
||||
}
|
||||
|
||||
setLook(vector: Vector3) {
|
||||
this._lookAt.lerp(vector, 0.15);
|
||||
}
|
||||
|
||||
tick(dt: number): void {
|
||||
if (!this.ready) return;
|
||||
this.mixer.update(dt);
|
||||
this.parent?.position.add(this._velocity.clone().multiplyScalar(dt));
|
||||
|
||||
this._currentLookAt.copy(this.parent!.position);
|
||||
this._currentLookAt.add(this._lookAt);
|
||||
this.parent?.lookAt(this._currentLookAt);
|
||||
}
|
||||
|
||||
setWalkAnimationState(index: number) {
|
||||
const previousState = this._animState;
|
||||
this._animState = index;
|
||||
if (previousState === index) return;
|
||||
|
||||
if (index === 1) {
|
||||
this.walkAction.reset().crossFadeFrom(this.idleAction, 0.5, false).play();
|
||||
} else {
|
||||
this.walkAction.crossFadeTo(this.idleAction.reset(), 0.5, false).play();
|
||||
}
|
||||
}
|
||||
|
||||
detach(bodyPart?: HumanoidBodyPart) {
|
||||
|
|
Loading…
Reference in New Issue