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
+19 -19
View File
@@ -3,7 +3,7 @@ import { ConfigUiParamsCategories } from "@24tools/ads_common";
export const globalSettings: ConfigUiParamsCategories[] = [
{
id: "global",
name: "Scene settings",
name: "Global settings",
params: [
{
id: "light_intensity",
@@ -30,7 +30,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [
values: [
30,
150,
60
50
]
},
{
@@ -43,17 +43,17 @@ export const globalSettings: ConfigUiParamsCategories[] = [
[
-10,
10,
0
6
],
[
0,
8,
9
],
[
-10,
10,
1
],
[
-10,
10,
5
7
]
]
},
@@ -67,7 +67,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [
[
-360,
360,
0
-5
],
[
-360,
@@ -88,7 +88,7 @@ export const globalSettings: ConfigUiParamsCategories[] = [
values: [
30,
150,
60
55
]
},
{
@@ -101,17 +101,17 @@ export const globalSettings: ConfigUiParamsCategories[] = [
[
-10,
10,
0
2
],
[
-10,
10,
1
],
[
-10,
10,
0,
8,
5
],
[
-10,
10,
3
]
]
},
+1 -17
View File
@@ -1,20 +1,16 @@
import { CameraC_internal, Helper, Template } from "@24tools/playable_template";
import { PerspectiveCamera, Vector3 } from "three";
import { PerspectiveCamera } from "three";
export class CameraC extends CameraC_internal {
static setCamera(portraitOrientation: boolean) {
const CATEGORY = Template.getCategory("global");
if (this.camera !== null) {
let position = portraitOrientation
? Helper.returnVectorCamera(CATEGORY["camera_position_p"] as number[])
: Helper.returnVectorCamera(CATEGORY["camera_position_l"] as number[]);
const rotation = portraitOrientation
? Helper.returnEulerCamera(CATEGORY["camera_rotation_p"] as number[])
: Helper.returnEulerCamera(CATEGORY["camera_rotation_l"] as number[]);
this.camera.rotation.x = rotation.x;
this.camera.rotation.y = rotation.y;
this.camera.rotation.z = rotation.z;
this.camera.position.copy(position.clone());
if (this.camera instanceof PerspectiveCamera) {
this.camera.fov = portraitOrientation
? Number(CATEGORY["camera_fov_p"])
@@ -23,16 +19,4 @@ export class CameraC extends CameraC_internal {
}
}
}
static setTopDownCamera() {
if (this.camera !== null) {
this.camera.position.set(20, 10, -5);
this.camera.rotation.set(-5 * Math.PI / 12, 0, 0);
this.camera.lookAt(new Vector3(0, 0, 0));
if (this.camera instanceof PerspectiveCamera) {
this.camera.fov = 55;
this.camera.updateProjectionMatrix();
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
import { Object3D } from "three";
import { ThreeC } from "../ThreeC";
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
export class Map {
static Init() {
const mapObject = ThreeC.getObject("map");
if (!mapObject) {
console.warn("Map model resource not found: map");
return;
}
mapObject.position.set(0, 0, 0);
ThreeC.setShadowsStateForChildren(mapObject, true, true);
ThreeC.addToScene(mapObject);
const colliders = mapObject.getObjectByName("Colliders");
if (colliders) {
colliders.visible = false;
this.buildColliders(colliders);
}
}
private static buildColliders(collidersRoot: Object3D) {
collidersRoot.traverse((child) => {
if (child === collidersRoot) return;
if ((child as any).isMesh) {
new PhysicsBody(child, false, 0, PhysicsLayer.Wall, PhysicsLayer.Player);
}
});
}
}
@@ -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)
}
}
+40 -60
View File
@@ -1,72 +1,52 @@
import { DirectionalLight, Object3D, Vector3, Mesh, Material } from "three";
import { BoxGeometry, Mesh, MeshStandardMaterial, Vector3 } from "three";
import { ThreeC } from "./ThreeC";
import { CameraC } from "./CameraC";
import { InputC, UpdateController } from "@24tools/playable_template";
import { InputC, JoystickC } from "@24tools/playable_template";
import { Player } from "./Presets/Player";
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
import { Map } from "./Map/Map";
export class TestSceneC {
private static character: Object3D | null = null;
static init() {
this.loadMapModel();
this.loadCharacter();
// this.createPrimitive();
Map.Init();
this.InitPlayer();
// example of using InputC events
InputC.onTouchDown.addDelegate((event) => {
console.log("onMouseDown", event);
});
// InputC.onTouchDown.addDelegate((event) => {
// console.log("onMouseDown", event);
// });
// Update loop to follow character with camera
UpdateController.Instance.onUpdate.addDelegate(() => {
this.updateCameraFollow();
});
// if you have update in your controller
// UpdateController.Instance.onUpdate.addDelegate(() => {
// this.update();
// });
}
private static loadMapModel() {
const mapObject = ThreeC.getObject("map");
if (!mapObject) {
console.warn("Map model resource not found: map");
return;
// private static createPrimitive() {
// const planeSize = new Vector3(10, 0.1, 10);
// const geometryPlane = new BoxGeometry(planeSize.x, planeSize.y, planeSize.z);
// const materialPlane = new MeshStandardMaterial({ color: 0xaaaaaa });
// const plane = new Mesh(geometryPlane, materialPlane);
// ThreeC.setShadowsStateForChildren(plane, false, true);
// plane.position.y = -planeSize.y / 2;
// new PhysicsBody(
// plane,
// false,
// 0,
// PhysicsLayer.Wall,
// PhysicsLayer.Player,
// );
// ThreeC.addToScene(plane);
// }
private static InitPlayer() {
const playerSpawnPoint = new Vector3(4, 0, 27);
Player.SetSpawnPosition(playerSpawnPoint);
Player.Init();
}
mapObject.position.set(0, 0, 0);
ThreeC.setShadowsStateForChildren(mapObject, true, true);
ThreeC.addToScene(mapObject);
}
private static updateCameraFollow() {
if (!this.character || !CameraC.camera) return;
// Position camera at 75 degree angle from horizontal, slightly to the side
const charPos = this.character.position;
const angle = 70 * (Math.PI / 180); // 75 degrees
const distance = 10;
const sideOffset = 5; // offset to the left side
// Calculate vertical and horizontal distances based on 75 degree angle
const verticalDistance = distance * Math.sin(angle);
const horizontalDistance = distance * Math.cos(angle);
CameraC.camera.position.set(
charPos.x + sideOffset,
charPos.y + verticalDistance,
charPos.z + horizontalDistance
);
CameraC.camera.lookAt(charPos.x, charPos.y + 1, charPos.z);
}
private static loadCharacter() {
const charObject = ThreeC.getObject("character");
if (!charObject) {
console.warn("Character model resource not found: character");
return;
}
// Place character near the map center; adjust Y for ground offset
charObject.position.copy(new Vector3(0, 0, 32));
this.character = charObject;
ThreeC.setShadowsStateForChildren(charObject, true, true);
ThreeC.addToScene(charObject);
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ window.setupConfig = async function (config) {
redirectOptions: {},
ticker: Template3d.ticker,
debug: {
physics: false,
physics: true,
// set true if you want to enable physics debugger
logger: false // set true if you want to enable logger
}
@@ -1,7 +1,7 @@
import {
CameraC_internal,
CameraType,
// JoystickC,
JoystickC,
Physics_internal,
Template,
Template3d,
@@ -11,6 +11,7 @@ import {
} from "@24tools/playable_template";
import { CameraC } from "../controllers/CameraC";
import { ThreeC } from "../controllers/ThreeC";
import { PlayerInput } from "../controllers/Presets/Input/PlayerInput";
import { Color } from "three";
import { Vec3 } from "cannon-es";
export const beforeResourcesLoadedCb = () => {
@@ -23,8 +24,10 @@ export const beforeResourcesLoadedCb = () => {
ThreeC_internal.init();
ThreeC.createBaseLights();
ThreeC.setupDirectionalLight();
PlayerInput.InitJoystick();
let physicsWorld = Physics_internal.init(new Vec3(0, -9.81, 0));
let physicsWorld = Physics_internal.init(new Vec3(0, 0, 0));
// example of using joystick. Uncomment if you need joystick
+4949
View File
File diff suppressed because one or more lines are too long