Add joystick-controlled character with camera follow and bat/pistol combat
Replaces the placeholder cube with ZombiePunk_Character: rigged model with Idle/SlowWalk/Loot animations, tap-to-move joystick control, AABB collision against crate colliders, and a pistol/bat weapon swap driven by facing a destructible object (Loot anim doubles as the melee hit since there's no dedicated swing clip). Camera (CameraFollowC) follows with framerate-independent damping, a smoothed look-ahead that pans toward the character's facing direction, and obstacle-avoidance raycasting from eye height so short props don't falsely block the view. Spawn point and forward convention flipped to the other end of the road per request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
import { Object3D, Raycaster, Vector3 } from "three";
|
||||||
|
import { CameraC_internal, UpdateController } from "@hitplay/playable_template";
|
||||||
|
|
||||||
|
// How fast the camera closes the gap to its target position/look point, per
|
||||||
|
// second. Higher = snappier, lower = floatier. Framerate-independent (see
|
||||||
|
// the exponential smoothing in update()), so the feel is the same at 30fps
|
||||||
|
// and 60fps instead of drifting with frame time.
|
||||||
|
const POSITION_DAMPING = 5;
|
||||||
|
const LOOK_DAMPING = 8;
|
||||||
|
|
||||||
|
// Keeps the camera a bit off any obstacle it lands on, and never lets it
|
||||||
|
// collapse onto the target itself.
|
||||||
|
const OBSTACLE_SKIN = 0.4;
|
||||||
|
const MIN_DISTANCE = 1.5;
|
||||||
|
|
||||||
|
// How far (world units) the whole camera rig — position and look-at point
|
||||||
|
// alike — pans toward wherever the character is currently facing, to open
|
||||||
|
// up more of the space ahead of them instead of centering them dead-on.
|
||||||
|
// Matches PlayerC's own forward convention (+z at rest); the two aren't
|
||||||
|
// coupled in code, but there's only one character to stay in sync with.
|
||||||
|
const LOOK_AHEAD_DISTANCE = 1.2;
|
||||||
|
|
||||||
|
// The character's body can spin at up to 540°/s (PlayerC.MAX_TURN_SPEED),
|
||||||
|
// which would yank the look-ahead point around just as fast if it followed
|
||||||
|
// the raw facing direction. Smoothing the direction itself, separately from
|
||||||
|
// (and slower than) the position/look damping below, is what actually makes
|
||||||
|
// the pan gentle regardless of how fast the character turns.
|
||||||
|
const LOOK_AHEAD_DAMPING = 2;
|
||||||
|
|
||||||
|
const FORWARD_AXIS = new Vector3(0, 0, 1);
|
||||||
|
|
||||||
|
export class CameraFollowC {
|
||||||
|
private static target: Object3D | null = null;
|
||||||
|
private static offset = new Vector3();
|
||||||
|
private static obstacles: Object3D[] = [];
|
||||||
|
private static eyeHeight = 0;
|
||||||
|
private static smoothedLookAt = new Vector3();
|
||||||
|
// The actual (x,y,z) offset, not a direction — see update() for why that
|
||||||
|
// distinction matters.
|
||||||
|
private static smoothedLookAhead = FORWARD_AXIS.clone().multiplyScalar(LOOK_AHEAD_DISTANCE);
|
||||||
|
private static raycaster = new Raycaster();
|
||||||
|
|
||||||
|
static init(target: Object3D, offset: Vector3, obstacles: Object3D[] = [], eyeHeight = 0) {
|
||||||
|
this.target = target;
|
||||||
|
this.offset = offset.clone();
|
||||||
|
this.obstacles = obstacles;
|
||||||
|
this.eyeHeight = eyeHeight;
|
||||||
|
this.smoothedLookAt.copy(target.position);
|
||||||
|
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate((delta) => this.update(delta));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static update(delta: number) {
|
||||||
|
if (!this.target) return;
|
||||||
|
|
||||||
|
const camera = CameraC_internal.getCamera();
|
||||||
|
const targetPosition = this.target.position;
|
||||||
|
|
||||||
|
const facing = FORWARD_AXIS.clone().applyQuaternion(this.target.quaternion);
|
||||||
|
facing.y = 0;
|
||||||
|
facing.normalize();
|
||||||
|
const desiredLookAhead = facing.multiplyScalar(LOOK_AHEAD_DISTANCE);
|
||||||
|
|
||||||
|
// Damping the raw offset vector (not a unit direction re-normalized
|
||||||
|
// every frame) is what keeps this a straight-line move through the
|
||||||
|
// character on a direction change, instead of an arc: re-normalizing
|
||||||
|
// pins the vector's length at LOOK_AHEAD_DISTANCE the whole time, which
|
||||||
|
// forces it to sweep around a circle of that radius as the direction
|
||||||
|
// changes. Left as plain (x,y,z) lerp, it can shrink through zero and
|
||||||
|
// grow back out the other way — a straight line, not a curve.
|
||||||
|
const lookAheadT = 1 - Math.exp(-LOOK_AHEAD_DAMPING * delta);
|
||||||
|
this.smoothedLookAhead.lerp(desiredLookAhead, lookAheadT);
|
||||||
|
const lookAhead = this.smoothedLookAhead;
|
||||||
|
|
||||||
|
const desiredPosition = targetPosition.clone().add(this.offset).add(lookAhead);
|
||||||
|
this.avoidObstacles(targetPosition, desiredPosition);
|
||||||
|
|
||||||
|
// Exponential (framerate-independent) damping instead of a fixed lerp
|
||||||
|
// factor per frame — a fixed factor changes speed with the frame rate
|
||||||
|
// and reads as jerky; this keeps the same feel regardless of fps.
|
||||||
|
const positionT = 1 - Math.exp(-POSITION_DAMPING * delta);
|
||||||
|
camera.position.lerp(desiredPosition, positionT);
|
||||||
|
|
||||||
|
const lookT = 1 - Math.exp(-LOOK_DAMPING * delta);
|
||||||
|
this.smoothedLookAt.lerp(targetPosition.clone().add(lookAhead), lookT);
|
||||||
|
camera.lookAt(this.smoothedLookAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only pulls the camera in when an obstacle would actually hide the
|
||||||
|
// character. The check ray starts at (roughly) eye height instead of the
|
||||||
|
// character's center/feet, so something that doesn't reach that high never
|
||||||
|
// triggers a zoom — the character is still visible over it. When it does
|
||||||
|
// trigger, the ray's own hit distance (measured from the character, not
|
||||||
|
// from the camera) is what places the camera, so it lands right next to
|
||||||
|
// the obstacle on the character's side instead of collapsing onto the
|
||||||
|
// character whenever they aren't standing flush against it.
|
||||||
|
private static avoidObstacles(targetPosition: Vector3, desired: Vector3) {
|
||||||
|
if (this.obstacles.length === 0) return;
|
||||||
|
|
||||||
|
const eyePosition = targetPosition.clone().add(new Vector3(0, this.eyeHeight, 0));
|
||||||
|
const toDesired = desired.clone().sub(eyePosition);
|
||||||
|
const distance = toDesired.length();
|
||||||
|
if (distance < 1e-4) return;
|
||||||
|
|
||||||
|
const direction = toDesired.divideScalar(distance);
|
||||||
|
this.raycaster.set(eyePosition, direction);
|
||||||
|
this.raycaster.far = distance;
|
||||||
|
|
||||||
|
const hits = this.raycaster.intersectObjects(this.obstacles, true);
|
||||||
|
if (hits.length === 0) return;
|
||||||
|
|
||||||
|
const safeDistance = Math.max(hits[0].distance - OBSTACLE_SKIN, MIN_DISTANCE);
|
||||||
|
desired.copy(eyePosition).addScaledVector(direction, safeDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import {
|
||||||
|
AnimationAction,
|
||||||
|
AnimationClip,
|
||||||
|
AnimationMixer,
|
||||||
|
Box3,
|
||||||
|
LoopRepeat,
|
||||||
|
Object3D,
|
||||||
|
Quaternion,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
} from "three";
|
||||||
|
import { JoystickC, UpdateController } from "@hitplay/playable_template";
|
||||||
|
import { ThreeC } from "./ThreeC";
|
||||||
|
|
||||||
|
const MOVE_SPEED = 4; // world units per second
|
||||||
|
const START_POSITION = { x: 1, y: 0, z: -8 }; // start of the road, the other end, feet on the ground
|
||||||
|
|
||||||
|
// Collision box used for movement — a rough silhouette of the rigged model
|
||||||
|
// (bbox is ~0.9 x 1.72 x 1.0), not its exact bounds.
|
||||||
|
const PLAYER_SIZE = new Vector3(0.8, 1.7, 0.8);
|
||||||
|
|
||||||
|
export const PLAYER_HEIGHT = PLAYER_SIZE.y;
|
||||||
|
|
||||||
|
// Colliders shorter than this are treated as ground, not obstacles: the
|
||||||
|
// map's floor collider is a ~0.3-unit-thin slab spanning almost the whole
|
||||||
|
// map, while real props (crates, walls) stand ~2 units tall.
|
||||||
|
const MIN_OBSTACLE_HEIGHT = 0.5;
|
||||||
|
|
||||||
|
// Max turn rate, in radians/second — a hard cap on angular speed, not a
|
||||||
|
// proportional ease. Exponential damping (like CameraFollowC uses for
|
||||||
|
// position) closes a fixed percentage of the remaining angle per second, so
|
||||||
|
// a big gap (e.g. a 180° reversal) still covers a huge arc in the very
|
||||||
|
// first frame and reads as a snap. A constant max speed turns a small
|
||||||
|
// correction and a full reversal at the same rate, which reads as smooth.
|
||||||
|
const MAX_TURN_SPEED = Math.PI * 3; // ~540°/s
|
||||||
|
|
||||||
|
const ANIMATION_FADE = 0.2; // seconds, crossfade between idle/walk/attack
|
||||||
|
|
||||||
|
// How far off-center (in the XZ plane) an obstacle can be from the
|
||||||
|
// character's forward direction and still count as "facing it".
|
||||||
|
// dot(forward, toObstacle) > this — 0.5 is a 120°-wide cone (60° each side).
|
||||||
|
const FACING_DOT_THRESHOLD = 0.5;
|
||||||
|
|
||||||
|
// tryMove() rejects the whole step that would overlap an obstacle, so the
|
||||||
|
// player always stops just short of actually touching it (up to one frame's
|
||||||
|
// movement worth of gap) — Box3.intersectsBox on the exact collision box
|
||||||
|
// would basically never see contact. This extra margin, checked only for
|
||||||
|
// the attack/interaction range (not movement), covers that gap.
|
||||||
|
const INTERACTION_REACH = 0.4;
|
||||||
|
|
||||||
|
const UP_AXIS = new Vector3(0, 1, 0);
|
||||||
|
const FORWARD_AXIS = new Vector3(0, 0, 1);
|
||||||
|
|
||||||
|
export class PlayerC {
|
||||||
|
static object: Object3D;
|
||||||
|
|
||||||
|
private static moveInput = new Vector2();
|
||||||
|
private static obstacles: Box3[] = [];
|
||||||
|
private static facingRotation = new Quaternion();
|
||||||
|
|
||||||
|
private static mixer: AnimationMixer;
|
||||||
|
private static idleAction: AnimationAction;
|
||||||
|
private static walkAction: AnimationAction;
|
||||||
|
private static attackAction: AnimationAction;
|
||||||
|
private static currentAction: AnimationAction;
|
||||||
|
private static isAttacking = false;
|
||||||
|
|
||||||
|
private static pistol: Object3D;
|
||||||
|
private static batInHand: Object3D;
|
||||||
|
private static batOnBack: Object3D;
|
||||||
|
|
||||||
|
static init(colliders: Object3D[] = []) {
|
||||||
|
// `object` is a plain container: movement/rotation logic below assumes
|
||||||
|
// its own +z is "forward" (see update()), which happens to already
|
||||||
|
// match the loaded model's rest pose — see createCharacter().
|
||||||
|
this.object = new Object3D();
|
||||||
|
this.object.position.set(START_POSITION.x, START_POSITION.y, START_POSITION.z);
|
||||||
|
this.object.add(this.createCharacter());
|
||||||
|
ThreeC.addToScene(this.object);
|
||||||
|
|
||||||
|
this.obstacles = colliders
|
||||||
|
.map((collider) => new Box3().setFromObject(collider))
|
||||||
|
.filter((box) => box.max.y - box.min.y >= MIN_OBSTACLE_HEIGHT);
|
||||||
|
|
||||||
|
this.bindJoystick();
|
||||||
|
|
||||||
|
UpdateController.Instance.onUpdate.addDelegate((delta) => this.update(delta));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createCharacter() {
|
||||||
|
const character = ThreeC.getObject("character");
|
||||||
|
|
||||||
|
// "Bullet" is a separate rig bundled in the same file for the (not yet
|
||||||
|
// implemented) shooting animations — hide it until it's actually used.
|
||||||
|
const bullet = character.getObjectByName("Bullet");
|
||||||
|
if (bullet) bullet.visible = false;
|
||||||
|
|
||||||
|
ThreeC.setShadowsStateForChildren(character, true, false);
|
||||||
|
|
||||||
|
this.setupAnimations(character);
|
||||||
|
this.setupWeapons(character);
|
||||||
|
|
||||||
|
return character;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static setupWeapons(character: Object3D) {
|
||||||
|
const pistol = character.getObjectByName("Character_Pistol");
|
||||||
|
const batInHand = character.getObjectByName("Tool_1");
|
||||||
|
const batOnBack = character.getObjectByName("Tool_2");
|
||||||
|
|
||||||
|
if (!pistol || !batInHand || !batOnBack) {
|
||||||
|
throw new Error("character mesh is missing the pistol/bat weapon props");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pistol = pistol;
|
||||||
|
this.batInHand = batInHand;
|
||||||
|
this.batOnBack = batOnBack;
|
||||||
|
|
||||||
|
this.equipPistol();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exactly one of {pistol, bat} is ever in hand, and the other is stowed —
|
||||||
|
// the pistol has no separate "in hand" prop to show (see PlayerC chat
|
||||||
|
// notes), so its own single mesh just toggles at its holster spot instead.
|
||||||
|
private static equipPistol() {
|
||||||
|
this.pistol.visible = true;
|
||||||
|
this.batInHand.visible = false;
|
||||||
|
this.batOnBack.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static equipBat() {
|
||||||
|
this.pistol.visible = false;
|
||||||
|
this.batInHand.visible = true;
|
||||||
|
this.batOnBack.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static setupAnimations(character: Object3D) {
|
||||||
|
this.mixer = new AnimationMixer(character);
|
||||||
|
ThreeC.addAnimMixer(this.mixer);
|
||||||
|
|
||||||
|
const clips: AnimationClip[] = ThreeC.getAnimations("character");
|
||||||
|
const idleClip = AnimationClip.findByName(clips, "Idle");
|
||||||
|
const walkClip = AnimationClip.findByName(clips, "SlowWalk");
|
||||||
|
const attackClip = AnimationClip.findByName(clips, "Loot");
|
||||||
|
|
||||||
|
if (!idleClip || !walkClip || !attackClip) {
|
||||||
|
throw new Error("character mesh is missing the Idle/SlowWalk/Loot animation clips");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.idleAction = this.mixer.clipAction(idleClip);
|
||||||
|
this.walkAction = this.mixer.clipAction(walkClip);
|
||||||
|
this.attackAction = this.mixer.clipAction(attackClip);
|
||||||
|
this.attackAction.setLoop(LoopRepeat, Infinity);
|
||||||
|
|
||||||
|
this.currentAction = this.idleAction;
|
||||||
|
this.currentAction.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static playAction(action: AnimationAction) {
|
||||||
|
if (this.currentAction === action) return;
|
||||||
|
|
||||||
|
action.reset().fadeIn(ANIMATION_FADE).play();
|
||||||
|
this.currentAction.fadeOut(ANIMATION_FADE);
|
||||||
|
this.currentAction = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bindJoystick() {
|
||||||
|
JoystickC.onJoysticMove.addDelegate(({ data }) => {
|
||||||
|
// nipplejs clamps the drag to a circle (not per-axis) whenever
|
||||||
|
// `follow: false` — see JoystickC.init in beforeResourcesLoadedCb —
|
||||||
|
// so `vector` is already length <= 1 and diagonals aren't faster.
|
||||||
|
this.moveInput.set(data.vector.x, data.vector.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
JoystickC.onJoysticEnd.addDelegate(() => {
|
||||||
|
this.moveInput.set(0, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static update(delta: number) {
|
||||||
|
const inputMagnitude = this.moveInput.length();
|
||||||
|
|
||||||
|
if (inputMagnitude > 0) {
|
||||||
|
// Matches the position mapping below (-x -> x, y -> z): the angle a
|
||||||
|
// movement vector needs to rotate the character's default +z-facing
|
||||||
|
// front to point the same way. x is negated because the camera now
|
||||||
|
// looks toward +Z (see TestSceneC's CAMERA_OFFSET) — that's a 180°
|
||||||
|
// yaw from the default view, which mirrors world +X to screen-left,
|
||||||
|
// so moving/facing needs the same mirror to keep "stick right" mean
|
||||||
|
// "screen right".
|
||||||
|
const angle = Math.atan2(-this.moveInput.x, this.moveInput.y);
|
||||||
|
this.facingRotation.setFromAxisAngle(UP_AXIS, angle);
|
||||||
|
|
||||||
|
const step = MOVE_SPEED * delta;
|
||||||
|
|
||||||
|
// Move one axis at a time so a wall blocking one direction still lets
|
||||||
|
// the player slide along it, instead of getting fully stuck.
|
||||||
|
this.tryMove(-this.moveInput.x * step, 0);
|
||||||
|
this.tryMove(0, this.moveInput.y * step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn toward the last movement direction and hold it while idle,
|
||||||
|
// rather than resetting to face forward the moment input stops.
|
||||||
|
// rotateTowards caps the step at MAX_TURN_SPEED * delta radians instead
|
||||||
|
// of interpolating a percentage of the remaining angle. Moving or
|
||||||
|
// turning away from the obstacle is exactly what breaks updateCombat()'s
|
||||||
|
// facing check below — no separate "walked/turned away" case needed.
|
||||||
|
this.object.quaternion.rotateTowards(this.facingRotation, MAX_TURN_SPEED * delta);
|
||||||
|
|
||||||
|
this.updateCombat();
|
||||||
|
|
||||||
|
if (!this.isAttacking) {
|
||||||
|
if (inputMagnitude > 0) {
|
||||||
|
// Match the walk cycle's playback speed to how hard the stick is
|
||||||
|
// pushed, not just whether it's pushed — otherwise a small nudge
|
||||||
|
// still plays the animation at full speed while the character
|
||||||
|
// barely moves, and the feet visibly skate across the ground.
|
||||||
|
this.walkAction.timeScale = inputMagnitude;
|
||||||
|
this.playAction(this.walkAction);
|
||||||
|
} else {
|
||||||
|
this.playAction(this.idleAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attacking requires standing next to a destructible AND facing it — pure
|
||||||
|
// proximity (e.g. backing into a crate) shouldn't swing the bat. Re-reads
|
||||||
|
// both conditions every frame, so walking or turning away — or the
|
||||||
|
// obstacle later being removed by a destruction system — stops it
|
||||||
|
// automatically, with no separate "stop" case to maintain.
|
||||||
|
private static updateCombat() {
|
||||||
|
const obstacleCenter = this.getFacingObstacleCenter();
|
||||||
|
const isAttacking = obstacleCenter !== null;
|
||||||
|
|
||||||
|
if (isAttacking === this.isAttacking) return;
|
||||||
|
this.isAttacking = isAttacking;
|
||||||
|
|
||||||
|
if (isAttacking) {
|
||||||
|
this.equipBat();
|
||||||
|
this.playAction(this.attackAction);
|
||||||
|
} else {
|
||||||
|
this.equipPistol();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the center of the destructible obstacle the player is touching
|
||||||
|
// AND facing, or null if there isn't one.
|
||||||
|
private static getFacingObstacleCenter(): Vector3 | null {
|
||||||
|
const bounds = this.getCollisionBounds(this.object.position).expandByScalar(INTERACTION_REACH);
|
||||||
|
const touching = this.obstacles.find((box) => box.intersectsBox(bounds));
|
||||||
|
if (!touching) return null;
|
||||||
|
|
||||||
|
const center = touching.getCenter(new Vector3());
|
||||||
|
|
||||||
|
const toObstacle = center.clone().sub(this.object.position);
|
||||||
|
toObstacle.y = 0;
|
||||||
|
if (toObstacle.lengthSq() < 1e-6) return center; // standing right on top of it — count as facing
|
||||||
|
|
||||||
|
toObstacle.normalize();
|
||||||
|
const forward = FORWARD_AXIS.clone().applyQuaternion(this.object.quaternion);
|
||||||
|
forward.y = 0;
|
||||||
|
forward.normalize();
|
||||||
|
|
||||||
|
return forward.dot(toObstacle) > FACING_DOT_THRESHOLD ? center : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static tryMove(dx: number, dz: number) {
|
||||||
|
const nextPosition = this.object.position.clone();
|
||||||
|
nextPosition.x += dx;
|
||||||
|
nextPosition.z += dz;
|
||||||
|
|
||||||
|
const nextBounds = this.getCollisionBounds(nextPosition);
|
||||||
|
const blocked = this.obstacles.some((box) => box.intersectsBox(nextBounds));
|
||||||
|
if (blocked) return;
|
||||||
|
|
||||||
|
this.object.position.copy(nextPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// position is anchored at the feet, but the collision box should be
|
||||||
|
// centered on the body
|
||||||
|
private static getCollisionBounds(position: Vector3): Box3 {
|
||||||
|
const center = position.clone();
|
||||||
|
center.y += PLAYER_SIZE.y / 2;
|
||||||
|
return new Box3().setFromCenterAndSize(center, PLAYER_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,44 @@
|
|||||||
|
import { Object3D, Vector3 } from "three";
|
||||||
import { ThreeC } from "./ThreeC";
|
import { ThreeC } from "./ThreeC";
|
||||||
import { InputC, JoystickC } from "@hitplay/playable_template";
|
import { PlayerC, PLAYER_HEIGHT } from "./PlayerC";
|
||||||
|
import { CameraFollowC } from "./CameraFollowC";
|
||||||
|
|
||||||
|
// Above and behind the player, along the road's forward (+Z) direction —
|
||||||
|
// player now spawns at the other end of the road, walking toward +Z, so
|
||||||
|
// "behind" flipped from +Z to -Z to keep looking the same way they walk.
|
||||||
|
const CAMERA_OFFSET = new Vector3(0, 9, -9);
|
||||||
|
|
||||||
|
// Obstacle checks look from roughly head height rather than the player's
|
||||||
|
// feet (position.y is anchored at the feet), so short obstacles don't
|
||||||
|
// falsely count as hiding the player. 0.9 of the full height keeps the ray
|
||||||
|
// just under the very top edge, avoiding edge-grazing false negatives.
|
||||||
|
const CAMERA_EYE_HEIGHT = PLAYER_HEIGHT * 0.9;
|
||||||
|
|
||||||
export class TestSceneC {
|
export class TestSceneC {
|
||||||
static init() {
|
static init() {
|
||||||
this.createMap();
|
const colliders = this.createMap();
|
||||||
|
|
||||||
// example of using InputC events
|
PlayerC.init(colliders);
|
||||||
InputC.onTouchDown.addDelegate((event) => {
|
CameraFollowC.init(PlayerC.object, CAMERA_OFFSET, colliders, CAMERA_EYE_HEIGHT);
|
||||||
console.log("onMouseDown", event);
|
|
||||||
});
|
|
||||||
|
|
||||||
// if you have update in your controller
|
|
||||||
// UpdateController.Instance.onUpdate.addDelegate(() => {
|
|
||||||
// this.update();
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createMap() {
|
private static createMap() {
|
||||||
const map = ThreeC.getObject("map");
|
const map = ThreeC.getObject("map");
|
||||||
|
const colliders: Object3D[] = [];
|
||||||
|
|
||||||
// physics-only collision proxies exported alongside the visual meshes; hide them
|
// physics-only collision proxies exported alongside the visual meshes;
|
||||||
|
// hide them from render, but keep them as camera-obstacle geometry
|
||||||
map.traverse((child) => {
|
map.traverse((child) => {
|
||||||
if (/collider/i.test(child.name)) {
|
if (/collider/i.test(child.name)) {
|
||||||
child.visible = false;
|
child.visible = false;
|
||||||
|
colliders.push(child);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ThreeC.setShadowsStateForChildren(map, true, true);
|
ThreeC.setShadowsStateForChildren(map, true, true);
|
||||||
|
|
||||||
ThreeC.addToScene(map);
|
ThreeC.addToScene(map);
|
||||||
|
|
||||||
|
return colliders;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -9,6 +9,10 @@ export const meshes : ConvertResourceType = {
|
|||||||
name: "map",
|
name: "map",
|
||||||
value: ConvertToBase64WhenRelease("./ZombiePunk_Map.glb"),
|
value: ConvertToBase64WhenRelease("./ZombiePunk_Map.glb"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "character",
|
||||||
|
value: ConvertToBase64WhenRelease("./ZombiePunk_Character.glb"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
loader: Template3d.meshLoader
|
loader: Template3d.meshLoader
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
CameraC_internal,
|
CameraC_internal,
|
||||||
CameraType,
|
CameraType,
|
||||||
// JoystickC,
|
JoystickC,
|
||||||
Physics_internal,
|
Physics_internal,
|
||||||
Template,
|
Template,
|
||||||
Template3d,
|
Template3d,
|
||||||
@@ -26,21 +26,15 @@ export const beforeResourcesLoadedCb = () => {
|
|||||||
|
|
||||||
Physics_internal.init(new Vec3(0, -9.81, 0));
|
Physics_internal.init(new Vec3(0, -9.81, 0));
|
||||||
|
|
||||||
// example of using joystick. Uncomment if you need joystick
|
// zone is omitted on purpose: JoystickC auto-creates a full-screen zone
|
||||||
|
// when none is given, so the joystick can appear wherever the player taps
|
||||||
// JoystickC.init({
|
JoystickC.init({
|
||||||
// zone: document.getElementById("joystick_zone") as HTMLDivElement,
|
mode: "dynamic",
|
||||||
// fadeTime: 0,
|
fadeTime: 100,
|
||||||
// mode: "dynamic",
|
restJoystick: true,
|
||||||
// restJoystick: true,
|
restOpacity: 0,
|
||||||
// catchDistance: 1,
|
follow: false,
|
||||||
// restOpacity: 0,
|
});
|
||||||
// follow: false,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// JoystickC.onJoysticMove.addDelegate(({event, data}) => {
|
|
||||||
// console.log('onJoysticMove', event, data);
|
|
||||||
// })
|
|
||||||
|
|
||||||
Template.updateVariableConfig.addDelegate(([category, variable, value]) => {
|
Template.updateVariableConfig.addDelegate(([category, variable, value]) => {
|
||||||
if (category === "global") {
|
if (category === "global") {
|
||||||
|
|||||||
Reference in New Issue
Block a user