Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implements variable and double jumps and pane control settings #138

Merged
merged 3 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions example/assets/models/anim_double_jump.glb
Git LFS file not shown
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { AudioListener, Euler, Scene, Vector3 } from "three";

import hdrJpgUrl from "../../../assets/hdr/puresky_2k.jpg";
import airAnimationFileUrl from "../../../assets/models/anim_air.glb";
import doubleJumpAnimationFileUrl from "../../../assets/models/anim_double_jump.glb";
import idleAnimationFileUrl from "../../../assets/models/anim_idle.glb";
import jogAnimationFileUrl from "../../../assets/models/anim_jog.glb";
import sprintAnimationFileUrl from "../../../assets/models/anim_run.glb";
Expand All @@ -30,6 +31,7 @@ const animationConfig: AnimationConfig = {
idleAnimationFileUrl,
jogAnimationFileUrl,
sprintAnimationFileUrl,
doubleJumpAnimationFileUrl,
};

// Specify the avatar to use here:
Expand Down
2 changes: 2 additions & 0 deletions example/multi-user-3d-web-experience/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Networked3dWebExperienceClient } from "@mml-io/3d-web-experience-client

import hdrJpgUrl from "../../../assets/hdr/puresky_2k.jpg";
import airAnimationFileUrl from "../../../assets/models/anim_air.glb";
import doubleJumpAnimationFileUrl from "../../../assets/models/anim_double_jump.glb";
import idleAnimationFileUrl from "../../../assets/models/anim_idle.glb";
import jogAnimationFileUrl from "../../../assets/models/anim_jog.glb";
import sprintAnimationFileUrl from "../../../assets/models/anim_run.glb";
Expand All @@ -21,6 +22,7 @@ const app = new Networked3dWebExperienceClient(holder, {
idleAnimationFileUrl,
jogAnimationFileUrl,
sprintAnimationFileUrl,
doubleJumpAnimationFileUrl,
},
hdrJpgUrl,
mmlDocuments: [{ url: `${protocol}//${host}/mml-documents/example-mml.html` }],
Expand Down
47 changes: 28 additions & 19 deletions packages/3d-web-client-core/src/camera/CameraManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class CameraManager {
public damping: number = camValues.damping;
public dampingScale: number = 0.01;
public zoomScale: number = camValues.zoomScale;
public zoomDamping: number = camValues.zoomDamping;
public invertFOVMapping: boolean = camValues.invertFOVMapping;
public fov: number = this.initialFOV;

Expand Down Expand Up @@ -66,7 +67,7 @@ export class CameraManager {
this.targetPhi = initialPhi;
this.theta = initialTheta;
this.targetTheta = initialTheta;
this.camera = new PerspectiveCamera(this.fov, window.innerWidth / window.innerHeight, 0.1, 300);
this.camera = new PerspectiveCamera(this.fov, window.innerWidth / window.innerHeight, 0.1, 400);
this.camera.position.set(0, 1.4, -this.initialDistance);
this.rayCaster = new Raycaster();

Expand All @@ -77,6 +78,7 @@ export class CameraManager {
[document, "mouseup", this.onMouseUp.bind(this)],
[document, "mousemove", this.onMouseMove.bind(this)],
[targetElement, "wheel", this.onMouseWheel.bind(this)],
[targetElement, "contextmenu", this.onContextMenu.bind(this)],
]);

if (this.hasTouchControl) {
Expand Down Expand Up @@ -136,21 +138,30 @@ export class CameraManager {
}
}

private onMouseDown(): void {
this.dragging = true;
private onMouseDown(event: MouseEvent): void {
if (event.button === 0 || event.button === 2) {
// Left or right mouse button
this.dragging = true;
document.body.style.cursor = "none";
}
}

private onMouseUp(_event: MouseEvent): void {
this.dragging = false;
private onMouseUp(event: MouseEvent): void {
if (event.button === 0 || event.button === 2) {
this.dragging = false;
document.body.style.cursor = "default";
}
}

private onMouseMove(event: MouseEvent): void {
if (!this.dragging || getTweakpaneActive()) return;
if (this.targetTheta === null || this.targetPhi === null) return;
this.targetTheta += event.movementX * this.dampingScale;
this.targetPhi -= event.movementY * this.dampingScale;
this.targetPhi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, this.targetPhi));
event.preventDefault();
if (getTweakpaneActive()) return;
if (this.dragging) {
if (this.targetTheta === null || this.targetPhi === null) return;
this.targetTheta += event.movementX * this.dampingScale;
this.targetPhi -= event.movementY * this.dampingScale;
this.targetPhi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, this.targetPhi));
event.preventDefault();
}
}

