mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00

* New actions are available to save & load the game state, making Saving & Loading as easy as adding 1 action to your game! * While it will work in most cases, it has a few limitations and hasn't been thoroughly tested on all types of objects/behaviors and games, so it is for the moment tagged as **Experimental** while we gather feedback and improve it * Check out the wiki for more info: https://wiki.gdevelop.io/gdevelop5/all-features/save-state
1960 lines
65 KiB
TypeScript
1960 lines
65 KiB
TypeScript
/// <reference path="./jolt-physics.d.ts" />
|
|
|
|
namespace gdjs {
|
|
interface PhysicsCharacter3DNetworkSyncDataType {
|
|
sma: float;
|
|
shm: float;
|
|
grav: float;
|
|
mfs: float;
|
|
facc: float;
|
|
fdec: float;
|
|
fsm: float;
|
|
sacc: float;
|
|
sdec: float;
|
|
ssm: float;
|
|
jumpspeed: float;
|
|
jumpsustime: float;
|
|
sbpa: boolean;
|
|
fwa: float;
|
|
fws: float;
|
|
sws: float;
|
|
cfs: float;
|
|
cjs: float;
|
|
cj: boolean;
|
|
lek: boolean;
|
|
rik: boolean;
|
|
upk: boolean;
|
|
dok: boolean;
|
|
juk: boolean;
|
|
us: boolean;
|
|
sa: float;
|
|
sf: float;
|
|
tscjs: float;
|
|
jkhsjs: boolean;
|
|
}
|
|
|
|
export interface PhysicsCharacter3DNetworkSyncData
|
|
extends BehaviorNetworkSyncData {
|
|
props: PhysicsCharacter3DNetworkSyncDataType;
|
|
}
|
|
|
|
type Physics3D = {
|
|
behavior: gdjs.Physics3DRuntimeBehavior;
|
|
extendedUpdateSettings: Jolt.ExtendedUpdateSettings;
|
|
broadPhaseLayerFilter: Jolt.BroadPhaseLayerFilter;
|
|
objectLayerFilter: Jolt.ObjectLayerFilter;
|
|
bodyFilter: Jolt.BodyFilter;
|
|
shapeFilter: Jolt.ShapeFilter;
|
|
};
|
|
|
|
export class PhysicsCharacter3DRuntimeBehavior
|
|
extends gdjs.RuntimeBehavior
|
|
implements gdjs.Physics3DRuntimeBehavior.Physics3DHook
|
|
{
|
|
owner3D: gdjs.RuntimeObject3D;
|
|
private _physics3DBehaviorName: string;
|
|
private _physics3D: Physics3D | null = null;
|
|
private _isHookedToPhysicsStep = false;
|
|
character: Jolt.CharacterVirtual | null = null;
|
|
/**
|
|
* sharedData is a reference to the shared data of the scene, that registers
|
|
* every physics behavior that is created so that collisions can be cleared
|
|
* before stepping the world.
|
|
*/
|
|
_sharedData: gdjs.Physics3DSharedData;
|
|
collisionChecker: gdjs.PhysicsCharacter3DRuntimeBehavior.CharacterCollisionChecker;
|
|
private _destroyedDuringFrameLogic: boolean = false;
|
|
|
|
// TODO Should there be angle were the character can climb but will slip?
|
|
_slopeMaxAngle: float;
|
|
private _slopeClimbingFactor: float = 1;
|
|
private _slopeClimbingMinNormalZ: float = Math.cos(Math.PI / 4);
|
|
private _forwardAngle: float = 0;
|
|
private _shouldBindObjectAndForwardAngle: boolean;
|
|
private _forwardAcceleration: float;
|
|
private _forwardDeceleration: float;
|
|
private _forwardSpeedMax: float;
|
|
private _sidewaysAcceleration: float;
|
|
private _sidewaysDeceleration: float;
|
|
private _sidewaysSpeedMax: float;
|
|
private _gravity: float;
|
|
private _maxFallingSpeed: float;
|
|
private _jumpSpeed: float;
|
|
private _jumpSustainTime: float;
|
|
private _stairHeightMax: float;
|
|
_canBePushed: boolean;
|
|
|
|
private _hasPressedForwardKey: boolean = false;
|
|
private _hasPressedBackwardKey: boolean = false;
|
|
private _hasPressedRightKey: boolean = false;
|
|
private _hasPressedLeftKey: boolean = false;
|
|
private _hasPressedJumpKey: boolean = false;
|
|
private _hasUsedStick: boolean = false;
|
|
private _stickAngle: float = 0;
|
|
private _stickForce: float = 0;
|
|
private _currentForwardSpeed: float = 0;
|
|
private _currentSidewaysSpeed: float = 0;
|
|
private _currentFallSpeed: float = 0;
|
|
private _canJump: boolean = false;
|
|
private _currentJumpSpeed: float = 0;
|
|
private _timeSinceCurrentJumpStart: float = 0;
|
|
private _jumpKeyHeldSinceJumpStart: boolean = false;
|
|
private _hasReallyMoved: boolean = false;
|
|
private _oldPhysicsPosition: FloatPoint = [0, 0];
|
|
|
|
// This is useful for extensions that need to know
|
|
// which keys were pressed and doesn't know the mapping
|
|
// done by the scene events.
|
|
private _wasLeftKeyPressed: boolean = false;
|
|
private _wasRightKeyPressed: boolean = false;
|
|
private _wasForwardKeyPressed: boolean = false;
|
|
private _wasBackwardKeyPressed: boolean = false;
|
|
private _wasJumpKeyPressed: boolean = false;
|
|
private _wasStickUsed: boolean = false;
|
|
|
|
// This is useful when the object is synchronized by an external source
|
|
// like in a multiplayer game, and we want to be able to predict the
|
|
// movement of the object, even if the inputs are not updated every frame.
|
|
private _clearInputsBetweenFrames: boolean = true;
|
|
|
|
/**
|
|
* A very small value compare to 1 pixel, yet very huge compare to rounding errors.
|
|
*/
|
|
private static readonly epsilon = 2 ** -20;
|
|
|
|
/** Handle collisions between characters that can push each other. */
|
|
charactersManager: gdjs.PhysicsCharacter3DRuntimeBehavior.CharactersManager;
|
|
|
|
constructor(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
behaviorData,
|
|
owner: gdjs.RuntimeObject3D
|
|
) {
|
|
super(instanceContainer, behaviorData, owner);
|
|
this.owner3D = owner;
|
|
this._physics3DBehaviorName = behaviorData.physics3D;
|
|
this._sharedData = gdjs.Physics3DSharedData.getSharedData(
|
|
instanceContainer.getScene(),
|
|
behaviorData.Physics3D
|
|
);
|
|
this.collisionChecker =
|
|
new gdjs.PhysicsCharacter3DRuntimeBehavior.CharacterCollisionChecker(
|
|
this
|
|
);
|
|
this.charactersManager =
|
|
gdjs.PhysicsCharacter3DRuntimeBehavior.CharactersManager.getManager(
|
|
instanceContainer
|
|
);
|
|
|
|
this._slopeMaxAngle = 0;
|
|
this.setSlopeMaxAngle(behaviorData.slopeMaxAngle);
|
|
this._forwardAcceleration = behaviorData.forwardAcceleration;
|
|
this._forwardDeceleration = behaviorData.forwardDeceleration;
|
|
this._forwardSpeedMax = behaviorData.forwardSpeedMax;
|
|
this._sidewaysAcceleration = behaviorData.sidewaysAcceleration;
|
|
this._sidewaysDeceleration = behaviorData.sidewaysDeceleration;
|
|
this._sidewaysSpeedMax = behaviorData.sidewaysSpeedMax;
|
|
this._gravity = behaviorData.gravity;
|
|
this._maxFallingSpeed = behaviorData.fallingSpeedMax;
|
|
this._jumpSustainTime = behaviorData.jumpSustainTime;
|
|
this._jumpSpeed = this.getJumpSpeedToReach(behaviorData.jumpHeight);
|
|
this._shouldBindObjectAndForwardAngle =
|
|
behaviorData.shouldBindObjectAndForwardAngle;
|
|
this._stairHeightMax =
|
|
behaviorData.stairHeightMax === undefined
|
|
? 20
|
|
: behaviorData.stairHeightMax;
|
|
this._canBePushed =
|
|
behaviorData.canBePushed === undefined
|
|
? true
|
|
: behaviorData.canBePushed;
|
|
}
|
|
|
|
private getVec3(x: float, y: float, z: float): Jolt.Vec3 {
|
|
const tempVec3 = this._sharedData._tempVec3;
|
|
tempVec3.Set(x, y, z);
|
|
return tempVec3;
|
|
}
|
|
|
|
getPhysics3D(): Physics3D | null {
|
|
if (this._destroyedDuringFrameLogic) {
|
|
return null;
|
|
}
|
|
if (this._physics3D) {
|
|
return this._physics3D;
|
|
}
|
|
if (!this.activated()) {
|
|
return null;
|
|
}
|
|
const behavior = this.owner.getBehavior(
|
|
this._physics3DBehaviorName
|
|
) as gdjs.Physics3DRuntimeBehavior;
|
|
if (!behavior.activated()) {
|
|
return null;
|
|
}
|
|
const sharedData = behavior._sharedData;
|
|
const jolt = sharedData.jolt;
|
|
const extendedUpdateSettings = new Jolt.ExtendedUpdateSettings();
|
|
const broadPhaseLayerFilter = new Jolt.DefaultBroadPhaseLayerFilter(
|
|
jolt.GetObjectVsBroadPhaseLayerFilter(),
|
|
gdjs.Physics3DSharedData.dynamicBroadPhaseLayerIndex
|
|
);
|
|
const objectLayerFilter = new Jolt.DefaultObjectLayerFilter(
|
|
jolt.GetObjectLayerPairFilter(),
|
|
behavior.getBodyLayer()
|
|
);
|
|
const bodyFilter = new Jolt.BodyFilter();
|
|
const shapeFilter = new Jolt.ShapeFilter();
|
|
|
|
this._physics3D = {
|
|
behavior,
|
|
extendedUpdateSettings,
|
|
broadPhaseLayerFilter,
|
|
objectLayerFilter,
|
|
bodyFilter,
|
|
shapeFilter,
|
|
};
|
|
this.setStairHeightMax(this._stairHeightMax);
|
|
if (!this._isHookedToPhysicsStep) {
|
|
sharedData.registerHook(this);
|
|
this._isHookedToPhysicsStep = true;
|
|
}
|
|
|
|
// Destroy the body before switching the bodyUpdater,
|
|
// to ensure the body of the previous bodyUpdater is not left alive.
|
|
// (would be a memory leak and would create a phantom body in the physics world)
|
|
// But transfer the linear and angular velocity to the new body,
|
|
// so the body doesn't stop when it is recreated.
|
|
let previousBodyData = {
|
|
linearVelocityX: 0,
|
|
linearVelocityY: 0,
|
|
linearVelocityZ: 0,
|
|
angularVelocityX: 0,
|
|
angularVelocityY: 0,
|
|
angularVelocityZ: 0,
|
|
};
|
|
if (behavior._body) {
|
|
const linearVelocity = behavior._body.GetLinearVelocity();
|
|
previousBodyData.linearVelocityX = linearVelocity.GetX();
|
|
previousBodyData.linearVelocityY = linearVelocity.GetY();
|
|
previousBodyData.linearVelocityZ = linearVelocity.GetZ();
|
|
const angularVelocity = behavior._body.GetAngularVelocity();
|
|
previousBodyData.angularVelocityX = angularVelocity.GetX();
|
|
previousBodyData.angularVelocityY = angularVelocity.GetY();
|
|
previousBodyData.angularVelocityZ = angularVelocity.GetZ();
|
|
behavior.bodyUpdater.destroyBody();
|
|
}
|
|
|
|
behavior.bodyUpdater =
|
|
new gdjs.PhysicsCharacter3DRuntimeBehavior.CharacterBodyUpdater(this);
|
|
behavior.collisionChecker = this.collisionChecker;
|
|
behavior.recreateBody(previousBodyData);
|
|
|
|
// Always begin in the direction of the object.
|
|
this._forwardAngle = this.owner.getAngle();
|
|
|
|
return this._physics3D;
|
|
}
|
|
|
|
override updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
|
|
if (oldBehaviorData.gravity !== newBehaviorData.gravity) {
|
|
this.setGravity(newBehaviorData.gravity);
|
|
}
|
|
if (oldBehaviorData.maxFallingSpeed !== newBehaviorData.maxFallingSpeed) {
|
|
this.setMaxFallingSpeed(newBehaviorData.maxFallingSpeed);
|
|
}
|
|
if (
|
|
oldBehaviorData.forwardAcceleration !==
|
|
newBehaviorData.forwardAcceleration
|
|
) {
|
|
this.setForwardAcceleration(newBehaviorData.forwardAcceleration);
|
|
}
|
|
if (
|
|
oldBehaviorData.forwardDeceleration !==
|
|
newBehaviorData.forwardDeceleration
|
|
) {
|
|
this.setForwardDeceleration(newBehaviorData.forwardDeceleration);
|
|
}
|
|
if (oldBehaviorData.forwardSpeedMax !== newBehaviorData.forwardSpeedMax) {
|
|
this.setForwardSpeedMax(newBehaviorData.forwardSpeedMax);
|
|
}
|
|
if (
|
|
oldBehaviorData.sidewaysAcceleration !==
|
|
newBehaviorData.sidewaysAcceleration
|
|
) {
|
|
this.setSidewaysAcceleration(newBehaviorData.sidewaysAcceleration);
|
|
}
|
|
if (
|
|
oldBehaviorData.sidewaysDeceleration !==
|
|
newBehaviorData.sidewaysDeceleration
|
|
) {
|
|
this.setSidewaysDeceleration(newBehaviorData.sidewaysDeceleration);
|
|
}
|
|
if (
|
|
oldBehaviorData.sidewaysSpeedMax !== newBehaviorData.sidewaysSpeedMax
|
|
) {
|
|
this.setSidewaysSpeedMax(newBehaviorData.sidewaysSpeedMax);
|
|
}
|
|
if (oldBehaviorData.jumpSustainTime !== newBehaviorData.jumpSustainTime) {
|
|
this.setJumpSustainTime(newBehaviorData.jumpSustainTime);
|
|
}
|
|
if (oldBehaviorData.jumpHeight !== newBehaviorData.jumpHeight) {
|
|
this.setJumpSpeed(this.getJumpSpeedToReach(newBehaviorData.jumpHeight));
|
|
}
|
|
if (
|
|
oldBehaviorData.shouldBindObjectAndForwardAngle !==
|
|
newBehaviorData.shouldBindObjectAndForwardAngle
|
|
) {
|
|
this.setShouldBindObjectAndForwardAngle(
|
|
newBehaviorData.shouldBindObjectAndForwardAngle
|
|
);
|
|
}
|
|
if (oldBehaviorData.stairHeightMax !== newBehaviorData.stairHeightMax) {
|
|
this.setStairHeightMax(newBehaviorData.stairHeightMax);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
override getNetworkSyncData(
|
|
options: GetNetworkSyncDataOptions
|
|
): PhysicsCharacter3DNetworkSyncData {
|
|
// This method is called, so we are synchronizing this object.
|
|
// Let's clear the inputs between frames as we control it.
|
|
this._clearInputsBetweenFrames = true;
|
|
|
|
return {
|
|
...super.getNetworkSyncData(options),
|
|
props: {
|
|
sma: this._slopeMaxAngle,
|
|
shm: this._stairHeightMax,
|
|
grav: this._gravity,
|
|
mfs: this._maxFallingSpeed,
|
|
facc: this._forwardAcceleration,
|
|
fdec: this._forwardDeceleration,
|
|
fsm: this._forwardSpeedMax,
|
|
sacc: this._sidewaysAcceleration,
|
|
sdec: this._sidewaysDeceleration,
|
|
ssm: this._sidewaysSpeedMax,
|
|
jumpspeed: this._jumpSpeed,
|
|
jumpsustime: this._jumpSustainTime,
|
|
fwa: this._forwardAngle,
|
|
sbpa: this._shouldBindObjectAndForwardAngle,
|
|
fws: this._currentForwardSpeed,
|
|
sws: this._currentSidewaysSpeed,
|
|
cfs: this._currentFallSpeed,
|
|
cjs: this._currentJumpSpeed,
|
|
cj: this._canJump,
|
|
lek: this._wasLeftKeyPressed,
|
|
rik: this._wasRightKeyPressed,
|
|
upk: this._wasForwardKeyPressed,
|
|
dok: this._wasBackwardKeyPressed,
|
|
juk: this._wasJumpKeyPressed,
|
|
us: this._wasStickUsed,
|
|
sa: this._stickAngle,
|
|
sf: this._stickForce,
|
|
tscjs: this._timeSinceCurrentJumpStart,
|
|
jkhsjs: this._jumpKeyHeldSinceJumpStart,
|
|
},
|
|
};
|
|
}
|
|
|
|
override updateFromNetworkSyncData(
|
|
networkSyncData: PhysicsCharacter3DNetworkSyncData,
|
|
options: UpdateFromNetworkSyncDataOptions
|
|
) {
|
|
super.updateFromNetworkSyncData(networkSyncData, options);
|
|
|
|
const behaviorSpecificProps = networkSyncData.props;
|
|
this._slopeMaxAngle = behaviorSpecificProps.sma;
|
|
this._stairHeightMax = behaviorSpecificProps.shm;
|
|
this._gravity = behaviorSpecificProps.grav;
|
|
this._maxFallingSpeed = behaviorSpecificProps.mfs;
|
|
this._forwardAcceleration = behaviorSpecificProps.facc;
|
|
this._forwardDeceleration = behaviorSpecificProps.fdec;
|
|
this._forwardSpeedMax = behaviorSpecificProps.fsm;
|
|
this._sidewaysAcceleration = behaviorSpecificProps.sacc;
|
|
this._sidewaysDeceleration = behaviorSpecificProps.sdec;
|
|
this._sidewaysSpeedMax = behaviorSpecificProps.ssm;
|
|
this._jumpSpeed = behaviorSpecificProps.jumpspeed;
|
|
this._jumpSustainTime = behaviorSpecificProps.jumpsustime;
|
|
this._forwardAngle = behaviorSpecificProps.fwa;
|
|
this._shouldBindObjectAndForwardAngle = behaviorSpecificProps.sbpa;
|
|
this._currentForwardSpeed = behaviorSpecificProps.fws;
|
|
this._currentSidewaysSpeed = behaviorSpecificProps.sws;
|
|
this._currentFallSpeed = behaviorSpecificProps.cfs;
|
|
this._currentJumpSpeed = behaviorSpecificProps.cjs;
|
|
this._canJump = behaviorSpecificProps.cj;
|
|
this._hasPressedForwardKey = behaviorSpecificProps.upk;
|
|
this._hasPressedBackwardKey = behaviorSpecificProps.dok;
|
|
this._hasPressedLeftKey = behaviorSpecificProps.lek;
|
|
this._hasPressedRightKey = behaviorSpecificProps.rik;
|
|
this._hasPressedJumpKey = behaviorSpecificProps.juk;
|
|
this._hasUsedStick = behaviorSpecificProps.us;
|
|
this._stickAngle = behaviorSpecificProps.sa;
|
|
this._stickForce = behaviorSpecificProps.sf;
|
|
this._timeSinceCurrentJumpStart = behaviorSpecificProps.tscjs;
|
|
this._jumpKeyHeldSinceJumpStart = behaviorSpecificProps.jkhsjs;
|
|
|
|
// Clear user inputs between frames only if requested.
|
|
this._clearInputsBetweenFrames = !!options.clearInputs;
|
|
}
|
|
|
|
_getPhysicsPosition(result: Jolt.RVec3): Jolt.RVec3 {
|
|
const physics3D = this.getPhysics3D();
|
|
if (!physics3D) {
|
|
result.Set(0, 0, 0);
|
|
return result;
|
|
}
|
|
const { behavior } = physics3D;
|
|
// The character origin is at its feet:
|
|
// - the center is used for X and Y because Box3D origin is at the top-left corner
|
|
// - the origin is used for Z because, when the character is made smaller,
|
|
// it must stay on the ground and not fell from its old size.
|
|
result.Set(
|
|
this.owner3D.getCenterXInScene() * this._sharedData.worldInvScale,
|
|
this.owner3D.getCenterYInScene() * this._sharedData.worldInvScale,
|
|
this.owner3D.getZ() * this._sharedData.worldInvScale +
|
|
behavior._shapeHalfDepth
|
|
);
|
|
return result;
|
|
}
|
|
|
|
_getPhysicsRotation(result: Jolt.Quat): Jolt.Quat {
|
|
// Characters body should not rotate around X and Y.
|
|
const rotation = result.sEulerAngles(
|
|
this.getVec3(0, 0, gdjs.toRad(this.owner3D.getAngle()))
|
|
);
|
|
result.Set(
|
|
rotation.GetX(),
|
|
rotation.GetY(),
|
|
rotation.GetZ(),
|
|
rotation.GetW()
|
|
);
|
|
Jolt.destroy(rotation);
|
|
return result;
|
|
}
|
|
|
|
_moveObjectToPhysicsPosition(physicsPosition: Jolt.RVec3): void {
|
|
const physics3D = this.getPhysics3D();
|
|
if (!physics3D) {
|
|
return;
|
|
}
|
|
const { behavior } = physics3D;
|
|
this.owner3D.setCenterXInScene(
|
|
physicsPosition.GetX() * this._sharedData.worldScale
|
|
);
|
|
this.owner3D.setCenterYInScene(
|
|
physicsPosition.GetY() * this._sharedData.worldScale
|
|
);
|
|
this.owner3D.setZ(
|
|
(physicsPosition.GetZ() - behavior._shapeHalfDepth) *
|
|
this._sharedData.worldScale
|
|
);
|
|
}
|
|
|
|
_moveObjectToPhysicsRotation(physicsRotation: Jolt.Quat): void {
|
|
const threeObject = this.owner3D.get3DRendererObject();
|
|
threeObject.quaternion.x = physicsRotation.GetX();
|
|
threeObject.quaternion.y = physicsRotation.GetY();
|
|
threeObject.quaternion.z = physicsRotation.GetZ();
|
|
threeObject.quaternion.w = physicsRotation.GetW();
|
|
// TODO Avoid this instantiation
|
|
const euler = new THREE.Euler(0, 0, 0, 'ZYX');
|
|
euler.setFromQuaternion(threeObject.quaternion);
|
|
// No need to update the rotation for X and Y as CharacterVirtual doesn't change it.
|
|
this.owner3D.setAngle(gdjs.toDegrees(euler.z));
|
|
}
|
|
|
|
override onDeActivate() {
|
|
if (!this._physics3D) {
|
|
return;
|
|
}
|
|
this._destroyBody();
|
|
}
|
|
|
|
override onActivate() {
|
|
const behavior = this.owner.getBehavior(
|
|
this._physics3DBehaviorName
|
|
) as gdjs.Physics3DRuntimeBehavior;
|
|
if (!behavior) {
|
|
return;
|
|
}
|
|
behavior._destroyBody();
|
|
}
|
|
|
|
override onDestroy() {
|
|
this._destroyedDuringFrameLogic = true;
|
|
this.onDeActivate();
|
|
}
|
|
|
|
/**
|
|
* Remove the character and its body from the physics engine.
|
|
* This method is called when:
|
|
* - The Physics3D behavior is deactivated
|
|
* - This behavior is deactivated
|
|
* - The object is destroyed
|
|
*/
|
|
_destroyBody() {
|
|
if (this.character) {
|
|
if (this._canBePushed) {
|
|
this.charactersManager.removeCharacter(this.character);
|
|
Jolt.destroy(this.character.GetListener());
|
|
}
|
|
this.collisionChecker.clearContacts();
|
|
// The body is destroyed with the character.
|
|
Jolt.destroy(this.character);
|
|
this.character = null;
|
|
if (this._physics3D) {
|
|
const { behavior } = this._physics3D;
|
|
behavior.resetToDefaultBodyUpdater();
|
|
behavior.resetToDefaultCollisionChecker();
|
|
this._physics3D.behavior._body = null;
|
|
const {
|
|
extendedUpdateSettings,
|
|
broadPhaseLayerFilter,
|
|
objectLayerFilter,
|
|
bodyFilter,
|
|
shapeFilter,
|
|
} = this._physics3D;
|
|
Jolt.destroy(extendedUpdateSettings);
|
|
Jolt.destroy(broadPhaseLayerFilter);
|
|
Jolt.destroy(objectLayerFilter);
|
|
Jolt.destroy(bodyFilter);
|
|
Jolt.destroy(shapeFilter);
|
|
this._physics3D = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
override doStepPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
|
// Trigger createAndAddBody()
|
|
this.getPhysics3D();
|
|
}
|
|
|
|
override doStepPostEvents(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer
|
|
) {
|
|
// Trigger createAndAddBody()
|
|
this.getPhysics3D();
|
|
}
|
|
|
|
doBeforePhysicsStep(timeDelta: float): void {
|
|
if (!this.activated()) {
|
|
return;
|
|
}
|
|
const physics3D = this.getPhysics3D();
|
|
if (!physics3D) {
|
|
return;
|
|
}
|
|
const {
|
|
behavior,
|
|
extendedUpdateSettings,
|
|
broadPhaseLayerFilter,
|
|
objectLayerFilter,
|
|
bodyFilter,
|
|
shapeFilter,
|
|
} = physics3D;
|
|
if (!this.character) {
|
|
return;
|
|
}
|
|
const characterBody = behavior._body;
|
|
if (!characterBody) {
|
|
return;
|
|
}
|
|
const worldInvScale = this._sharedData.worldInvScale;
|
|
|
|
const oldX = this.character.GetPosition().GetX();
|
|
const oldY = this.character.GetPosition().GetY();
|
|
const oldZ = this.character.GetPosition().GetZ();
|
|
|
|
if (this._shouldBindObjectAndForwardAngle) {
|
|
this._forwardAngle = this.owner.getAngle();
|
|
}
|
|
|
|
this.updateCharacterSpeedFromInputs(timeDelta);
|
|
|
|
if (this._currentJumpSpeed > 0) {
|
|
this._timeSinceCurrentJumpStart += timeDelta;
|
|
}
|
|
// Check if the jump key is continuously held since
|
|
// the beginning of the jump.
|
|
if (!this._hasPressedJumpKey) {
|
|
this._jumpKeyHeldSinceJumpStart = false;
|
|
}
|
|
if (
|
|
this._canJump &&
|
|
this._hasPressedJumpKey &&
|
|
// Avoid the character to jump in loop when the jump key is held.
|
|
!this._jumpKeyHeldSinceJumpStart
|
|
) {
|
|
this._currentJumpSpeed = this._jumpSpeed;
|
|
this._currentFallSpeed = 0;
|
|
this._canJump = false;
|
|
this._jumpKeyHeldSinceJumpStart = true;
|
|
this._timeSinceCurrentJumpStart = 0;
|
|
}
|
|
if (!this.isOnFloor()) {
|
|
// Decrease jump speed after the (optional) jump sustain time is over.
|
|
const sustainJumpSpeed =
|
|
this._jumpKeyHeldSinceJumpStart &&
|
|
this._timeSinceCurrentJumpStart < this._jumpSustainTime;
|
|
if (!sustainJumpSpeed) {
|
|
this._currentJumpSpeed = Math.max(
|
|
0,
|
|
this._currentJumpSpeed - this._gravity * timeDelta
|
|
);
|
|
}
|
|
this._currentFallSpeed = Math.min(
|
|
this._maxFallingSpeed,
|
|
this._currentFallSpeed + this._gravity * timeDelta
|
|
);
|
|
}
|
|
|
|
// Follow moving platforms.
|
|
let groundVelocityX = 0;
|
|
let groundVelocityY = 0;
|
|
let groundVelocityZ = 0;
|
|
if (this.character.IsSupported()) {
|
|
const shouldFollow = this.updateGroundVelocity(behavior, timeDelta);
|
|
if (shouldFollow) {
|
|
const groundVelocity = this.character.GetGroundVelocity();
|
|
groundVelocityX = groundVelocity.GetX();
|
|
groundVelocityY = groundVelocity.GetY();
|
|
groundVelocityZ = groundVelocity.GetZ();
|
|
}
|
|
}
|
|
|
|
// Update walking speed
|
|
let forwardSpeed = this._currentForwardSpeed;
|
|
let sidewaysSpeed = this._currentSidewaysSpeed;
|
|
if (sidewaysSpeed !== 0 && forwardSpeed !== 0) {
|
|
// It avoids the speed vector to go outside of an ellipse.
|
|
const speedNormalizationInverseRatio = Math.hypot(
|
|
forwardSpeed / this._forwardSpeedMax,
|
|
sidewaysSpeed / this._sidewaysSpeedMax
|
|
);
|
|
if (speedNormalizationInverseRatio > 1) {
|
|
forwardSpeed /= speedNormalizationInverseRatio;
|
|
sidewaysSpeed /= speedNormalizationInverseRatio;
|
|
}
|
|
}
|
|
forwardSpeed *= worldInvScale;
|
|
sidewaysSpeed *= worldInvScale;
|
|
const angle = gdjs.toRad(this._forwardAngle);
|
|
const cosA = Math.cos(angle);
|
|
const sinA = Math.sin(angle);
|
|
const speedX = forwardSpeed * cosA - sidewaysSpeed * sinA;
|
|
const speedY = forwardSpeed * sinA + sidewaysSpeed * cosA;
|
|
this.character.SetLinearVelocity(
|
|
this.getVec3(
|
|
groundVelocityX + speedX,
|
|
groundVelocityY + speedY,
|
|
// The ground velocity is not added on Z as it's handled by mStickToFloorStepDown.
|
|
(this._currentJumpSpeed - this._currentFallSpeed) * worldInvScale
|
|
)
|
|
);
|
|
|
|
if (this.isOnFloor()) {
|
|
// Keep the character on the floor when walking down-hill.
|
|
const walkingDistance = Math.max(
|
|
Math.hypot(
|
|
this.character.GetPosition().GetX() - this._oldPhysicsPosition[0],
|
|
this.character.GetPosition().GetY() - this._oldPhysicsPosition[1]
|
|
),
|
|
Math.hypot(
|
|
this.character.GetLinearVelocity().GetX(),
|
|
this.character.GetLinearVelocity().GetY()
|
|
) * timeDelta
|
|
);
|
|
this._oldPhysicsPosition[0] = this.character.GetPosition().GetX();
|
|
this._oldPhysicsPosition[1] = this.character.GetPosition().GetY();
|
|
|
|
// A safety margin is taken as if the ground normal is too steep, the
|
|
// character will fall next step anyway.
|
|
const stickToFloorStepDownZ = Math.max(
|
|
-0.01 +
|
|
1.01 *
|
|
Math.min(
|
|
// Follow the platform slope...
|
|
-walkingDistance * this._slopeClimbingFactor,
|
|
// ...and follow a platform falling...
|
|
groundVelocityZ * timeDelta
|
|
),
|
|
// ...but never fall faster than the max fall speed.
|
|
-this._maxFallingSpeed * worldInvScale * timeDelta
|
|
);
|
|
extendedUpdateSettings.mStickToFloorStepDown.Set(
|
|
0,
|
|
0,
|
|
stickToFloorStepDownZ
|
|
);
|
|
} else {
|
|
extendedUpdateSettings.mStickToFloorStepDown.Set(0, 0, 0);
|
|
}
|
|
|
|
this.character.ExtendedUpdate(
|
|
timeDelta,
|
|
this.character.GetUp(),
|
|
extendedUpdateSettings,
|
|
broadPhaseLayerFilter,
|
|
objectLayerFilter,
|
|
bodyFilter,
|
|
shapeFilter,
|
|
this._sharedData.jolt.GetTempAllocator()
|
|
);
|
|
this.collisionChecker.updateContacts();
|
|
|
|
if (this.isOnFloor()) {
|
|
this._canJump = true;
|
|
this._currentFallSpeed = 0;
|
|
this._currentJumpSpeed = 0;
|
|
}
|
|
|
|
this._wasForwardKeyPressed = this._hasPressedForwardKey;
|
|
this._wasBackwardKeyPressed = this._hasPressedBackwardKey;
|
|
this._wasRightKeyPressed = this._hasPressedRightKey;
|
|
this._wasLeftKeyPressed = this._hasPressedLeftKey;
|
|
this._wasJumpKeyPressed = this._hasPressedJumpKey;
|
|
this._wasStickUsed = this._hasUsedStick;
|
|
|
|
if (this._clearInputsBetweenFrames) {
|
|
this._hasPressedForwardKey = false;
|
|
this._hasPressedBackwardKey = false;
|
|
this._hasPressedRightKey = false;
|
|
this._hasPressedLeftKey = false;
|
|
this._hasPressedJumpKey = false;
|
|
this._hasUsedStick = false;
|
|
}
|
|
|
|
this._hasReallyMoved =
|
|
Math.abs(this.character.GetPosition().GetX() - oldX) >
|
|
PhysicsCharacter3DRuntimeBehavior.epsilon ||
|
|
Math.abs(this.character.GetPosition().GetY() - oldY) >
|
|
PhysicsCharacter3DRuntimeBehavior.epsilon ||
|
|
Math.abs(this.character.GetPosition().GetZ() - oldZ) >
|
|
PhysicsCharacter3DRuntimeBehavior.epsilon;
|
|
}
|
|
|
|
private updateCharacterSpeedFromInputs(timeDelta: float) {
|
|
/** A stick with a half way force targets a lower speed than the maximum speed. */
|
|
let targetedForwardSpeed = 0;
|
|
// Change the speed according to the player's input.
|
|
// TODO Give priority to the last key for faster reaction time.
|
|
if (this._hasPressedBackwardKey !== this._hasPressedForwardKey) {
|
|
if (this._hasPressedBackwardKey) {
|
|
targetedForwardSpeed = -this._forwardSpeedMax;
|
|
} else if (this._hasPressedForwardKey) {
|
|
targetedForwardSpeed = this._forwardSpeedMax;
|
|
}
|
|
} else if (this._hasUsedStick) {
|
|
targetedForwardSpeed =
|
|
-this._forwardSpeedMax *
|
|
this._stickForce *
|
|
Math.sin(gdjs.toRad(this._stickAngle));
|
|
}
|
|
this._currentForwardSpeed =
|
|
PhysicsCharacter3DRuntimeBehavior.getAcceleratedSpeed(
|
|
this._currentForwardSpeed,
|
|
targetedForwardSpeed,
|
|
this._forwardSpeedMax,
|
|
this._forwardAcceleration,
|
|
this._forwardDeceleration,
|
|
timeDelta
|
|
);
|
|
/** A stick with a half way force targets a lower speed than the maximum speed. */
|
|
let targetedSidewaysSpeed = 0;
|
|
if (this._hasPressedLeftKey !== this._hasPressedRightKey) {
|
|
if (this._hasPressedLeftKey) {
|
|
targetedSidewaysSpeed = -this._sidewaysSpeedMax;
|
|
} else if (this._hasPressedRightKey) {
|
|
targetedSidewaysSpeed = this._sidewaysSpeedMax;
|
|
}
|
|
} else if (this._hasUsedStick) {
|
|
targetedSidewaysSpeed =
|
|
this._sidewaysSpeedMax *
|
|
this._stickForce *
|
|
Math.cos(gdjs.toRad(this._stickAngle));
|
|
}
|
|
this._currentSidewaysSpeed =
|
|
PhysicsCharacter3DRuntimeBehavior.getAcceleratedSpeed(
|
|
this._currentSidewaysSpeed,
|
|
targetedSidewaysSpeed,
|
|
this._sidewaysSpeedMax,
|
|
this._sidewaysAcceleration,
|
|
this._sidewaysDeceleration,
|
|
timeDelta
|
|
);
|
|
}
|
|
|
|
private static getAcceleratedSpeed(
|
|
currentSpeed: float,
|
|
targetedSpeed: float,
|
|
speedMax: float,
|
|
acceleration: float,
|
|
deceleration: float,
|
|
timeDelta: float
|
|
): float {
|
|
let newSpeed = currentSpeed;
|
|
if (targetedSpeed < 0) {
|
|
if (currentSpeed <= targetedSpeed) {
|
|
// Reduce the speed to match the stick force.
|
|
newSpeed = Math.min(
|
|
targetedSpeed,
|
|
currentSpeed + deceleration * timeDelta
|
|
);
|
|
} else if (currentSpeed <= 0) {
|
|
// Accelerate
|
|
newSpeed -= Math.max(-speedMax, acceleration * timeDelta);
|
|
} else {
|
|
// Turn back at least as fast as it would stop.
|
|
newSpeed = Math.max(
|
|
targetedSpeed,
|
|
currentSpeed - Math.max(acceleration, deceleration) * timeDelta
|
|
);
|
|
}
|
|
} else if (targetedSpeed > 0) {
|
|
if (currentSpeed >= targetedSpeed) {
|
|
// Reduce the speed to match the stick force.
|
|
newSpeed = Math.max(
|
|
targetedSpeed,
|
|
currentSpeed - deceleration * timeDelta
|
|
);
|
|
} else if (currentSpeed >= 0) {
|
|
// Accelerate
|
|
newSpeed = Math.min(
|
|
speedMax,
|
|
currentSpeed + acceleration * timeDelta
|
|
);
|
|
} else {
|
|
// Turn back at least as fast as it would stop.
|
|
newSpeed = Math.min(
|
|
targetedSpeed,
|
|
currentSpeed + Math.max(acceleration, deceleration) * timeDelta
|
|
);
|
|
}
|
|
} else {
|
|
// Decelerate and stop.
|
|
if (currentSpeed < 0) {
|
|
newSpeed = Math.min(currentSpeed + deceleration * timeDelta, 0);
|
|
}
|
|
if (currentSpeed > 0) {
|
|
newSpeed = Math.max(currentSpeed - deceleration * timeDelta, 0);
|
|
}
|
|
}
|
|
return newSpeed;
|
|
}
|
|
|
|
private updateGroundVelocity(
|
|
behavior: Physics3DRuntimeBehavior,
|
|
timeDelta: float
|
|
): boolean {
|
|
if (!this.character) {
|
|
return false;
|
|
}
|
|
const characterBody = behavior._body;
|
|
if (!characterBody) {
|
|
return false;
|
|
}
|
|
const worldInvScale = this._sharedData.worldInvScale;
|
|
|
|
if (!this.character.IsSupported()) {
|
|
return false;
|
|
}
|
|
|
|
const groundBody = this._sharedData.physicsSystem
|
|
.GetBodyLockInterface()
|
|
.TryGetBody(this.character.GetGroundBodyID());
|
|
|
|
const stillKinematicPlatform =
|
|
groundBody.IsKinematic() &&
|
|
groundBody.GetLinearVelocity().Equals(Jolt.Vec3.prototype.sZero()) &&
|
|
groundBody.GetAngularVelocity().Equals(Jolt.Vec3.prototype.sZero());
|
|
if (stillKinematicPlatform) {
|
|
const groundBehavior = groundBody.gdjsAssociatedBehavior;
|
|
if (groundBehavior) {
|
|
const inverseTimeDelta = 1 / timeDelta;
|
|
// The platform may be moved by position changes instead of velocity.
|
|
// Emulate a velocity from the position changes.
|
|
groundBody.SetLinearVelocity(
|
|
this.getVec3(
|
|
(groundBehavior.owner3D.getX() - groundBehavior._objectOldX) *
|
|
worldInvScale *
|
|
inverseTimeDelta,
|
|
(groundBehavior.owner3D.getY() - groundBehavior._objectOldY) *
|
|
worldInvScale *
|
|
inverseTimeDelta,
|
|
(groundBehavior.owner3D.getZ() - groundBehavior._objectOldZ) *
|
|
worldInvScale *
|
|
inverseTimeDelta
|
|
)
|
|
);
|
|
groundBody.SetAngularVelocity(
|
|
this.getVec3(
|
|
0,
|
|
0,
|
|
gdjs.toRad(
|
|
gdjs.evtTools.common.angleDifference(
|
|
groundBehavior.owner3D.getAngle(),
|
|
groundBehavior._objectOldRotationZ
|
|
)
|
|
) * inverseTimeDelta
|
|
)
|
|
);
|
|
}
|
|
}
|
|
this.character.UpdateGroundVelocity();
|
|
|
|
const groundAngularVelocityZ = groundBody.GetAngularVelocity().GetZ();
|
|
if (groundAngularVelocityZ !== 0) {
|
|
// Make the character rotate with the platform on Z axis.
|
|
const angleDelta = groundAngularVelocityZ * timeDelta;
|
|
this.character.SetRotation(
|
|
Jolt.Quat.prototype.sEulerAngles(
|
|
this.getVec3(
|
|
0,
|
|
0,
|
|
this.character
|
|
.GetRotation()
|
|
.GetRotationAngle(Jolt.Vec3.prototype.sAxisZ()) + angleDelta
|
|
)
|
|
)
|
|
);
|
|
// Also update the forward angle to make sure it stays the same
|
|
// relatively to the object angle.
|
|
this._forwardAngle += gdjs.toDegrees(angleDelta);
|
|
}
|
|
if (stillKinematicPlatform) {
|
|
groundBody.SetLinearVelocity(Jolt.Vec3.prototype.sZero());
|
|
groundBody.SetAngularVelocity(Jolt.Vec3.prototype.sZero());
|
|
}
|
|
|
|
// Characters should not try to magnet on a body that rolls on the ground.
|
|
const rollingSpeedMax = (1 * Math.PI) / 180;
|
|
const shouldFollow =
|
|
Math.abs(groundBody.GetAngularVelocity().GetX()) < rollingSpeedMax &&
|
|
Math.abs(groundBody.GetAngularVelocity().GetY()) < rollingSpeedMax;
|
|
return shouldFollow;
|
|
}
|
|
|
|
onObjectHotReloaded() {}
|
|
|
|
/**
|
|
* Get maximum angle of a slope for the Character to run on it as a floor.
|
|
* @returns the slope maximum angle, in degrees.
|
|
*/
|
|
getSlopeMaxAngle(): float {
|
|
return this._slopeMaxAngle;
|
|
}
|
|
|
|
/**
|
|
* Set the maximum slope angle of the Character.
|
|
* @param slopeMaxAngle The new maximum slope angle.
|
|
*/
|
|
setSlopeMaxAngle(slopeMaxAngle: float): void {
|
|
if (slopeMaxAngle < 0 || slopeMaxAngle >= 90) {
|
|
return;
|
|
}
|
|
this._slopeMaxAngle = slopeMaxAngle;
|
|
|
|
//Avoid rounding errors
|
|
if (slopeMaxAngle === 45) {
|
|
this._slopeClimbingFactor = 1;
|
|
} else {
|
|
// Avoid a `_slopeClimbingFactor` set to exactly 0.
|
|
// Otherwise, this can lead the floor finding functions to consider
|
|
// a floor to be "too high" to reach, even if the object is very slightly
|
|
// inside it, which can happen because of rounding errors.
|
|
// See "Floating-point error mitigations" tests.
|
|
this._slopeClimbingFactor = Math.max(
|
|
1 / 1024,
|
|
Math.tan(gdjs.toRad(slopeMaxAngle))
|
|
);
|
|
}
|
|
// The floor is in 3D but to go back to 2D trigonometry, we can take the
|
|
// 2D space generated by the floor normal and the Z axis, given that:
|
|
// - The normal keeps the same Z coordinate (as the Z axis is included in the 2D space)
|
|
// - The normal keeps the same length (as the normal is included in the 2D space)
|
|
this._slopeClimbingMinNormalZ = Math.min(
|
|
Math.cos(gdjs.toRad(slopeMaxAngle)),
|
|
1 - 1 / 1024
|
|
);
|
|
}
|
|
|
|
getStairHeightMax(): float {
|
|
return this._stairHeightMax;
|
|
}
|
|
|
|
setStairHeightMax(stairHeightMax: float): void {
|
|
this._stairHeightMax = stairHeightMax;
|
|
|
|
const physics3D = this.getPhysics3D();
|
|
if (!physics3D) {
|
|
return;
|
|
}
|
|
const { extendedUpdateSettings } = physics3D;
|
|
const walkStairsStepUp = stairHeightMax * this._sharedData.worldInvScale;
|
|
extendedUpdateSettings.mWalkStairsStepUp = this.getVec3(
|
|
0,
|
|
0,
|
|
walkStairsStepUp
|
|
);
|
|
// Use default values proportionally;
|
|
// "The factors are arbitrary but works well when tested in a game."
|
|
extendedUpdateSettings.mWalkStairsMinStepForward =
|
|
(0.02 / 0.4) * walkStairsStepUp;
|
|
extendedUpdateSettings.mWalkStairsStepForwardTest =
|
|
(0.15 / 0.4) * walkStairsStepUp;
|
|
}
|
|
|
|
/**
|
|
* Get the gravity of the Character.
|
|
* @returns The current gravity.
|
|
*/
|
|
getGravity(): float {
|
|
return this._gravity;
|
|
}
|
|
|
|
/**
|
|
* Set the gravity of the Character.
|
|
* @param gravity The new gravity.
|
|
*/
|
|
setGravity(gravity: float): void {
|
|
this._gravity = gravity;
|
|
}
|
|
|
|
/**
|
|
* Get the maximum falling speed of the Character.
|
|
* @returns The maximum falling speed.
|
|
*/
|
|
getMaxFallingSpeed(): float {
|
|
return this._maxFallingSpeed;
|
|
}
|
|
|
|
/**
|
|
* Set the maximum falling speed of the Character.
|
|
* @param maxFallingSpeed The maximum falling speed.
|
|
* @param tryToPreserveAirSpeed If true and if jumping, tune the current
|
|
* jump speed to preserve the overall speed in the air.
|
|
*/
|
|
setMaxFallingSpeed(
|
|
maxFallingSpeed: float,
|
|
tryToPreserveAirSpeed: boolean = false
|
|
): void {
|
|
if (tryToPreserveAirSpeed && !this.isOnFloor()) {
|
|
// If the falling speed is too high compared to the new max falling speed,
|
|
// reduce it and adapt the jump speed to preserve the overall vertical speed.
|
|
const fallingSpeedOverflow = this._currentFallSpeed - maxFallingSpeed;
|
|
if (fallingSpeedOverflow > 0) {
|
|
this._currentFallSpeed -= fallingSpeedOverflow;
|
|
this._currentJumpSpeed = Math.max(
|
|
0,
|
|
this.getCurrentJumpSpeed() - fallingSpeedOverflow
|
|
);
|
|
}
|
|
}
|
|
this._maxFallingSpeed = maxFallingSpeed;
|
|
}
|
|
|
|
/**
|
|
* Get the forward acceleration value of the Character.
|
|
* @returns The current acceleration.
|
|
*/
|
|
getForwardAcceleration(): float {
|
|
return this._forwardAcceleration;
|
|
}
|
|
|
|
/**
|
|
* Set the forward acceleration of the Character.
|
|
* @param forwardAcceleration The new acceleration.
|
|
*/
|
|
setForwardAcceleration(forwardAcceleration: float): void {
|
|
this._forwardAcceleration = forwardAcceleration;
|
|
}
|
|
|
|
/**
|
|
* Get the forward deceleration of the Character.
|
|
* @returns The current deceleration.
|
|
*/
|
|
getForwardDeceleration(): float {
|
|
return this._forwardDeceleration;
|
|
}
|
|
|
|
/**
|
|
* Set the forward deceleration of the Character.
|
|
* @param forwardDeceleration The new deceleration.
|
|
*/
|
|
setForwardDeceleration(forwardDeceleration: float): void {
|
|
this._forwardDeceleration = forwardDeceleration;
|
|
}
|
|
|
|
/**
|
|
* Get the forward maximum speed of the Character.
|
|
* @returns The maximum speed.
|
|
*/
|
|
getForwardSpeedMax(): float {
|
|
return this._forwardSpeedMax;
|
|
}
|
|
|
|
/**
|
|
* Set the forward maximum speed of the Character.
|
|
* @param forwardSpeedMax The new maximum speed.
|
|
*/
|
|
setForwardSpeedMax(forwardSpeedMax: float): void {
|
|
this._forwardSpeedMax = forwardSpeedMax;
|
|
}
|
|
|
|
/**
|
|
* Get the sideways acceleration value of the Character.
|
|
* @returns The current acceleration.
|
|
*/
|
|
getSidewaysAcceleration(): float {
|
|
return this._sidewaysAcceleration;
|
|
}
|
|
|
|
/**
|
|
* Set the sideways acceleration of the Character.
|
|
* @param sidewaysAcceleration The new acceleration.
|
|
*/
|
|
setSidewaysAcceleration(sidewaysAcceleration: float): void {
|
|
this._sidewaysAcceleration = sidewaysAcceleration;
|
|
}
|
|
|
|
/**
|
|
* Get the sideways deceleration of the Character.
|
|
* @returns The current deceleration.
|
|
*/
|
|
getSidewaysDeceleration(): float {
|
|
return this._sidewaysDeceleration;
|
|
}
|
|
|
|
/**
|
|
* Set the sideways deceleration of the Character.
|
|
* @param sidewaysDeceleration The new deceleration.
|
|
*/
|
|
setSidewaysDeceleration(sidewaysDeceleration: float): void {
|
|
this._sidewaysDeceleration = sidewaysDeceleration;
|
|
}
|
|
|
|
/**
|
|
* Get the sideways maximum speed of the Character.
|
|
* @returns The maximum speed.
|
|
*/
|
|
getSidewaysSpeedMax(): float {
|
|
return this._sidewaysSpeedMax;
|
|
}
|
|
|
|
/**
|
|
* Set the sideways maximum speed of the Character.
|
|
* @param sidewaysSpeedMax The new maximum speed.
|
|
*/
|
|
setSidewaysSpeedMax(sidewaysSpeedMax: float): void {
|
|
this._sidewaysSpeedMax = sidewaysSpeedMax;
|
|
}
|
|
|
|
/**
|
|
* Get the jump speed of the Character.
|
|
* @returns The jump speed.
|
|
*/
|
|
getJumpSpeed(): float {
|
|
return this._jumpSpeed;
|
|
}
|
|
|
|
/**
|
|
* Set the jump speed of the Character.
|
|
* @param jumpSpeed The new jump speed.
|
|
*/
|
|
setJumpSpeed(jumpSpeed: float): void {
|
|
this._jumpSpeed = jumpSpeed;
|
|
}
|
|
|
|
/**
|
|
* Get the jump sustain time of the Character.
|
|
* @returns The jump sustain time.
|
|
*/
|
|
getJumpSustainTime(): float {
|
|
return this._jumpSustainTime;
|
|
}
|
|
|
|
/**
|
|
* Set the jump sustain time of the Character.
|
|
* @param jumpSustainTime The new jump sustain time.
|
|
*/
|
|
setJumpSustainTime(jumpSustainTime: float): void {
|
|
this._jumpSustainTime = jumpSustainTime;
|
|
}
|
|
|
|
getForwardAngle(): float {
|
|
return this._forwardAngle;
|
|
}
|
|
|
|
setForwardAngle(angle: float): void {
|
|
this._forwardAngle = angle;
|
|
if (this._shouldBindObjectAndForwardAngle) {
|
|
this.owner.setAngle(angle);
|
|
}
|
|
}
|
|
|
|
isForwardAngleAround(degreeAngle: float, tolerance: float) {
|
|
return (
|
|
Math.abs(
|
|
gdjs.evtTools.common.angleDifference(this._forwardAngle, degreeAngle)
|
|
) <= tolerance
|
|
);
|
|
}
|
|
|
|
shouldBindObjectAndForwardAngle(): boolean {
|
|
return this._shouldBindObjectAndForwardAngle;
|
|
}
|
|
|
|
setShouldBindObjectAndForwardAngle(
|
|
shouldBindObjectAndForwardAngle: boolean
|
|
): void {
|
|
this._shouldBindObjectAndForwardAngle = shouldBindObjectAndForwardAngle;
|
|
}
|
|
|
|
/**
|
|
* Get the current speed of the Character.
|
|
* @returns The current speed.
|
|
*/
|
|
getCurrentForwardSpeed(): float {
|
|
return this._currentForwardSpeed;
|
|
}
|
|
|
|
/**
|
|
* Set the current speed of the Character.
|
|
* @param currentForwardSpeed The current speed.
|
|
*/
|
|
setCurrentForwardSpeed(currentForwardSpeed: float): void {
|
|
this._currentForwardSpeed = currentForwardSpeed;
|
|
}
|
|
|
|
/**
|
|
* Get the current speed of the Character.
|
|
* @returns The current speed.
|
|
*/
|
|
getCurrentSidewaysSpeed(): float {
|
|
return this._currentSidewaysSpeed;
|
|
}
|
|
|
|
/**
|
|
* Set the current speed of the Character.
|
|
* @param currentSidewaysSpeed The current speed.
|
|
*/
|
|
setCurrentSidewaysSpeed(currentSidewaysSpeed: float): void {
|
|
this._currentSidewaysSpeed = currentSidewaysSpeed;
|
|
}
|
|
|
|
/**
|
|
* Get the speed at which the object is falling. It is 0 when the object is
|
|
* on a floor, and non 0 as soon as the object leaves the floor.
|
|
* @returns The current fall speed.
|
|
*/
|
|
getCurrentFallSpeed(): float {
|
|
return this._currentFallSpeed;
|
|
}
|
|
|
|
/**
|
|
* Set the current fall speed.
|
|
*/
|
|
setCurrentFallSpeed(currentFallSpeed: float) {
|
|
this._currentFallSpeed = gdjs.evtTools.common.clamp(
|
|
currentFallSpeed,
|
|
0,
|
|
this._maxFallingSpeed
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the current jump speed of the Character.
|
|
* @returns The current jump speed.
|
|
*/
|
|
getCurrentJumpSpeed(): float {
|
|
return this._currentJumpSpeed;
|
|
}
|
|
|
|
/**
|
|
* Set the current jump speed.
|
|
*/
|
|
setCurrentJumpSpeed(currentJumpSpeed: float) {
|
|
this._currentJumpSpeed = Math.max(0, currentJumpSpeed);
|
|
}
|
|
|
|
/**
|
|
* Check if the Character can jump.
|
|
* @returns Returns true if the object can jump.
|
|
*/
|
|
canJump(): boolean {
|
|
return this._canJump;
|
|
}
|
|
|
|
/**
|
|
* Allow the Character to jump again.
|
|
*/
|
|
setCanJump(): void {
|
|
this._canJump = true;
|
|
}
|
|
|
|
/**
|
|
* Forbid the Character to air jump.
|
|
*/
|
|
setCanNotAirJump(): void {
|
|
if (this.isJumping() || this.isFalling()) {
|
|
this._canJump = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abort the current jump.
|
|
*
|
|
* When the character is not in the jumping state this method has no effect.
|
|
*/
|
|
abortJump(): void {
|
|
if (this.isJumping()) {
|
|
this._currentFallSpeed = 0;
|
|
this._currentJumpSpeed = 0;
|
|
}
|
|
}
|
|
|
|
simulateForwardKey(): void {
|
|
this._hasPressedForwardKey = true;
|
|
}
|
|
|
|
wasForwardKeyPressed(): boolean {
|
|
return this._wasForwardKeyPressed;
|
|
}
|
|
|
|
simulateBackwardKey(): void {
|
|
this._hasPressedBackwardKey = true;
|
|
}
|
|
|
|
wasBackwardKeyPressed(): boolean {
|
|
return this._wasBackwardKeyPressed;
|
|
}
|
|
|
|
simulateRightKey(): void {
|
|
this._hasPressedRightKey = true;
|
|
}
|
|
|
|
wasRightKeyPressed(): boolean {
|
|
return this._wasRightKeyPressed;
|
|
}
|
|
|
|
simulateLeftKey(): void {
|
|
this._hasPressedLeftKey = true;
|
|
}
|
|
|
|
wasLeftKeyPressed(): boolean {
|
|
return this._wasLeftKeyPressed;
|
|
}
|
|
|
|
simulateJumpKey(): void {
|
|
this._hasPressedJumpKey = true;
|
|
}
|
|
|
|
wasJumpKeyPressed(): boolean {
|
|
return this._wasJumpKeyPressed;
|
|
}
|
|
|
|
simulateStick(stickAngle: float, stickForce: float) {
|
|
this._hasUsedStick = true;
|
|
this._stickAngle = stickAngle;
|
|
this._stickForce = Math.max(0, Math.min(1, stickForce));
|
|
}
|
|
|
|
wasStickUsed(): boolean {
|
|
return this._wasStickUsed;
|
|
}
|
|
|
|
getStickAngle(): float {
|
|
return this._wasStickUsed ? this._stickAngle : 0;
|
|
}
|
|
|
|
getStickForce(): float {
|
|
return this._wasStickUsed ? this._stickForce : 0;
|
|
}
|
|
|
|
// TODO Should we add a "is sliding" condition?
|
|
/**
|
|
* Check if the Character is on a floor.
|
|
* @returns Returns true if on a floor and false if not.
|
|
*/
|
|
isOnFloor(): boolean {
|
|
return this.character
|
|
? this.character.IsSupported() &&
|
|
// Ensure characters don't land on too step floor.
|
|
this.character.GetGroundNormal().GetZ() >=
|
|
this._slopeClimbingMinNormalZ &&
|
|
// Ensure characters don't land on a platform corner while jumping.
|
|
this._currentJumpSpeed <= this._currentFallSpeed
|
|
: false;
|
|
}
|
|
|
|
/**
|
|
* Check if the Character is on the given object.
|
|
* @returns Returns true if on the object and false if not.
|
|
*/
|
|
isOnFloorObject(physics3DBehavior: gdjs.Physics3DRuntimeBehavior): boolean {
|
|
if (!physics3DBehavior._body || !this.character || !this.isOnFloor()) {
|
|
return false;
|
|
}
|
|
return (
|
|
this.character.GetGroundBodyID().GetIndexAndSequenceNumber() ===
|
|
physics3DBehavior._body.GetID().GetIndexAndSequenceNumber()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the Character is jumping.
|
|
* @returns Returns true if jumping and false if not.
|
|
*/
|
|
isJumping(): boolean {
|
|
return this._currentJumpSpeed > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if the Character is in the falling state. This is false
|
|
* if the object is jumping, even if the object is going down after reaching
|
|
* the jump peak.
|
|
* @returns Returns true if it is falling and false if not.
|
|
*/
|
|
isFallingWithoutJumping(): boolean {
|
|
return !this.isOnFloor() && !this.isJumping();
|
|
}
|
|
|
|
/**
|
|
* Check if the Character is "going down", either because it's in the
|
|
* falling state *or* because it's jumping but reached the jump peak and
|
|
* is now going down (because the jump speed can't compensate anymore the
|
|
* falling speed).
|
|
*
|
|
* If you want to check if the object is falling outside of a jump (or because
|
|
* the jump is entirely finished and there is no jump speed applied to the object
|
|
* anymore), consider using `isFallingWithoutJumping`.
|
|
*
|
|
* @returns Returns true if it is "going down" and false if not.
|
|
*/
|
|
isFalling(): boolean {
|
|
return (
|
|
this.isFallingWithoutJumping() ||
|
|
(this.isJumping() && this._currentFallSpeed > this._currentJumpSpeed)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the Character is moving.
|
|
* @returns Returns true if it is moving and false if not.
|
|
*/
|
|
isMovingEvenALittle(): boolean {
|
|
return (
|
|
(this._hasReallyMoved &&
|
|
(this._currentForwardSpeed !== 0 ||
|
|
this._currentSidewaysSpeed !== 0)) ||
|
|
this._currentJumpSpeed !== 0 ||
|
|
this._currentFallSpeed !== 0
|
|
);
|
|
}
|
|
|
|
getJumpSpeedToReach(jumpHeight: float): float {
|
|
// Formulas used in this extension were generated from a math model.
|
|
// They are probably not understandable on their own.
|
|
// If you need to modify them or need to write new feature,
|
|
// please take a look at the platformer extension documentation:
|
|
// https://github.com/4ian/GDevelop/tree/master/Extensions/PlatformBehavior#readme
|
|
|
|
jumpHeight = -Math.abs(jumpHeight);
|
|
|
|
const gravity = this._gravity;
|
|
const maxFallingSpeed = this._maxFallingSpeed;
|
|
const jumpSustainTime = this._jumpSustainTime;
|
|
|
|
const maxFallingSpeedReachedTime = maxFallingSpeed / gravity;
|
|
|
|
// The implementation jumps from one quadratic resolution to another
|
|
// to find the right formula to use as the time is unknown.
|
|
|
|
const sustainCase = (jumpHeight) => Math.sqrt(-jumpHeight * gravity * 2);
|
|
const maxFallingCase = (jumpHeight) =>
|
|
-gravity * jumpSustainTime +
|
|
maxFallingSpeed +
|
|
Math.sqrt(
|
|
gravity * gravity * jumpSustainTime * jumpSustainTime -
|
|
2 * jumpHeight * gravity -
|
|
maxFallingSpeed * maxFallingSpeed
|
|
);
|
|
|
|
let jumpSpeed = 0;
|
|
let peakTime = 0;
|
|
if (maxFallingSpeedReachedTime > jumpSustainTime) {
|
|
// common case
|
|
jumpSpeed =
|
|
-gravity * jumpSustainTime +
|
|
Math.sqrt(
|
|
2 * gravity * gravity * jumpSustainTime * jumpSustainTime -
|
|
4 * jumpHeight * gravity
|
|
);
|
|
peakTime = (gravity * jumpSustainTime + jumpSpeed) / (2 * gravity);
|
|
if (peakTime < jumpSustainTime) {
|
|
jumpSpeed = sustainCase(jumpHeight);
|
|
} else if (peakTime > maxFallingSpeedReachedTime) {
|
|
jumpSpeed = maxFallingCase(jumpHeight);
|
|
}
|
|
} else {
|
|
// affine case can't have a maximum
|
|
|
|
// sustain case
|
|
jumpSpeed = sustainCase(jumpHeight);
|
|
peakTime = jumpSpeed / gravity;
|
|
if (peakTime > maxFallingSpeedReachedTime) {
|
|
jumpSpeed = maxFallingCase(jumpHeight);
|
|
}
|
|
}
|
|
return jumpSpeed;
|
|
}
|
|
}
|
|
|
|
gdjs.registerBehavior(
|
|
'Physics3D::PhysicsCharacter3D',
|
|
gdjs.PhysicsCharacter3DRuntimeBehavior
|
|
);
|
|
|
|
export namespace PhysicsCharacter3DRuntimeBehavior {
|
|
/**
|
|
* Handle collisions between characters that can push each other.
|
|
*/
|
|
export class CharactersManager {
|
|
/** Handle collisions between characters that can push each other. */
|
|
private characterVsCharacterCollision: Jolt.CharacterVsCharacterCollisionSimple;
|
|
|
|
constructor(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
|
this.characterVsCharacterCollision =
|
|
new Jolt.CharacterVsCharacterCollisionSimple();
|
|
}
|
|
|
|
/**
|
|
* Get the characters manager of an instance container.
|
|
*/
|
|
static getManager(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
|
// @ts-ignore
|
|
if (!instanceContainer.charactersManager) {
|
|
//Create the shared manager if necessary.
|
|
// @ts-ignore
|
|
instanceContainer.charactersManager =
|
|
new gdjs.PhysicsCharacter3DRuntimeBehavior.CharactersManager(
|
|
instanceContainer
|
|
);
|
|
}
|
|
// @ts-ignore
|
|
return instanceContainer.charactersManager;
|
|
}
|
|
|
|
/**
|
|
* Add a character to the list of characters that can push each other.
|
|
*/
|
|
addCharacter(character: Jolt.CharacterVirtual) {
|
|
this.characterVsCharacterCollision.Add(character);
|
|
character.SetCharacterVsCharacterCollision(
|
|
this.characterVsCharacterCollision
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Remove a character from the list of characters that can push each other.
|
|
*/
|
|
removeCharacter(character: Jolt.CharacterVirtual) {
|
|
this.characterVsCharacterCollision.Remove(character);
|
|
}
|
|
|
|
destroy() {
|
|
Jolt.destroy(this.characterVsCharacterCollision);
|
|
}
|
|
}
|
|
gdjs.registerRuntimeSceneUnloadedCallback(function (runtimeScene) {
|
|
gdjs.PhysicsCharacter3DRuntimeBehavior.CharactersManager.getManager(
|
|
runtimeScene
|
|
).destroy();
|
|
});
|
|
|
|
export class CharacterBodyUpdater
|
|
implements gdjs.Physics3DRuntimeBehavior.BodyUpdater
|
|
{
|
|
characterBehavior: gdjs.PhysicsCharacter3DRuntimeBehavior;
|
|
|
|
constructor(characterBehavior: gdjs.PhysicsCharacter3DRuntimeBehavior) {
|
|
this.characterBehavior = characterBehavior;
|
|
}
|
|
|
|
createAndAddBody(): Jolt.Body | null {
|
|
const physics3D = this.characterBehavior.getPhysics3D();
|
|
if (!physics3D) {
|
|
return null;
|
|
}
|
|
const { behavior } = physics3D;
|
|
const { _slopeMaxAngle, owner3D, _sharedData } = this.characterBehavior;
|
|
|
|
// Jolt doesn't support center of mass offset for characters.
|
|
const shape = behavior.createShapeWithoutMassCenterOffset();
|
|
|
|
const settings = new Jolt.CharacterVirtualSettings();
|
|
// Characters innerBody are Kinematic body, they don't allow other
|
|
// characters to push them.
|
|
// The layer 0 doesn't allow any collision as masking them always result to 0.
|
|
// This allows CharacterVsCharacterCollisionSimple to handle the collisions.
|
|
settings.mInnerBodyLayer = this.characterBehavior._canBePushed
|
|
? 0
|
|
: behavior.getBodyLayer();
|
|
settings.mInnerBodyShape = shape;
|
|
settings.mMass = shape.GetMassProperties().get_mMass();
|
|
settings.mMaxSlopeAngle = gdjs.toRad(_slopeMaxAngle);
|
|
settings.mShape = shape;
|
|
settings.mUp = Jolt.Vec3.prototype.sAxisZ();
|
|
settings.mBackFaceMode = Jolt.EBackFaceMode_CollideWithBackFaces;
|
|
// TODO Should we make them configurable?
|
|
//settings.mMaxStrength = maxStrength;
|
|
//settings.mCharacterPadding = characterPadding;
|
|
//settings.mPenetrationRecoverySpeed = penetrationRecoverySpeed;
|
|
//settings.mPredictiveContactDistance = predictiveContactDistance;
|
|
const depth = owner3D.getDepth() * _sharedData.worldInvScale;
|
|
const width = owner3D.getWidth() * _sharedData.worldInvScale;
|
|
const height = owner3D.getHeight() * _sharedData.worldInvScale;
|
|
// Only the bottom of the capsule can make a contact with the floor.
|
|
// It avoids characters from sticking to walls.
|
|
const capsuleHalfLength = depth / 2;
|
|
const capsuleRadius = Math.sqrt(width * height) / 2;
|
|
settings.mSupportingVolume = new Jolt.Plane(
|
|
Jolt.Vec3.prototype.sAxisZ(),
|
|
// TODO It's strange that the value is positive.
|
|
// Use a big safety margin as the ground normal will be checked anyway.
|
|
// It only avoids to detect walls as ground.
|
|
capsuleHalfLength -
|
|
capsuleRadius *
|
|
(1 - Math.cos(gdjs.toRad(Math.min(_slopeMaxAngle + 20, 70))))
|
|
);
|
|
const character = new Jolt.CharacterVirtual(
|
|
settings,
|
|
this.characterBehavior._getPhysicsPosition(
|
|
_sharedData.getRVec3(0, 0, 0)
|
|
),
|
|
behavior._getPhysicsRotation(_sharedData.getQuat(0, 0, 0, 1)),
|
|
_sharedData.physicsSystem
|
|
);
|
|
Jolt.destroy(settings);
|
|
const body = _sharedData.physicsSystem
|
|
.GetBodyLockInterface()
|
|
.TryGetBody(character.GetInnerBodyID());
|
|
if (this.characterBehavior.character) {
|
|
if (this.characterBehavior._canBePushed) {
|
|
this.characterBehavior.charactersManager.removeCharacter(
|
|
this.characterBehavior.character
|
|
);
|
|
// Character.mListener is a plain pointer, it's not destroyed with the character.
|
|
Jolt.destroy(this.characterBehavior.character.GetListener());
|
|
}
|
|
Jolt.destroy(this.characterBehavior.character);
|
|
}
|
|
this.characterBehavior.character = character;
|
|
|
|
if (this.characterBehavior._canBePushed) {
|
|
// CharacterVsCharacterCollisionSimple handle characters pushing each other.
|
|
this.characterBehavior.charactersManager.addCharacter(character);
|
|
|
|
const characterContactListener =
|
|
new Jolt.CharacterContactListenerJS();
|
|
characterContactListener.OnAdjustBodyVelocity = (
|
|
character,
|
|
body2Ptr,
|
|
linearVelocityPtr,
|
|
angularVelocity
|
|
) => {};
|
|
characterContactListener.OnContactValidate = (
|
|
character,
|
|
bodyID2,
|
|
subShapeID2
|
|
) => {
|
|
return true;
|
|
};
|
|
characterContactListener.OnCharacterContactValidate = (
|
|
characterPtr,
|
|
otherCharacterPtr,
|
|
subShapeID2
|
|
) => {
|
|
// CharacterVsCharacterCollisionSimple doesn't handle collision layers.
|
|
// We have to filter characters ourself.
|
|
const character = Jolt.wrapPointer(
|
|
characterPtr,
|
|
Jolt.CharacterVirtual
|
|
);
|
|
const otherCharacter = Jolt.wrapPointer(
|
|
otherCharacterPtr,
|
|
Jolt.CharacterVirtual
|
|
);
|
|
|
|
const body = _sharedData.physicsSystem
|
|
.GetBodyLockInterface()
|
|
.TryGetBody(character.GetInnerBodyID());
|
|
const otherBody = _sharedData.physicsSystem
|
|
.GetBodyLockInterface()
|
|
.TryGetBody(otherCharacter.GetInnerBodyID());
|
|
|
|
const physicsBehavior = body.gdjsAssociatedBehavior;
|
|
const otherPhysicsBehavior = otherBody.gdjsAssociatedBehavior;
|
|
|
|
if (!physicsBehavior || !otherPhysicsBehavior) {
|
|
return true;
|
|
}
|
|
return physicsBehavior.canCollideAgainst(otherPhysicsBehavior);
|
|
};
|
|
characterContactListener.OnContactAdded = (
|
|
character,
|
|
bodyID2,
|
|
subShapeID2,
|
|
contactPosition,
|
|
contactNormal,
|
|
settings
|
|
) => {};
|
|
characterContactListener.OnContactPersisted = (
|
|
inCharacter,
|
|
inBodyID2,
|
|
inSubShapeID2,
|
|
inContactPosition,
|
|
inContactNormal,
|
|
ioSettings
|
|
) => {};
|
|
characterContactListener.OnContactRemoved = (
|
|
inCharacter,
|
|
inBodyID2,
|
|
inSubShapeID2
|
|
) => {};
|
|
characterContactListener.OnCharacterContactAdded = (
|
|
character,
|
|
otherCharacter,
|
|
subShapeID2,
|
|
contactPosition,
|
|
contactNormal,
|
|
settings
|
|
) => {};
|
|
characterContactListener.OnCharacterContactPersisted = (
|
|
inCharacter,
|
|
inOtherCharacter,
|
|
inSubShapeID2,
|
|
inContactPosition,
|
|
inContactNormal,
|
|
ioSettings
|
|
) => {};
|
|
characterContactListener.OnCharacterContactRemoved = (
|
|
inCharacter,
|
|
inOtherCharacter,
|
|
inSubShapeID2
|
|
) => {};
|
|
characterContactListener.OnContactSolve = (
|
|
character,
|
|
bodyID2,
|
|
subShapeID2,
|
|
contactPosition,
|
|
contactNormal,
|
|
contactVelocity,
|
|
contactMaterial,
|
|
characterVelocity,
|
|
newCharacterVelocity
|
|
) => {};
|
|
characterContactListener.OnCharacterContactSolve = (
|
|
character,
|
|
otherCharacter,
|
|
subShapeID2,
|
|
contactPosition,
|
|
contactNormal,
|
|
contactVelocity,
|
|
contactMaterial,
|
|
characterVelocityPtr,
|
|
newCharacterVelocityPtr
|
|
) => {};
|
|
character.SetListener(characterContactListener);
|
|
}
|
|
|
|
// TODO This is not really reliable. We could choose to disable it and force user to use the "is on platform" condition.
|
|
//body.SetCollideKinematicVsNonDynamic(true);
|
|
return body;
|
|
}
|
|
|
|
updateObjectFromBody() {
|
|
const { character } = this.characterBehavior;
|
|
if (!character) {
|
|
return;
|
|
}
|
|
// We can't rely on the body position because of mCharacterPadding.
|
|
this.characterBehavior._moveObjectToPhysicsPosition(
|
|
character.GetPosition()
|
|
);
|
|
this.characterBehavior._moveObjectToPhysicsRotation(
|
|
character.GetRotation()
|
|
);
|
|
}
|
|
|
|
updateBodyFromObject() {
|
|
const physics3D = this.characterBehavior.getPhysics3D();
|
|
if (!physics3D) {
|
|
return;
|
|
}
|
|
const { behavior } = physics3D;
|
|
const { character, owner3D, _sharedData } = this.characterBehavior;
|
|
if (!character) {
|
|
return;
|
|
}
|
|
if (
|
|
behavior._objectOldX !== owner3D.getX() ||
|
|
behavior._objectOldY !== owner3D.getY() ||
|
|
behavior._objectOldZ !== owner3D.getZ()
|
|
) {
|
|
this.updateCharacterPosition();
|
|
}
|
|
if (
|
|
behavior._objectOldRotationX !== owner3D.getRotationX() ||
|
|
behavior._objectOldRotationY !== owner3D.getRotationY() ||
|
|
behavior._objectOldRotationZ !== owner3D.getAngle()
|
|
) {
|
|
character.SetRotation(
|
|
this.characterBehavior._getPhysicsRotation(
|
|
_sharedData.getQuat(0, 0, 0, 1)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
updateCharacterPosition() {
|
|
const { character, _sharedData } = this.characterBehavior;
|
|
if (!character) {
|
|
return;
|
|
}
|
|
character.SetPosition(
|
|
this.characterBehavior._getPhysicsPosition(
|
|
_sharedData.getRVec3(0, 0, 0)
|
|
)
|
|
);
|
|
}
|
|
|
|
recreateShape() {
|
|
const physics3D = this.characterBehavior.getPhysics3D();
|
|
if (!physics3D) {
|
|
return;
|
|
}
|
|
const {
|
|
behavior,
|
|
broadPhaseLayerFilter,
|
|
objectLayerFilter,
|
|
bodyFilter,
|
|
shapeFilter,
|
|
} = physics3D;
|
|
const { character, _sharedData } = this.characterBehavior;
|
|
if (!character) {
|
|
return;
|
|
}
|
|
const shape = behavior.createShapeWithoutMassCenterOffset();
|
|
const isShapeValid = character.SetShape(
|
|
shape,
|
|
Number.MAX_VALUE,
|
|
broadPhaseLayerFilter,
|
|
objectLayerFilter,
|
|
bodyFilter,
|
|
shapeFilter,
|
|
_sharedData.jolt.GetTempAllocator()
|
|
);
|
|
if (!isShapeValid) {
|
|
return;
|
|
}
|
|
character.SetInnerBodyShape(shape);
|
|
character.SetMass(shape.GetMassProperties().get_mMass());
|
|
|
|
// shapeHalfDepth may have changed, update the character position accordingly.
|
|
this.updateCharacterPosition();
|
|
}
|
|
|
|
destroyBody() {
|
|
this.characterBehavior._destroyBody();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A character is simulated by Jolt before the rest of the physics simulation
|
|
* (see `doBeforePhysicsStep`).
|
|
* This means that contacts with the character would only rarely be recognized by
|
|
* the physics engine if using the default contact listeners.
|
|
* Instead, this class allows to properly track contacts of the character
|
|
* using Jolt `CharacterVirtual::GetActiveContacts`.
|
|
*/
|
|
export class CharacterCollisionChecker
|
|
implements gdjs.Physics3DRuntimeBehavior.CollisionChecker
|
|
{
|
|
characterBehavior: gdjs.PhysicsCharacter3DRuntimeBehavior;
|
|
|
|
_currentContacts: Array<Physics3DRuntimeBehavior> = [];
|
|
_previousContacts: Array<Physics3DRuntimeBehavior> = [];
|
|
|
|
constructor(characterBehavior: gdjs.PhysicsCharacter3DRuntimeBehavior) {
|
|
this.characterBehavior = characterBehavior;
|
|
}
|
|
|
|
clearContacts(): void {
|
|
this._previousContacts.length = 0;
|
|
this._currentContacts.length = 0;
|
|
}
|
|
|
|
updateContacts(): void {
|
|
const swap = this._previousContacts;
|
|
this._previousContacts = this._currentContacts;
|
|
this._currentContacts = swap;
|
|
this._currentContacts.length = 0;
|
|
|
|
const { character, _sharedData } = this.characterBehavior;
|
|
if (!character) {
|
|
return;
|
|
}
|
|
const contacts = character.GetActiveContacts();
|
|
for (let index = 0; index < contacts.size(); index++) {
|
|
const contact = contacts.at(index);
|
|
|
|
const bodyLockInterface =
|
|
_sharedData.physicsSystem.GetBodyLockInterface();
|
|
const body = bodyLockInterface.TryGetBody(contact.mBodyB);
|
|
const behavior = body.gdjsAssociatedBehavior;
|
|
if (behavior) {
|
|
this._currentContacts.push(behavior);
|
|
}
|
|
}
|
|
}
|
|
|
|
isColliding(object: gdjs.RuntimeObject): boolean {
|
|
const { character } = this.characterBehavior;
|
|
if (!character) {
|
|
return false;
|
|
}
|
|
return this._currentContacts.some(
|
|
(behavior) => behavior.owner === object
|
|
);
|
|
}
|
|
|
|
hasCollisionStartedWith(object: gdjs.RuntimeObject): boolean {
|
|
const { character } = this.characterBehavior;
|
|
if (!character) {
|
|
return false;
|
|
}
|
|
return (
|
|
this._currentContacts.some((behavior) => behavior.owner === object) &&
|
|
!this._previousContacts.some((behavior) => behavior.owner === object)
|
|
);
|
|
}
|
|
|
|
hasCollisionStoppedWith(object: gdjs.RuntimeObject): boolean {
|
|
const { character } = this.characterBehavior;
|
|
if (!character) {
|
|
return false;
|
|
}
|
|
return (
|
|
!this._currentContacts.some(
|
|
(behavior) => behavior.owner === object
|
|
) &&
|
|
this._previousContacts.some((behavior) => behavior.owner === object)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|