I thought I should share this with you.
It’s a code that I modified and edited to my game needs. It includes walking, running, and crouching.
Note: it does not have jumping
here you go.
var FirstPersonMovementV2 = pc.createScript('firstPersonMovementV2');
FirstPersonMovementV2.attributes.add('camera', {
type: 'entity',
description: 'Optional, assign a camera entity, otherwise one is created'
});
FirstPersonMovementV2.attributes.add('power', {
type: 'number',
default: 2500,
description: 'Adjusts the speed of player movement'
});
FirstPersonMovementV2.attributes.add('powerRun', {
type: 'number',
default: 2500,
description: 'Adjusts the speed of player running'
});
FirstPersonMovementV2.attributes.add('powerCrouching', {
type: 'number',
default: 2000,
description: 'Adjusts the speed of player movement when crouching'
});
FirstPersonMovementV2.attributes.add('lookSpeed', {
type: 'number',
default: 0.25,
description: 'Adjusts the sensitivity of looking'
});
FirstPersonMovementV2.attributes.add('cameraHeight', {
type: 'number',
default: 0.5,
description: 'Adjusts the Y position of the camera'
});
FirstPersonMovementV2.attributes.add('cameraSmoothingUp', {
type: 'number',
default: 0.25,
description: 'Camera smooth amount for crouching'
});
FirstPersonMovementV2.attributes.add('cameraSmoothingDown', {
type: 'number',
default: 0.15,
description: 'Camera smooth amount for crouching'
});
FirstPersonMovementV2.attributes.add('playerHeight', {
type: 'number',
default: 2.0,
description: 'Adjusts the Y position of the camera'
});
FirstPersonMovementV2.attributes.add('debug', {
type: 'boolean',
default: false,
description: 'Debug player movement'
});
// initialize code called once per entity
FirstPersonMovementV2.prototype.initialize = function() {
// player child entities
this.playerCollisionUpper = this.entity.findByName('player-collision-upper');
this.playerCollisionLower = this.entity.findByName('player-collision-lower');
this.playerModel = this.entity.findByName('player-model');
// physics
this.force = new pc.Vec3();
this.eulers = new pc.Vec3();
this.velocity = new pc.Vec3();
this.crouchRaycastStart = new pc.Vec3();
this.crouchRaycastEnd = new pc.Vec3();
this.crouchRaycastLastAngle = 0;
this.playerHeightCrouching = this.playerCollisionLower.collision.height;
// player data
this.playerSpawnPosition = this.entity.getPosition().clone();
this.playerHeightCurrent = this.playerHeight;
// states
this.playerMoving = false;
this.playerCrouching = false;
this.playerJumping = false;
this.playerRunning = false;
// (optional) enable ccd, prevents rigidbody clipping through at high speeds
const body = this.entity.rigidbody.body;
body.setCcdMotionThreshold(1);
body.setCcdSweptSphereRadius(0.1);
body.setContactProcessingThreshold(0.1);
// camera
this.cameraHeightCurrent = this.playerHeight * this.cameraHeight;
// reparent camera if not a child of this entity
if (this.entity.children.indexOf(this.camera) < 0) {
this.camera.reparent( this.entity );
}
// debug
this.debugThirdPerson = false; // toggle with 'X' key
this.debugCamLocalPosition = new pc.Vec3();
// events
var app = this.app;
// Listen for mouse move events
app.mouse.on("mousemove", this._onMouseMove, this);
// when the mouse is clicked hide the cursor
app.mouse.on("mousedown", function () {
app.mouse.enablePointerLock();
}, this);
// Check for required components
if (!this.entity.collision) {
console.error("First Person Movement script needs to have a 'collision' component");
}
if (!this.entity.rigidbody || this.entity.rigidbody.type !== pc.BODYTYPE_DYNAMIC) {
console.error("First Person Movement script needs to have a DYNAMIC 'rigidbody' component");
}
};
FirstPersonMovementV2.prototype.shoot = function() {
var bullet = this.entity.findByName('Bullet').clone();
this.app.root.addChild(bullet);
bullet.setLocalScale(0.055,0.055,0.055);
bullet.setPosition(this.entity.findByName('Bullet').getPosition());
bullet.setRotation(this.entity.findByName('Bullet').getRotation());
bullet.enabled = true;
};
// update code called every frame
FirstPersonMovementV2.prototype.update = function(dt) {
// If a camera isn't assigned from the Editor, create one
if (!this.camera) {
this._createCamera();
}
const force = this.force;
const app = this.app;
// Get camera directions to determine movement directions
const forward = this.camera.forward;
const right = this.camera.right;
// movement
let x = 0;
let z = 0;
// attributes
let playerHeight = this.playerHeight;
let power = this.power;
let powerRun = this.power*3;
// Movement mechanic: input
// Use W-A-S-D keys to move player
// Check for key presses
// start shooting
if (this.app.mouse.isPressed(pc.KEY_E)) {
this.timer += dt;
if (this.timer > 0.5) {
this.timer = 0;
this.shoot();
}
}
if (app.keyboard.isPressed(pc.KEY_A) || app.keyboard.isPressed(pc.KEY_Q)) {
x -= right.x;
z -= right.z;
}
if (app.keyboard.isPressed(pc.KEY_D)) {
x += right.x;
z += right.z;
}
if (app.keyboard.isPressed(pc.KEY_W)) {
x += forward.x;
z += forward.z;
}
if (app.keyboard.isPressed(pc.KEY_S)) {
x -= forward.x;
z -= forward.z;
}
if (app.keyboard.isPressed(pc.KEY_SHIFT) && app.keyboard.isPressed(pc.KEY_W)) {
x += forward.x;
z += forward.z;
power = powerRun;
playerRunning = true;
}
else(
(playerRunning = false)
);
// Crouching mechanic: input
if (app.keyboard.isPressed(pc.KEY_C)) {
this.playerCrouching = true;
}
else if (this.playerCrouching) {
// verify that there is room to stand up
this.playerCrouching = this.raycastCrouch();
}
// Crouching mechanic: apply
if (this.playerCrouching) {
playerHeight = this.playerHeightCrouching;
power = this.powerCrouching;
}
this._updateCrouchMechanic( playerHeight, dt );
// Movement mechanic: use direction from keypresses to apply a force to the character
this.playerMoving = x !== 0 && z !== 0;
if (this.playerMoving) {
force.set(x, 0, z).normalize().scale(power);
this.entity.rigidbody.applyForce(force);
}
// Running mechanic: run like mad
this.playerRunning = x !== 0 && z !== 0;
if (this.playerRunning) {
force.set(x, 0, z).normalize().scale(powerRun);
this.entity.rigidbody.applyForce(force);
}
// respawn when falling off map
if (this.entity.getPosition().y < -60) {
this.entity.rigidbody.teleport(0, -45, 0);
this.entity.rigidbody.linearVelocity = this.velocity.set(0, 0, 0);
this.entity.rigidbody.angularVelocity = new pc.Vec3(0, 0, 0);
}
// update camera
this._updateCamera();
};
//
// Utilities
//
FirstPersonMovementV2.prototype._updateCrouchMechanic = function (heightTarget, dt) {
// on height change
let height = this.playerHeightCurrent;
if (height !== heightTarget) {
this.playerHeightCurrent = heightTarget;
// (optional) instantly scale player model
//this.playerModel.setLocalScale(1, heightTarget * 0.5, 1);
// (optional) instantly offset player model. usefull for a crouching skeletal animations.
//this.playerModel.setLocalPosition(0, (this.playerHeight - heightTarget) * 0.5, 0);
// disable the upper collision body when crouching
const upperCollisionEnabled = this.playerCollisionUpper.collision.enabled;
if (this.playerCrouching) {
if (upperCollisionEnabled) {
this.playerCollisionUpper.collision.enabled = false;
this.playerCollisionUpper.enabled = false;
}
}
else {
if (!upperCollisionEnabled) {
this.playerCollisionUpper.collision.enabled = true;
this.playerCollisionUpper.enabled = true;
}
}
}
// smoothing //
// setup smoothing speed based on going up or down
const smoothing = this.playerCrouching ? this.cameraSmoothingDown : this.cameraSmoothingUp;
const t = Math.min(dt / smoothing, 1.0);
/// update camera height smooth
let cameraHeightTarget = heightTarget * this.cameraHeight * 0.5; // - this.playerHeight * 0.5;
this.cameraHeightCurrent = pc.math.lerp(this.cameraHeightCurrent, cameraHeightTarget, t);
/// (optional) update player model smooth, not ideal for skeletal animations.
// scale
let playerModelScaleY = this.playerModel.getLocalScale().y;
playerModelScaleY = pc.math.lerp( playerModelScaleY, heightTarget * 0.5, t);
this.playerModel.setLocalScale(1, playerModelScaleY, 1);
// pos
let playerModelPosY = this.playerModel.getLocalPosition().y;
const scaleDelta = - playerModelScaleY;
this.playerModel.setLocalPosition(0, -scaleDelta, 0);
};
// Raycast above player when in Crouching State
FirstPersonMovementV2.prototype.raycastCrouch = function() {
const height = this.playerHeightCurrent;
const radius = this.playerCollisionLower.collision.radius;
const positionStart = this.playerCollisionLower.getPosition();
const positionEntity = this.entity.getPosition();
const padding = -0.15; // add a little buffer to radius to cast from inside
const precision = 9; // amount of raycasters to cast
// raycaster is colliding with rigidbody
let colliding = false;
// nifty optimization (starts angle at last hit angle)
let angleOffset = this.crouchRaycastLastAngle;
// make a circle of raycasters to cast above player
for (let i = 0; i < precision; i ++) {
// setup raycaster positions
this.crouchRaycastStart.copy(positionStart);
this.crouchRaycastEnd.copy(positionEntity);
this.crouchRaycastEnd.y += this.playerHeight;
// offset raycaster positions in a circle, index 0 reserved for center raycast
let phi = 0;
if (i !== 0) {
const len = precision - 1;
const slice = ((i - 1) / len);
const pizza = 360 * pc.math.DEG_TO_RAD;
phi = (pizza * slice) + angleOffset;
const x = Math.cos( phi ) * (radius + padding);
const z = Math.sin( phi ) * (radius + padding);
this.crouchRaycastStart.x += x;
this.crouchRaycastStart.z += z;
this.crouchRaycastEnd.x += x;
this.crouchRaycastEnd.z += z;
}
// raycast from center to to player height
const result = this.app.systems.rigidbody.raycastFirst(this.crouchRaycastStart, this.crouchRaycastEnd);
// is raycaster colliding with a rigidbody?
colliding = result && result.entity.rigidbody;
// debug: render line
if (this.debugThirdPerson && this.debug) {
this.app.renderLine(this.crouchRaycastStart, this.crouchRaycastEnd, colliding ? pc.Color.RED : pc.Color.GREEN);
}
// exit loop when collision success
if (colliding) {
this.crouchRaycastLastAngle = phi;
break;
}
}
return colliding;
};
FirstPersonMovementV2.prototype._createCamera = function () {
// If user hasn't assigned a camera, create a new one
this.camera = new pc.Entity();
this.camera.setName("First Person Camera");
this.camera.addComponent("camera");
this.entity.addChild(this.camera);
this.camera.translateLocal(0, this.cameraHeight, 0);
};
FirstPersonMovementV2.prototype._updateCamera = function () {
// update camera angle from mouse events
this.camera.setLocalEulerAngles(this.eulers.y, this.eulers.x, 0);
// update position
this.camera.setLocalPosition(0, this.cameraHeightCurrent, 0);
// debug: third person camera
this.debugThirdPersonCamera();
};
//
// Input
//
FirstPersonMovementV2.prototype._onMouseMove = function (e) {
// If pointer is disabled
// If the left mouse button is down update the camera from mouse movement
if (pc.Mouse.isPointerLocked() || e.buttons[0]) {
this.eulers.x -= this.lookSpeed * e.dx;
this.eulers.y -= this.lookSpeed * e.dy;
}
};
//
// Debug
//
// simple third person camera
FirstPersonMovementV2.prototype.debugThirdPersonCamera = function() {
const camForward = this.camera.forward;
const camDistance = 6;
// toggle camera
if (this.app.keyboard.wasPressed(pc.KEY_X)) {
this.debugThirdPerson = !this.debugThirdPerson;
// on toggle state change
if (this.debugThirdPerson) {
// setup cam for TPV
const camLocalPosition = this.camera.getLocalPosition();
this.debugCamLocalPosition.copy( camLocalPosition );
}
else {
// reset cam to FPV
this.camera.setLocalPosition( 0, this.cameraHeightCurrent, 0);
}
}
// update camera position
if (this.debugThirdPerson) {
const cameraHeight = this.cameraHeightCurrent;
this.camera.setLocalPosition(
-camForward.x * camDistance,
-camForward.y * camDistance + cameraHeight,
-camForward.z * camDistance);
}
};