private onMouseWheel(event: WheelEvent): void {
Expand All @@ -164,6 +175,10 @@ export class CameraManager {
event.preventDefault();
}

private onContextMenu(event: MouseEvent): void {
event.preventDefault();
}

public setTarget(target: Vector3): void {
if (!this.isLerping) {
this.target.copy(target);
Expand Down Expand Up @@ -201,13 +216,6 @@ export class CameraManager {
}

public adjustCameraPosition(): void {
/*
The purpose for the offsetDistance is to set the rayCaster further from the player
than the camera is on the z relative axis, so we can avoid having a camera collider
and expensive checks to prevent seeing clipped wals or floors or objects when we
readjust the camera. 50cm (the current offset) should get a good balance for most
indoor environments
*/
const offsetDistance = 0.5;
const offset = new Vector3(0, 0, offsetDistance);
offset.applyEuler(this.camera.rotation);
Expand Down Expand Up @@ -266,7 +274,8 @@ export class CameraManager {
this.theta !== null &&
this.targetTheta !== null
) {
this.distance += (this.targetDistance - this.distance) * this.damping * 0.21;
this.distance +=
(this.targetDistance - this.distance) * this.damping * (0.21 + this.zoomDamping);
this.phi += (this.targetPhi - this.phi) * this.damping;
this.theta += (this.targetTheta - this.theta) * this.damping;

Expand Down
1 change: 1 addition & 0 deletions packages/3d-web-client-core/src/character/Character.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type AnimationConfig = {
jogAnimationFileUrl: string;
sprintAnimationFileUrl: string;
airAnimationFileUrl: string;
doubleJumpAnimationFileUrl: string;
};

export type CharacterDescription = {
Expand Down
8 changes: 6 additions & 2 deletions packages/3d-web-client-core/src/character/CharacterManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Euler, Group, Quaternion, Vector3 } from "three";

import { CameraManager } from "../camera/CameraManager";
import { CollisionsManager } from "../collisions/CollisionsManager";
import { ease } from "../helpers/math-helpers";
import { KeyInputManager } from "../input/KeyInputManager";
import { VirtualJoystick } from "../input/VirtualJoystick";
import { Composer } from "../rendering/composer";
import { TimeManager } from "../time/TimeManager";
import { TweakPane } from "../tweakpane/TweakPane";

import { AnimationConfig, Character, CharacterDescription } from "./Character";
import { CharacterModelLoader } from "./CharacterModelLoader";
Expand Down Expand Up @@ -43,7 +43,7 @@ export class CharacterManager {
public remoteCharacterControllers: Map<number, RemoteController> = new Map();

private localCharacterSpawned: boolean = false;
private localController: LocalController;
public localController: LocalController;
public localCharacter: Character | null = null;

private speakingCharacters: Map<number, boolean> = new Map();
Expand Down Expand Up @@ -102,6 +102,10 @@ export class CharacterManager {
this.localCharacterSpawned = true;
}

public setupTweakPane(tweakPane: TweakPane) {
tweakPane.setupCharacterController(this.localController);
}

public spawnRemoteCharacter(
id: number,
username: string,
Expand Down
87 changes: 64 additions & 23 deletions packages/3d-web-client-core/src/character/CharacterModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,38 @@ export class CharacterModel {

public mmlCharacterDescription: MMLCharacterDescription;

private isPostDoubleJump = false;

constructor(private config: CharacterModelConfig) {}

public async init(): Promise<void> {
await this.loadMainMesh();
await Promise.all([
this.setAnimationFromFile(
this.config.animationConfig.idleAnimationFileUrl,
AnimationState.idle,
),
this.setAnimationFromFile(
this.config.animationConfig.jogAnimationFileUrl,
AnimationState.walking,
),
this.setAnimationFromFile(
this.config.animationConfig.sprintAnimationFileUrl,
AnimationState.running,
),
this.setAnimationFromFile(
this.config.animationConfig.airAnimationFileUrl,
AnimationState.air,
),
]);
await this.setAnimationFromFile(
this.config.animationConfig.idleAnimationFileUrl,
AnimationState.idle,
true,
);
await this.setAnimationFromFile(
this.config.animationConfig.jogAnimationFileUrl,
AnimationState.walking,
true,
);
await this.setAnimationFromFile(
this.config.animationConfig.sprintAnimationFileUrl,
AnimationState.running,
true,
);
await this.setAnimationFromFile(
this.config.animationConfig.airAnimationFileUrl,
AnimationState.air,
true,
);
await this.setAnimationFromFile(
this.config.animationConfig.doubleJumpAnimationFileUrl,
AnimationState.doubleJump,
false,
1.3,
);
this.applyCustomMaterials();
}

Expand Down Expand Up @@ -108,6 +118,15 @@ export class CharacterModel {
}

public updateAnimation(targetAnimation: AnimationState) {
if (this.isPostDoubleJump) {
if (targetAnimation === AnimationState.doubleJump) {
// Double jump is requested, but we're in the post double jump state so we play air instead
targetAnimation = AnimationState.air;
} else {
// Reset the post double jump flag if something other than double jump is requested
this.isPostDoubleJump = false;
}
}
if (this.currentAnimation !== targetAnimation) {
this.transitionToAnimation(targetAnimation);
}
Expand Down Expand Up @@ -214,16 +233,23 @@ export class CharacterModel {
private async setAnimationFromFile(
animationFileUrl: string,
animationType: AnimationState,
loop: boolean = true,
playbackSpeed: number = 1.0,
): Promise<void> {
return new Promise(async (resolve, reject) => {
const animation = await this.config.characterModelLoader.load(animationFileUrl, "animation");
const cleanAnimation = this.cleanAnimationClips(this.mesh!, animation as AnimationClip);
if (typeof animation !== "undefined" && cleanAnimation instanceof AnimationClip) {
this.animations[animationType] = this.animationMixer!.clipAction(cleanAnimation);
this.animations[animationType].stop();
this.animations[animationType].timeScale = playbackSpeed;
if (animationType === AnimationState.idle) {
this.animations[animationType].play();
}
if (!loop) {
this.animations[animationType].setLoop(LoopRepeat, 1); // Ensure non-looping
this.animations[animationType].clampWhenFinished = true;
}
resolve();
} else {
reject(`failed to load ${animationType} from ${animationFileUrl}`);
Expand All @@ -235,22 +261,37 @@ export class CharacterModel {
targetAnimation: AnimationState,
transitionDuration: number = 0.15,
): void {
if (!this.mesh) return;
if (!this.mesh) {
return;
}

const currentAction = this.animations[this.currentAnimation];
this.currentAnimation = targetAnimation;
const targetAction = this.animations[targetAnimation];

if (!targetAction) return;
if (!targetAction) {
return;
}

if (currentAction) {
currentAction.enabled = true;
currentAction.fadeOut(transitionDuration);
}

if (!targetAction.isRunning()) targetAction.play();
targetAction.reset();
if (!targetAction.isRunning()) {
targetAction.play();
}

if (targetAnimation === AnimationState.doubleJump) {
targetAction.getMixer().addEventListener("finished", (_event) => {
if (this.currentAnimation === AnimationState.doubleJump) {
this.isPostDoubleJump = true;
// This triggers the transition to the air animation because the double jump animation is done
this.updateAnimation(AnimationState.doubleJump);
}
});
}

targetAction.setLoop(LoopRepeat, Infinity);
targetAction.enabled = true;
targetAction.fadeIn(transitionDuration);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum AnimationState {
"jumpToAir" = 3,
"air" = 4,
"airToGround" = 5,
"doubleJump" = 6,
}

export type CharacterState = {
Expand Down
Loading
Loading