This commit is contained in:
Vasyl Kazakov
2026-05-28 18:35:39 +03:00
parent aefe01860e
commit 2633be60a1
18 changed files with 5545 additions and 109 deletions
@@ -0,0 +1,77 @@
import { EasyEvent } from "@24tools/playable_template";
import { AnimationAction, AnimationClip, AnimationMixer, Color, LoopOnce, LoopRepeat, Mesh, MeshBasicMaterial, Object3D, Vector3 } from "three";
import { clone } from "three/examples/jsm/utils/SkeletonUtils";
import { ThreeC } from "../../ThreeC";
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
export class Character {
tObj: Object3D;
animMixer: AnimationMixer;
animationList: AnimationClip[] = [];
// isWalking: boolean = false;
curClipAction: null | AnimationAction = null;
animStopTimeout: null | NodeJS.Timeout = null
onAnimLoop: EasyEvent<{}> = new EasyEvent<{}>();
onAnimFinish: EasyEvent<{}> = new EasyEvent<{}>();
constructor(prefab: GLTF, start_position = new Vector3()) {
let tObj = clone(prefab.scene);
tObj.castShadow = true;
let animMixer = new AnimationMixer(tObj);
animMixer.addEventListener('loop', () => {
this.onAnimLoop.Invoke({});
});
animMixer.addEventListener('finished', () => {
this.onAnimFinish.Invoke({});
});
if (start_position) tObj.position.copy(start_position);
this.tObj = tObj;
this.animMixer = animMixer;
this.animationList = prefab.animations;
ThreeC.addToScene(tObj);
ThreeC.addAnimMixer(animMixer);
return this;
}
set AnimationSpeed(timeScale: number) {
if (this.curClipAction)
this.curClipAction.timeScale = timeScale;
}
set AnimationWeight(weight: number) {
if (this.curClipAction)
this.curClipAction.weight = weight;
}
playAnimation(anim_id: number, one_time: boolean = false, fade = 0.25, randomStart = false) {
let oldClipAction: null | AnimationAction = this.curClipAction;
var clipAction = this.animMixer.clipAction(this.animationList[anim_id]);
if (one_time) {
clipAction.clampWhenFinished = true;
clipAction.setLoop(LoopOnce, 1);
}
else {
clipAction.clampWhenFinished = false;
clipAction.setLoop(LoopRepeat, Infinity);
}
clipAction.timeScale = 1;
clipAction.weight = 1;
clipAction.reset();
if (randomStart)
clipAction.time = Math.random() * this.animationList[anim_id].duration;
clipAction.play();
if (oldClipAction && oldClipAction != clipAction)
oldClipAction.crossFadeTo(clipAction, fade, true);
this.curClipAction = clipAction;
}
}
@@ -0,0 +1,5 @@
export enum BaseAnimation {
Nan = -1,
Idle = 0,
Run = 1,
}
@@ -0,0 +1,3 @@
export enum MeshType {
Character = "character",
}
@@ -0,0 +1,3 @@
export enum ResourcesType {
Mesh = "mesh",
}
+16
View File
@@ -0,0 +1,16 @@
import { Vec3 } from "cannon-es";
import { Vector3 } from "three";
/**
* Vec3(cannon) To Vector3(three)
*/
export function Vector3CToT(value: Vec3) {
return new Vector3(value.x, value.y, value.z);
}
/**
* Vector3(three) To Vec3(cannon)
*/
export function Vector3TToC(value: Vector3) {
return new Vec3(value.x, value.y, value.z);
}
@@ -0,0 +1,6 @@
import { Vector3 } from "three";
export interface IMoveInput {
get CurrentDirection(): Vector3;
update(delta);
}
@@ -0,0 +1,108 @@
import { Delegate, JoystickC, UpdateController } from "@24tools/playable_template";
import { Vector3 } from "three";
import { IMoveInput } from "./MoveInput";
import { FollowCameraC } from "../Movment/CameraMovment/FollowCamera";
// type JoystickVectorData = {
// vector?: {
// x: number;
// y: number;
// };
// };
// type JoystickPayload = {
// event?: Event;
// data?: JoystickVectorData;
// };
export class PlayerInput implements IMoveInput {
public static InitJoystick() {
const screenSize = window.screenSize;
const minSize = Math.min(screenSize.width, screenSize.height);
const joystickSizeAspect = 0.2;
const fadeTime = 200;
const options = {
zone: document.getElementById("joystick_zone") as HTMLElement,
size: minSize * joystickSizeAspect,
restJoystick: true,
dynamicPage: true,
catchDistance: minSize * joystickSizeAspect / 2,
fadeTime: fadeTime,
};
JoystickC.init(options);
// JoystickC.onJoysticMove.addDelegate(({ event, data }) => {
// console.log('onJoysticMove', event, data);
// })
}
protected currentDirection: Vector3 = new Vector3();
private updateDelegate: Delegate<number>;
private StartDelegate: Delegate<any>;
private MoveDelegate: Delegate<any>;
private StopDelegate: Delegate<any>;
private static threshold = 0.25;
get CurrentDirection() { return this.currentDirection.clone(); };
inputaActive: boolean = false;
constructor() {
this.updateDelegate = UpdateController.Instance.onUpdate.addDelegate(this.update.bind(this));
this.MoveDelegate = JoystickC.onJoysticMove.addDelegate(this.onTouchMove.bind(this));
// "down" also carries joystick data in this SDK, useful for first non-zero direction.
JoystickC.onJoysticDown.addListener(this.MoveDelegate);
this.StopDelegate = JoystickC.onJoysticEnd.addDelegate(this.onTouchUp.bind(this));
this.StartDelegate = JoystickC.onJoysticStart.addDelegate(this.onTouchDown.bind(this));
}
update(delta: number) {
}
onTouchMove(payload: any) {
this.currentDirection = this.GetDiraction(payload);
if (this.currentDirection.length() <= PlayerInput.threshold)
this.currentDirection.multiplyScalar(0);
}
onTouchDown(_event: any) {
// console.log(event);
if (this.inputaActive) return;
this.inputaActive = true;
}
onTouchUp() {
if (!this.inputaActive) return;
this.inputaActive = false;
this.currentDirection.multiplyScalar(0);
}
GetDiraction(payload: any) {
// Compatible with old/new nipplejs payloads wrapped by JoystickC:
// - { event, data } where data.vector exists
// - { event, data: undefined } where event.data.vector exists
const normalizedData =
payload?.data ??
payload?.event?.data ??
payload?.event;
const vector = normalizedData?.vector;
if (!vector) return new Vector3();
const x = vector.x;
const y = vector.y;
const dir = new Vector3(-x, 0, y);
dir.applyEuler(FollowCameraC.RotationCorection);
return dir;
}
}
@@ -0,0 +1,93 @@
import { Delegate, Helper, Template, UpdateController } from "@24tools/playable_template";
import { Object3D, Vector3 } from "three";
import { ThreeC } from "../../../ThreeC";
import { CameraC } from "../../../CameraC";
export class FollowCameraC {
private static updateDelegate: Delegate<number>;
static target: Object3D;
static offset: Vector3;
static mainContainer: Object3D = new Object3D();
static cameraContainer: Object3D = new Object3D();
static cameraRotation: Object3D = new Object3D();
/** Normalized movement direction set each frame by the player */
static inputDirection: Vector3 = new Vector3();
/** How far (world units) the camera shifts ahead of the player */
static lookAheadAmount: number = 2;
/** Lerp speed for the look-ahead offset (lower = smoother/slower) */
static lookAheadLerpSpeed: number = 0.7;
private static lookAheadCurrent: Vector3 = new Vector3();
static Init(target: Object3D) {
this.target = target;
this.offset = this.Offset;
console.log(target);
this.updateDelegate = new Delegate<number>(this.Update.bind(this));
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
ThreeC.addToScene(this.mainContainer);
this.mainContainer.add(this.cameraContainer);
this.cameraContainer.add(this.cameraRotation);
this.cameraRotation.add(CameraC.cameraContainer);
this.cameraRotation.rotateY(Math.PI);
this.mainContainer.position.copy(target.position);
this.mainContainer.position.x += this.Offset.x;
this.mainContainer.position.z += this.Offset.z;
this.cameraContainer.position.y += this.Offset.y;
}
private static Update(delta: number) {
if (!this.target.position) return;
// Lerp look-ahead toward current movement direction (stays at last direction when stopped)
const lookAheadTarget = this.inputDirection.clone().multiplyScalar(this.lookAheadAmount);
this.lookAheadCurrent.lerp(lookAheadTarget, delta * this.lookAheadLerpSpeed);
const offset = this.Offset;
// Base position without look-ahead
const basePos = this.target.position.clone();
basePos.x += offset.x;
basePos.z += offset.z;
// Final target = base + look-ahead shift
const targetPos = basePos.clone();
targetPos.x += this.lookAheadCurrent.x;
targetPos.z += this.lookAheadCurrent.z;
// Capture actual previous position BEFORE any mutation
const oldPos = this.mainContainer.position.clone();
// Compute rotation using base position to avoid tilt from look-ahead offset
this.mainContainer.position.copy(basePos);
this.mainContainer.lookAt(this.target.position);
this.cameraContainer.lookAt(this.target.position);
// Smooth follow lerp from real previous position toward target with look-ahead
const lerpSpeed = 10;
this.mainContainer.position.lerpVectors(oldPos, targetPos, delta * lerpSpeed);
}
static get RotationCorection() {
const rotation = this.mainContainer.rotation.clone();
return rotation;
}
static get Offset() {
const portrait = window.screenSize.portrait
const values = portrait
? Template.getValue<number[]>("global", "camera_position_p")
: Template.getValue<number[]>("global", "camera_position_l");
const offset = Helper.returnVectorCamera(values);
return offset
}
}
+25
View File
@@ -0,0 +1,25 @@
import { Delegate, UpdateController } from "@24tools/playable_template";
import { IMoveInput } from "../Input/MoveInput";
import { Vector3 } from "three";
export class MoveC {
private input: IMoveInput;
private speed: number = 5;
private updateDelegate: Delegate<number>;
private moveDiraction: Vector3 = new Vector3();
get Diraction() { return this.moveDiraction };
get Weight() { return this.moveDiraction.length() / this.speed };
constructor(Input: IMoveInput, speed: number = 5) {
this.updateDelegate = new Delegate<number>(this.update.bind(this));
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
this.input = Input;
this.speed = speed;
}
private update(delta: number) {
// delta *= TimeC.TimeScale;
const moveStep = this.input.CurrentDirection.multiplyScalar(this.speed);
this.moveDiraction.copy(moveStep);
}
}
@@ -0,0 +1,35 @@
import { Delegate, UpdateController } from "@24tools/playable_template";
import { IMoveInput } from "../Input/MoveInput";
import { Object3D, Quaternion, Vector3 } from "three";
export class RotationC {
private target: Object3D;
private input: IMoveInput;
private speed: number = 5;
private updateDelegate: Delegate<number>;
private currentQ: Quaternion = new Quaternion();
private targetQ: Quaternion = new Quaternion();
constructor(target: Object3D, Input: IMoveInput, speed: number = 5) {
this.updateDelegate = new Delegate<number>(this.update.bind(this));
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
this.input = Input;
this.speed = speed;
this.target = target;
this.currentQ.copy(this.target.quaternion);
this.targetQ.copy(this.target.quaternion);
}
private update(delta: number) {
const loockAtStep = this.input.CurrentDirection;
if (loockAtStep.length() != 0) {
const loockAtPoint = this.target.position.clone().add(loockAtStep);
this.target.lookAt(loockAtPoint);
this.targetQ.copy(this.target.quaternion);
this.target.quaternion.copy(this.currentQ);
}
this.target.quaternion.slerp(this.targetQ, delta * this.speed);
this.currentQ.copy(this.target.quaternion);
}
}
+117
View File
@@ -0,0 +1,117 @@
import { Delegate, ResourcesC, UpdateController } from "@24tools/playable_template";
import { Character } from "./Character/Character";
import { ResourcesType } from "./Enums/ResourcesType";
import { MeshType } from "./Enums/MeshType";
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
import { BaseAnimation } from "./Enums/BaseAnimation";
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
import { Object3D } from "three";
import { ThreeC } from "../ThreeC";
import { PlayerInput } from "./Input/PlayerInput";
import { MoveC } from "./Movment/MoveC";
import { Vec3 } from "cannon-es";
import { contain } from "three/src/extras/TextureUtils";
import { Vector3CToT, Vector3TToC } from "./Helper";
import { RotationC } from "./Movment/RotationC";
import { FollowCameraC } from "./Movment/CameraMovment/FollowCamera";
import { Vector3 } from "three";
export class Player {
private static inited: boolean = false;
private static isRunning: boolean = false;
private static updateDelegate: Delegate<number>;
private static container: Object3D = new Object3D;
private static input: PlayerInput;
private static movement: MoveC;
private static rotation: RotationC;
private static spawnPosition: Vector3 = new Vector3(0, 0, 0);
static character: Character;
static physics: PhysicsBody;
static SetSpawnPosition(position: Vector3) {
this.spawnPosition.copy(position);
}
static Init() {
if (this.inited) return;
this.inited = true
const asset = ResourcesC.getResource<GLTF>(ResourcesType.Mesh, MeshType.Character)
this.character = new Character(asset);
this.container.position.copy(this.spawnPosition);
this.character.playAnimation(BaseAnimation.Idle)
this.container.add(this.character.tObj);
ThreeC.addToScene(this.container);
this.InitPhisic();
const input = new PlayerInput();
const moveSpeed = 3;
const rotationSpeed = 8;
this.movement = new MoveC(input, moveSpeed);
this.rotation = new RotationC(this.container, input, rotationSpeed);
this.updateDelegate = new Delegate<number>((delta) => this.Update(delta));
UpdateController.Instance.onUpdate.addListener(this.updateDelegate);
FollowCameraC.Init(this.container);
}
private static InitPhisic() {
this.physics = new PhysicsBody(
this.container,
false,
1,
PhysicsLayer.Player,
PhysicsLayer.Wall,
);
}
private static StartRunning() {
if (this.isRunning) return;
this.character.playAnimation(BaseAnimation.Run);
this.isRunning = true;
}
private static StopRunning() {
if (!this.isRunning) return;
this.character.playAnimation(BaseAnimation.Idle);
this.AnimationValue = 1;
this.isRunning = false;
}
private static set AnimationValue(value: number) {
this.character.AnimationSpeed = value;
this.character.AnimationWeight = value * 12.5 + 87.5;
}
private static Update(delta: number) {
const diraction = this.movement.Diraction;
const weight = this.movement.Weight;
if (diraction.length() > 0) {
this.StartRunning();
this.AnimationValue = weight;
FollowCameraC.inputDirection.copy(diraction).normalize();
} else {
this.StopRunning();
}
const cPos = Vector3TToC(diraction);
this.physics.getPhysicsBody().velocity.copy(cPos);
this.MoveVisual(delta);
}
private static MoveVisual(delta: number) {
const lerpSpeed = 10;
const targetPos = (Vector3CToT(this.physics.getPhysicsBody().position));
this.container.position.lerp(targetPos, delta * lerpSpeed)
}
}