新增角色控制器

This commit is contained in:
yhh
2021-06-22 14:41:21 +08:00
parent fe308d35f2
commit e5dfb20aa2
13 changed files with 2074 additions and 31 deletions

View File

@@ -37,7 +37,7 @@ module es {
public _coroutineManager: CoroutineManager = new CoroutineManager();
public _timerManager: TimerManager = new TimerManager();
private constructor(debug: boolean = true, enableEntitySystems: boolean = true, remoteUrl: string = "") {
private constructor(debug: boolean = true, enableEntitySystems: boolean = true) {
Core._instance = this;
Core.emitter = new Emitter<CoreEvents>();
Core.emitter.addObserver(CoreEvents.frameUpdated, this.update, this);

View File

@@ -0,0 +1,588 @@
module es {
class CharacterRaycastOrigins {
public topLeft: Vector2;
public bottomRight: Vector2;
public bottomLeft: Vector2;
public constructor() {
this.topLeft = Vector2.zero;
this.bottomRight = Vector2.zero;
this.bottomLeft = Vector2.zero;
}
}
class CharacterCollisionState2D {
public right: boolean = false;
public left: boolean = false;
public above: boolean = false;
public below: boolean = false;
public becameGroundedThisFrame: boolean = false;
public wasGroundedLastFrame: boolean = false;
public movingDownSlope: boolean = false;
public slopeAngle: number = 0;
public hasCollision(): boolean {
return this.below || this.right || this.left || this.above;
}
public reset(): void {
this.right = this.left = false;
this.above = this.below = false;
this.becameGroundedThisFrame = this.movingDownSlope = false;
this.slopeAngle = 0;
}
public toString(): string {
return `[CharacterCollisionState2D] r: ${this.right}, l: ${this.left}, a: ${this.above}, b: ${this.below}, movingDownSlope: ${this.movingDownSlope}, angle: ${this.slopeAngle}, wasGroundedLastFrame: ${this.wasGroundedLastFrame}, becameGroundedThisFrame: ${this.becameGroundedThisFrame}`;
}
}
export class CharacterController implements ITriggerListener {
public onControllerCollidedEvent: ObservableT<RaycastHit>;
public onTriggerEnterEvent: ObservableT<Collider>;
public onTriggerExitEvent: ObservableT<Collider>;
/**
* 如果为 true则在垂直移动单帧时将忽略平台的一种方式
*/
public ignoreOneWayPlatformsTime: number;
public supportSlopedOneWayPlatforms: boolean;
public ignoredColliders: Set<Collider> = new Set();
/**
* 定义距离碰撞射线的边缘有多远。
* 如果使用 0 范围进行投射,则通常会导致不需要的光线击中(例如,直接从表面水平投射的足部碰撞器可能会导致击中)
*/
public get skinWidth() {
return this._skinWidth;
}
public set skinWidth(value: number) {
this._skinWidth = value;
this.recalculateDistanceBetweenRays();
}
/**
* CC2D 可以爬升的最大坡度角
*/
public slopeLimit: number = 30;
/**
* 构成跳跃的帧之间垂直运动变化的阈值
*/
public jumpingThreshold: number = -7;
/**
* 基于斜率乘以速度的曲线(负 = 下坡和正 = 上坡)
*/
public slopeSpeedMultiplier: AnimCurve;
public totalHorizontalRays: number = 5;
public totalVerticalRays: number = 3;
public collisionState: CharacterCollisionState2D = new CharacterCollisionState2D();
public velocity: Vector2 = new Vector2(0, 0);
public get isGrounded(): boolean {
return this.collisionState.below;
}
public get raycastHitsThisFrame(): RaycastHit[] {
return this._raycastHitsThisFrame;
}
public constructor(
player: Entity,
skinWidth?: number,
platformMask: number = -1,
onewayPlatformMask: number = -1,
triggerMask: number = -1
) {
this.onTriggerEnterEvent = new ObservableT();
this.onTriggerExitEvent = new ObservableT();
this.onControllerCollidedEvent = new ObservableT();
this.platformMask = platformMask;
this.oneWayPlatformMask = onewayPlatformMask;
this.triggerMask = triggerMask;
// 将我们的单向平台添加到我们的普通平台掩码中,以便我们可以从上方降落
this.platformMask |= this.oneWayPlatformMask;
this._player = player;
let collider = null;
for (let i = 0; i < this._player.components.buffer.length; i++) {
let component = this._player.components.buffer[i];
if (component instanceof Collider) {
collider = component;
break;
}
}
collider.isTrigger = false;
if (collider instanceof BoxCollider) {
this._collider = collider as BoxCollider;
} else {
throw new Error('player collider must be box');
}
// 在这里,我们触发了具有主体的 setter 的属性
this.skinWidth = skinWidth || collider.width * 0.05;
this._slopeLimitTangent = Math.tan(75 * MathHelper.Deg2Rad);
this._triggerHelper = new ColliderTriggerHelper(this._player);
// 我们想设置我们的 CC2D 忽略所有碰撞层,除了我们的 triggerMask
for (let i = 0; i < 32; i++) {
// 查看我们的 triggerMask 是否包含此层,如果不包含则忽略它
if ((this.triggerMask & (1 << i)) === 0) {
Flags.unsetFlag(this._collider.collidesWithLayers, i);
}
}
}
public onTriggerEnter(other: Collider, local: Collider): void {
this.onTriggerEnterEvent.notify(other);
}
public onTriggerExit(other: Collider, local: Collider): void {
this.onTriggerExitEvent.notify(other);
}
/**
* 尝试将角色移动到位置 + deltaMovement。 任何挡路的碰撞器都会在遇到时导致运动停止
* @param deltaMovement
* @param deltaTime
*/
public move(deltaMovement: Vector2, deltaTime: number): void {
this.collisionState.wasGroundedLastFrame = this.collisionState.below;
this.collisionState.reset();
this._raycastHitsThisFrame = [];
this._isGoingUpSlope = false;
this.primeRaycastOrigins();
if (deltaMovement.y > 0 && this.collisionState.wasGroundedLastFrame) {
deltaMovement = this.handleVerticalSlope(deltaMovement);
}
if (deltaMovement.x !== 0) {
deltaMovement = this.moveHorizontally(deltaMovement);
}
if (deltaMovement.y !== 0) {
deltaMovement = this.moveVertically(deltaMovement);
}
this._player.setPosition(
this._player.position.x + deltaMovement.x,
this._player.position.y + deltaMovement.y
);
if (deltaTime > 0) {
this.velocity.x = deltaMovement.x / deltaTime;
this.velocity.y = deltaMovement.y / deltaTime;
}
if (
!this.collisionState.wasGroundedLastFrame &&
this.collisionState.below
) {
this.collisionState.becameGroundedThisFrame = true;
}
if (this._isGoingUpSlope) {
this.velocity.y = 0;
}
if (!this._isWarpingToGround) {
this._triggerHelper.update();
}
for (let i = 0; i < this._raycastHitsThisFrame.length; i++) {
this.onControllerCollidedEvent.notify(this._raycastHitsThisFrame[i]);
}
if (this.ignoreOneWayPlatformsTime > 0) {
this.ignoreOneWayPlatformsTime -= deltaTime;
}
}
/**
* 直接向下移动直到接地
* @param maxDistance
*/
public warpToGrounded(maxDistance: number = 1000): void {
this.ignoreOneWayPlatformsTime = 0;
this._isWarpingToGround = true;
let delta = 0;
do {
delta += 1;
this.move(new Vector2(0, 1), 0.02);
if (delta > maxDistance) {
break;
}
} while (!this.isGrounded);
this._isWarpingToGround = false;
}
/**
* 这应该在您必须在运行时修改 BoxCollider2D 的任何时候调用。
* 它将重新计算用于碰撞检测的光线之间的距离。
* 它也用于 skinWidth setter以防在运行时更改。
*/
public recalculateDistanceBetweenRays(): void {
const colliderUsableHeight =
this._collider.height * Math.abs(this._player.scale.y) -
2 * this._skinWidth;
this._verticalDistanceBetweenRays =
colliderUsableHeight / (this.totalHorizontalRays - 1);
const colliderUsableWidth =
this._collider.width * Math.abs(this._player.scale.x) -
2 * this._skinWidth;
this._horizontalDistanceBetweenRays =
colliderUsableWidth / (this.totalVerticalRays - 1);
}
/**
* 将 raycastOrigins 重置为由 skinWidth 插入的框碰撞器的当前范围。
* 插入它是为了避免从直接接触另一个碰撞器的位置投射光线,从而导致不稳定的法线数据。
*/
private primeRaycastOrigins(): void {
const rect = this._collider.bounds;
this._raycastOrigins.topLeft = new Vector2(
rect.x + this._skinWidth,
rect.y + this._skinWidth
);
this._raycastOrigins.bottomRight = new Vector2(
rect.right - this._skinWidth,
rect.bottom - this._skinWidth
);
this._raycastOrigins.bottomLeft = new Vector2(
rect.x + this._skinWidth,
rect.bottom - this._skinWidth
);
}
/**
* 我们必须在这方面使用一些技巧。
* 光线必须从我们的碰撞器skinWidth内部的一小段距离投射以避免零距离光线会得到错误的法线。
* 由于这个小偏移,我们必须增加光线距离 skinWidth 然后记住在实际移动玩家之前从 deltaMovement 中删除 skinWidth
* @param deltaMovement
* @returns
*/
private moveHorizontally(deltaMovement: Vector2): Vector2 {
const isGoingRight = deltaMovement.x > 0;
let rayDistance =
Math.abs(deltaMovement.x) +
this._skinWidth * this.rayOriginSkinMutiplier;
const rayDirection: Vector2 = isGoingRight ? Vector2.right : Vector2.left;
const initialRayOriginY = this._raycastOrigins.bottomLeft.y;
const initialRayOriginX = isGoingRight
? this._raycastOrigins.bottomRight.x -
this._skinWidth * (this.rayOriginSkinMutiplier - 1)
: this._raycastOrigins.bottomLeft.x +
this._skinWidth * (this.rayOriginSkinMutiplier - 1);
for (let i = 0; i < this.totalHorizontalRays; i++) {
const ray = new Vector2(
initialRayOriginX,
initialRayOriginY - i * this._verticalDistanceBetweenRays
);
// if we are grounded we will include oneWayPlatforms
// only on the first ray (the bottom one). this will allow us to
// walk up sloped oneWayPlatforms
if (
i === 0 &&
this.supportSlopedOneWayPlatforms &&
this.collisionState.wasGroundedLastFrame
) {
this._raycastHit = Physics.linecastIgnoreCollider(
ray,
ray.add(rayDirection.multiplyScaler(rayDistance)),
this.platformMask,
this.ignoredColliders
);
} else {
this._raycastHit = Physics.linecastIgnoreCollider(
ray,
ray.add(rayDirection.multiplyScaler(rayDistance)),
this.platformMask & ~this.oneWayPlatformMask,
this.ignoredColliders
);
}
if (this._raycastHit.collider) {
if (
i === 0 &&
this.handleHorizontalSlope(
deltaMovement,
Vector2.unsignedAngle(this._raycastHit.normal, Vector2.up)
)
) {
this._raycastHitsThisFrame.push(this._raycastHit);
break;
}
deltaMovement.x = this._raycastHit.point.x - ray.x;
rayDistance = Math.abs(deltaMovement.x);
if (isGoingRight) {
deltaMovement.x -= this._skinWidth * this.rayOriginSkinMutiplier;
this.collisionState.right = true;
} else {
deltaMovement.x += this._skinWidth * this.rayOriginSkinMutiplier;
this.collisionState.left = true;
}
this._raycastHitsThisFrame.push(this._raycastHit);
if (
rayDistance <
this._skinWidth * this.rayOriginSkinMutiplier +
this.kSkinWidthFloatFudgeFactor
) {
break;
}
}
}
return deltaMovement;
}
private moveVertically(deltaMovement: Vector2): Vector2 {
const isGoingUp = deltaMovement.y < 0;
let rayDistance =
Math.abs(deltaMovement.y) +
this._skinWidth * this.rayOriginSkinMutiplier;
const rayDirection = isGoingUp ? Vector2.up : Vector2.down;
let initialRayOriginX = this._raycastOrigins.topLeft.x;
const initialRayOriginY = isGoingUp
? this._raycastOrigins.topLeft.y +
this._skinWidth * (this.rayOriginSkinMutiplier - 1)
: this._raycastOrigins.bottomLeft.y -
this._skinWidth * (this.rayOriginSkinMutiplier - 1);
initialRayOriginX += deltaMovement.x;
let mask = this.platformMask;
if (isGoingUp || this.ignoreOneWayPlatformsTime > 0) {
mask &= ~this.oneWayPlatformMask;
}
for (let i = 0; i < this.totalVerticalRays; i++) {
const rayStart = new Vector2(
initialRayOriginX + i * this._horizontalDistanceBetweenRays,
initialRayOriginY
);
this._raycastHit = Physics.linecastIgnoreCollider(
rayStart,
rayStart.add(rayDirection.multiplyScaler(rayDistance)),
mask,
this.ignoredColliders
);
if (this._raycastHit.collider) {
deltaMovement.y = this._raycastHit.point.y - rayStart.y;
rayDistance = Math.abs(deltaMovement.y);
if (isGoingUp) {
deltaMovement.y += this._skinWidth * this.rayOriginSkinMutiplier;
this.collisionState.above = true;
} else {
deltaMovement.y -= this._skinWidth * this.rayOriginSkinMutiplier;
this.collisionState.below = true;
}
this._raycastHitsThisFrame.push(this._raycastHit);
if (!isGoingUp && deltaMovement.y < -0.00001) {
this._isGoingUpSlope = true;
}
if (
rayDistance <
this._skinWidth * this.rayOriginSkinMutiplier +
this.kSkinWidthFloatFudgeFactor
) {
break;
}
}
}
return deltaMovement;
}
/**
* 检查 BoxCollider2D 下的中心点是否存在坡度。
* 如果找到一个,则调整 deltaMovement 以便玩家保持接地并考虑slopeSpeedModifier 以加快移动速度。
* @param deltaMovement
* @returns
*/
private handleVerticalSlope(deltaMovement: Vector2): Vector2 {
const centerOfCollider =
(this._raycastOrigins.bottomLeft.x +
this._raycastOrigins.bottomRight.x) *
0.5;
const rayDirection = Vector2.down;
const slopeCheckRayDistance =
this._slopeLimitTangent *
(this._raycastOrigins.bottomRight.x - centerOfCollider);
const slopeRay = new Vector2(
centerOfCollider,
this._raycastOrigins.bottomLeft.y
);
this._raycastHit = Physics.linecastIgnoreCollider(
slopeRay,
slopeRay.add(rayDirection.multiplyScaler(slopeCheckRayDistance)),
this.platformMask,
this.ignoredColliders
);
if (this._raycastHit.collider) {
const angle = Vector2.unsignedAngle(this._raycastHit.normal, Vector2.up);
if (angle === 0) {
return deltaMovement;
}
const isMovingDownSlope =
Math.sign(this._raycastHit.normal.x) === Math.sign(deltaMovement.x);
if (isMovingDownSlope) {
const slopeModifier = this.slopeSpeedMultiplier
? this.slopeSpeedMultiplier.lerp(-angle)
: 1;
deltaMovement.y +=
this._raycastHit.point.y - slopeRay.y - this.skinWidth;
deltaMovement.x *= slopeModifier;
this.collisionState.movingDownSlope = true;
this.collisionState.slopeAngle = angle;
}
}
return deltaMovement;
}
/**
* 如果我们要上坡,则处理调整 deltaMovement
* @param deltaMovement
* @param angle
* @returns
*/
private handleHorizontalSlope(
deltaMovement: Vector2,
angle: number
): boolean {
if (Math.round(angle) === 90) {
return false;
}
if (angle < this.slopeLimit) {
if (deltaMovement.y > this.jumpingThreshold) {
const slopeModifier = this.slopeSpeedMultiplier
? this.slopeSpeedMultiplier.lerp(angle)
: 1;
deltaMovement.x *= slopeModifier;
deltaMovement.y = Math.abs(
Math.tan(angle * MathHelper.Deg2Rad) * deltaMovement.x
);
const isGoingRight = deltaMovement.x > 0;
const ray = isGoingRight
? this._raycastOrigins.bottomRight
: this._raycastOrigins.bottomLeft;
let raycastHit = null;
if (
this.supportSlopedOneWayPlatforms &&
this.collisionState.wasGroundedLastFrame
) {
raycastHit = Physics.linecastIgnoreCollider(
ray,
ray.add(deltaMovement),
this.platformMask,
this.ignoredColliders
);
} else {
raycastHit = Physics.linecastIgnoreCollider(
ray,
ray.add(deltaMovement),
this.platformMask & ~this.oneWayPlatformMask,
this.ignoredColliders
);
}
if (raycastHit.collider) {
deltaMovement.x = raycastHit.point.x - ray.x;
deltaMovement.y = raycastHit.point.y - ray.y;
if (isGoingRight) {
deltaMovement.x -= this._skinWidth;
} else {
deltaMovement.x += this._skinWidth;
}
}
this._isGoingUpSlope = true;
this.collisionState.below = true;
}
} else {
deltaMovement.x = 0;
}
return true;
}
private _player: Entity;
private _collider: BoxCollider;
private _skinWidth: number = 0.02;
private _triggerHelper: ColliderTriggerHelper;
/**
* 这用于计算为检查坡度而投射的向下光线。
* 我们使用有点随意的值 75 度来计算检查斜率的射线的长度。
*/
private _slopeLimitTangent: number;
private readonly kSkinWidthFloatFudgeFactor: number = 0.001;
/**
* 我们的光线投射原点角的支架TR、TL、BR、BL
*/
private _raycastOrigins: CharacterRaycastOrigins = new CharacterRaycastOrigins();
/**
* 存储我们在移动过程中命中的光线投射
*/
private _raycastHit: RaycastHit = new RaycastHit();
/**
* 存储此帧发生的任何光线投射命中。
* 我们必须存储它们,以防我们遇到水平和垂直移动的碰撞,以便我们可以在设置所有碰撞状态后发送事件
*/
private _raycastHitsThisFrame: RaycastHit[];
// 水平/垂直移动数据
private _verticalDistanceBetweenRays: number;
private _horizontalDistanceBetweenRays: number;
/**
* 我们使用这个标志来标记我们正在爬坡的情况,我们修改了 delta.y 以允许爬升。
* 原因是,如果我们到达斜坡的尽头,我们可以进行调整以保持接地
*/
private _isGoingUpSlope: boolean = false;
private _isWarpingToGround: boolean = true;
private platformMask: number = -1;
private triggerMask: number = -1;
private oneWayPlatformMask: number = -1;
private readonly rayOriginSkinMutiplier = 4;
}
}

View File

@@ -1,5 +1,6 @@
module es {
export abstract class Collider extends Component {
protected _isEnabled: boolean = true;
/**
* 对撞机的基本形状
*/

View File

@@ -129,6 +129,10 @@ module es {
return from + (to - from) * this.clamp01(t);
}
public static betterLerp(a: number, b: number, t: number, epsilon: number): number {
return Math.abs(a - b) < epsilon ? b : MathHelper.lerp(a, b, t);
}
/**
* 使度数的角度在a和b之间
* 用于处理360度环绕
@@ -604,5 +608,68 @@ module es {
return !Number.isFinite(x);
}
public static smoothDamp(current: number, target: number, currentVelocity: number, smoothTime: number, maxSpeed: number, deltaTime: number): { value: number; currentVelocity: number } {
smoothTime = Math.max(0.0001, smoothTime);
const num: number = 2 / smoothTime;
const num2: number = num * deltaTime;
const num3: number =
1 /
(1 + (num2 + (0.48 * (num2 * num2) + 0.235 * (num2 * (num2 * num2)))));
let num4: number = current - target;
const num5: number = target;
const num6: number = maxSpeed * smoothTime;
num4 = this.clamp(num4, num6 * -1, num6);
target = current - num4;
const num7: number = (currentVelocity + num * num4) * deltaTime;
currentVelocity = (currentVelocity - num * num7) * num3;
let num8: number = target + (num4 + num7) * num3;
if (num5 - current > 0 === num8 > num5) {
num8 = num5;
currentVelocity = (num8 - num5) / deltaTime;
}
return { value: num8, currentVelocity };
}
public static smoothDampVector(current: Vector2, target: Vector2, currentVelocity: Vector2, smoothTime: number, maxSpeed: number, deltaTime: number): Vector2 {
const v = Vector2.zero;
const resX = this.smoothDamp(
current.x,
target.x,
currentVelocity.x,
smoothTime,
maxSpeed,
deltaTime
);
v.x = resX.value;
currentVelocity.x = resX.currentVelocity;
const resY = this.smoothDamp(
current.y,
target.y,
currentVelocity.y,
smoothTime,
maxSpeed,
deltaTime
);
v.y = resY.value;
currentVelocity.y = resY.currentVelocity;
return v;
}
/**
* 将值(在 leftMin - leftMax 范围内)映射到 rightMin - rightMax 范围内的值
* @param value
* @param leftMin
* @param leftMax
* @param rightMin
* @param rightMax
* @returns
*/
public static mapMinMax(value: number, leftMin: number, leftMax: number, rightMin: number, rightMax): number {
return rightMin + ((MathHelper.clamp(value, leftMin, leftMax) - leftMin) * (rightMax - rightMin)) / (leftMax - leftMin);
}
}
}

View File

@@ -30,6 +30,22 @@ module es {
return new Vector2(0, 1);
}
public static get up() {
return new Vector2(0, -1);
}
public static get down() {
return new Vector2(0, 1);
}
public static get left() {
return new Vector2(-1, 0);
}
public static get right() {
return new Vector2(1, 0);
}
/**
*
* @param value1
@@ -200,7 +216,7 @@ module es {
* @param from
* @param to
*/
public static angle(from: Vector2, to: Vector2): number{
public static angle(from: Vector2, to: Vector2): number {
from = Vector2.normalize(from);
to = Vector2.normalize(to);
return Math.acos(MathHelper.clamp(Vector2.dot(from, to), -1, 1)) * MathHelper.Rad2Deg;
@@ -348,10 +364,10 @@ module es {
* @returns 如果实例相同true 否则false
*/
public equals(other: Vector2 | object): boolean {
if (other instanceof Vector2){
if (other instanceof Vector2) {
return other.x == this.x && other.y == this.y;
}
return false;
}
@@ -390,11 +406,20 @@ module es {
* @param amount
* @returns
*/
public static hermite(value1: Vector2, tangent1: Vector2, value2: Vector2, tangent2: Vector2, amount: number){
public static hermite(value1: Vector2, tangent1: Vector2, value2: Vector2, tangent2: Vector2, amount: number) {
return new Vector2(MathHelper.hermite(value1.x, tangent1.x, value2.x, tangent2.x, amount),
MathHelper.hermite(value1.y, tangent1.y, value2.y, tangent2.y, amount));
}
public static unsignedAngle(from: Vector2, to: Vector2, round: boolean = true) {
from.normalize();
to.normalize();
const angle =
Math.acos(MathHelper.clamp(Vector2.dot(from, to), -1, 1)) * MathHelper.Rad2Deg;
return round ? Math.round(angle) : angle;
}
public clone(): Vector2 {
return new Vector2(this.x, this.y);
}

View File

@@ -156,6 +156,18 @@ module es {
return this._hitArray[0];
}
public static linecastIgnoreCollider(start: Vector2,end: Vector2,layerMask: number = this.allLayers,ignoredColliders: Set<Collider> = null): RaycastHit {
this._hitArray[0].reset();
Physics.linecastAllIgnoreCollider(
start,
end,
this._hitArray,
layerMask,
ignoredColliders
);
return this._hitArray[0].clone();
}
/**
* 通过空间散列强制执行一行并用该行命中的任何碰撞器填充hits数组
* @param start
@@ -172,6 +184,16 @@ module es {
return this._spatialHash.linecast(start, end, hits, layerMask);
}
public static linecastAllIgnoreCollider(start: Vector2,end: Vector2,hits: RaycastHit[],layerMask: number = this.allLayers,ignoredColliders: Set<Collider> = null): number {
return this._spatialHash.linecastIgnoreCollider(
start,
end,
hits,
layerMask,
ignoredColliders
);
}
/**
* 检查是否有对撞机落在一个矩形区域中
* @param rect

View File

@@ -30,7 +30,7 @@ module es {
*/
public centroid: Vector2;
constructor(collider?: Collider, fraction?: number, distance?: number, point?: Vector2, normal?: Vector2){
constructor(collider?: Collider, fraction?: number, distance?: number, point?: Vector2, normal?: Vector2) {
this.collider = collider;
this.fraction = fraction;
this.distance = distance;
@@ -38,26 +38,46 @@ module es {
this.centroid = Vector2.zero;
}
public setValues(collider: Collider, fraction: number, distance: number, point: Vector2){
public setValues(collider: Collider, fraction: number, distance: number, point: Vector2) {
this.collider = collider;
this.fraction = fraction;
this.distance = distance;
this.point = point;
}
public setValuesNonCollider(fraction: number, distance: number, point: Vector2, normal: Vector2){
public setValuesNonCollider(fraction: number, distance: number, point: Vector2, normal: Vector2) {
this.fraction = fraction;
this.distance = distance;
this.point = point;
this.normal = normal;
}
public reset(){
public setAllValues(collider: Collider, fraction: number, distance: number, point: Vector2, normal: Vector2) {
this.collider = collider;
this.fraction = fraction;
this.distance = distance;
this.point = point;
this.normal = normal;
}
public reset() {
this.collider = null;
this.fraction = this.distance = 0;
}
public toString(){
public clone(): RaycastHit {
const hit = new RaycastHit();
hit.setAllValues(
this.collider,
this.fraction,
this.distance,
this.point,
this.normal
);
return hit;
}
public toString() {
return `[RaycastHit] fraction: ${this.fraction}, distance: ${this.distance}, normal: ${this.normal}, centroid: ${this.centroid}, point: ${this.point}`;
}
}

View File

@@ -95,8 +95,8 @@ module es {
}
public debugDraw(secondsToDisplay: number) {
for (let x = this.gridBounds.x; x <= this.gridBounds.right; x ++) {
for (let y = this.gridBounds.y; y <= this.gridBounds.bottom; y ++) {
for (let x = this.gridBounds.x; x <= this.gridBounds.right; x++) {
for (let y = this.gridBounds.y; y <= this.gridBounds.bottom; y++) {
let cell = this.cellAtPosition(x, y);
if (cell != null && cell.length > 0)
this.debugDrawCellDetails(x, y, secondsToDisplay);
@@ -154,7 +154,7 @@ module es {
* @param hits
* @param layerMask
*/
public linecast(start: Vector2, end: Vector2, hits: RaycastHit[], layerMask: number){
public linecast(start: Vector2, end: Vector2, hits: RaycastHit[], layerMask: number) {
let ray = new Ray2D(start, end);
this._raycastParser.start(ray, hits, layerMask);
@@ -189,24 +189,24 @@ module es {
// 开始遍历并返回交叉单元格。
let cell = this.cellAtPosition(currentCell.x, currentCell.y);
if (cell != null && this._raycastParser.checkRayIntersection(currentCell.x, currentCell.y, cell)){
if (cell != null && this._raycastParser.checkRayIntersection(currentCell.x, currentCell.y, cell)) {
this._raycastParser.reset();
return this._raycastParser.hitCounter;
}
while (currentCell.x != lastCell.x || currentCell.y != lastCell.y){
if (tMaxX < tMaxY){
while (currentCell.x != lastCell.x || currentCell.y != lastCell.y) {
if (tMaxX < tMaxY) {
currentCell.x = MathHelper.approach(currentCell.x, lastCell.x, Math.abs(stepX));
tMaxX += tDeltaX;
}else{
} else {
currentCell.y = MathHelper.approach(currentCell.y, lastCell.y, Math.abs(stepY));
tMaxY += tDeltaY;
}
cell = this.cellAtPosition(currentCell.x, currentCell.y);
if (cell && this._raycastParser.checkRayIntersection(currentCell.x, currentCell.y, cell)){
if (cell && this._raycastParser.checkRayIntersection(currentCell.x, currentCell.y, cell)) {
this._raycastParser.reset();
return this._raycastParser.hitCounter;
}
@@ -217,6 +217,68 @@ module es {
return this._raycastParser.hitCounter;
}
public linecastIgnoreCollider(start: Vector2, end: Vector2, hits: RaycastHit[], layerMask: number, ignoredColliders: Set<Collider>): number {
start = start.clone();
const ray = new Ray2D(start, end);
this._raycastParser.startIgnoreCollider(ray, hits, layerMask, ignoredColliders);
start.x = start.x * this._inverseCellSize;
start.y = start.y * this._inverseCellSize;
const endCell = this.cellCoords(end.x, end.y);
let intX = Math.floor(start.x);
let intY = Math.floor(start.y);
let stepX = Math.sign(ray.direction.x);
let stepY = Math.sign(ray.direction.y);
if (intX === endCell.x) {
stepX = 0;
}
if (intY === endCell.y) {
stepY = 0;
}
const boundaryX = intX + (stepX > 0 ? 1 : 0);
const boundaryY = intY + (stepY > 0 ? 1 : 0);
let tMaxX = (boundaryX - start.x) / ray.direction.x;
let tMaxY = (boundaryY - start.y) / ray.direction.y;
if (ray.direction.x === 0 || stepX === 0) {
tMaxX = Number.POSITIVE_INFINITY;
}
if (ray.direction.y === 0 || stepY === 0) {
tMaxY = Number.POSITIVE_INFINITY;
}
const tDeltaX = stepX / ray.direction.x;
const tDeltaY = stepY / ray.direction.y;
let cell = this.cellAtPosition(intX, intY);
if (cell && this._raycastParser.checkRayIntersection(intX, intY, cell)) {
this._raycastParser.reset();
return this._raycastParser.hitCounter;
}
let n = 0;
while ((intX !== endCell.x || intY !== endCell.y) && n < 100) {
if (tMaxX < tMaxY) {
intX = intX + stepX;
tMaxX = tMaxX + tDeltaX;
} else {
intY = intY + stepY;
tMaxY = tMaxY + tDeltaY;
}
cell = this.cellAtPosition(intX, intY);
if (cell && this._raycastParser.checkRayIntersection(intX, intY, cell)) {
this._raycastParser.reset();
return this._raycastParser.hitCounter;
}
n++;
}
this._raycastParser.reset();
return this._raycastParser.hitCounter;
}
/**
* 获取所有在指定矩形范围内的碰撞器
* @param rect
@@ -232,16 +294,16 @@ module es {
for (let collider of potentials) {
if (collider instanceof BoxCollider) {
results[resultCounter] = collider;
resultCounter ++;
} else if(collider instanceof CircleCollider) {
resultCounter++;
} else if (collider instanceof CircleCollider) {
if (Collisions.rectToCircle(rect, collider.bounds.center, collider.bounds.width * 0.5)) {
results[resultCounter] = collider;
resultCounter ++;
resultCounter++;
}
} else if(collider instanceof PolygonCollider) {
} else if (collider instanceof PolygonCollider) {
if (collider.shape.overlaps(this._overlapTestBox)) {
results[resultCounter] = collider;
resultCounter ++;
resultCounter++;
}
} else {
throw new Error("overlapRectangle对这个类型没有实现!");
@@ -348,7 +410,7 @@ module es {
return this._store.get(this.getKey(x, y));
}
public getKey(x: number, y: number){
public getKey(x: number, y: number) {
return `${x}_${y}`;
}
@@ -372,6 +434,7 @@ module es {
public _cellHits: RaycastHit[] = [];
public _ray: Ray2D;
public _layerMask: number;
private _ignoredColliders: Set<Collider>;
public start(ray: Ray2D, hits: RaycastHit[], layerMask: number) {
this._ray = ray;
@@ -380,6 +443,14 @@ module es {
this.hitCounter = 0;
}
public startIgnoreCollider(ray: Ray2D, hits: RaycastHit[], layerMask: number, ignoredColliders: Set<Collider>) {
this._ray = ray;
this._hits = hits;
this._layerMask = layerMask;
this._ignoredColliders = ignoredColliders;
this.hitCounter = 0;
}
/**
* 如果hits数组被填充返回true。单元格不能为空!
* @param cellX
@@ -408,7 +479,7 @@ module es {
// TODO: 如果边界检查返回更多数据我们就不需要为BoxCollider检查做任何事情
// 在做形状测试之前先做一个边界检查
let colliderBounds = potential.bounds.clone();
if (colliderBounds.rayIntersects(this._ray, fraction) && fraction.value <= 1){
if (colliderBounds.rayIntersects(this._ray, fraction) && fraction.value <= 1) {
if (potential.shape.collidesWithLine(this._ray.start, this._ray.end, this._tempHit)) {
// 检查一下我们应该排除这些射线射线cast是否在碰撞器中开始
if (!Physics.raycastsStartInColliders && potential.shape.containsPoint(this._ray.start))
@@ -427,11 +498,11 @@ module es {
// 所有处理单元完成。对结果进行排序并将命中结果打包到结果数组中
this._cellHits.sort(RaycastResultParser.compareRaycastHits);
for (let i = 0; i < this._cellHits.length; i ++){
for (let i = 0; i < this._cellHits.length; i++) {
this._hits[this.hitCounter] = this._cellHits[i];
// 增加命中计数器,如果它已经达到数组大小的限制,我们就完成了
this.hitCounter ++;
this.hitCounter++;
if (this.hitCounter == this._hits.length)
return true;
}
@@ -439,7 +510,7 @@ module es {
return false;
}
public reset(){
public reset() {
this._hits = null;
this._checkedColliders.length = 0;
this._cellHits.length = 0;

View File

@@ -0,0 +1,48 @@
module es {
export interface IAnimFrame {
t: number;
value: number;
}
export class AnimCurve {
public get points(): IAnimFrame[] {
return this._points;
}
public constructor(points: IAnimFrame[]) {
if (points.length < 2) {
throw new Error('curve length must be >= 2');
}
points.sort((a: IAnimFrame, b: IAnimFrame) => {
return a.t - b.t;
});
if (points[0].t !== 0) {
throw new Error('curve must start with 0');
}
if (points[points.length - 1].t !== 1) {
throw new Error('curve must end with 1');
}
this._points = points;
}
public lerp(t: number): number {
for (let i = 1; i < this._points.length; i++) {
if (t <= this._points[i].t) {
const m = MathHelper.map01(
t,
this._points[i - 1].t,
this._points[i].t
);
return MathHelper.lerp(
this._points[i - 1].value,
this._points[i].value,
m
);
}
}
throw new Error('should never be here');
}
public _points: IAnimFrame[];
}
}

View File

@@ -0,0 +1,184 @@
module es {
export interface IListener {
caller: object;
callback: Function;
}
export interface IObservable {
addListener(caller: object, callback: Function);
removeListener(caller: object, callback: Function);
clearListener();
clearListenerWithCaller(caller: object);
}
export class Observable implements IObservable {
public constructor() {
this._listeners = [];
}
public addListener(caller: object, callback: Function) {
if (
this._listeners.findIndex(
listener =>
listener.callback === callback && listener.caller === caller
) === -1
) {
this._listeners.push({ caller, callback });
}
}
public removeListener(caller: object, callback: Function) {
const index = this._listeners.findIndex(
listener => listener.callback === callback && listener.caller === caller
);
if (index >= 0) {
this._listeners.splice(index, 1);
}
}
public clearListener() {
this._listeners = [];
}
public clearListenerWithCaller(caller: object) {
for (let i = this._listeners.length - 1; i >= 0; i--) {
const listener = this._listeners[i];
if (listener.caller === caller) {
this._listeners.splice(i, 1);
}
}
}
public notify(...args) {
for (let i = this._listeners.length - 1; i >= 0; i--) {
const listener = this._listeners[i];
if (listener.caller) {
listener.callback.call(listener.caller, ...args);
} else {
listener.callback(...args);
}
}
}
private _listeners: IListener[];
}
export class ObservableT<T> extends Observable {
public addListener(caller: object, callback: (arg: T) => void) {
super.addListener(caller, callback);
}
public removeListener(caller: object, callback: (arg: T) => void) {
super.removeListener(caller, callback);
}
public notify(arg: T) {
super.notify(arg);
}
}
export class ObservableTT<T, R> extends Observable {
public addListener(caller: object, callback: (arg1: T, arg2: R) => void) {
super.addListener(caller, callback);
}
public removeListener(caller: object, callback: (arg: T, arg2: R) => void) {
super.removeListener(caller, callback);
}
public notify(arg1: T, arg2: R) {
super.notify(arg1, arg2);
}
}
export class Command implements IObservable {
public constructor(caller: object, action: Function) {
this.bindAction(caller, action);
this._onExec = new Observable();
}
public bindAction(caller: object, action: Function) {
this._caller = caller;
this._action = action;
}
public dispatch(...args: any[]) {
if (this._action) {
if (this._caller) {
this._action.call(this._caller, ...args);
} else {
this._action(...args);
}
this._onExec.notify();
} else {
console.warn('command not bind with an action');
}
}
public addListener(caller: object, callback: Function) {
this._onExec.addListener(caller, callback);
}
public removeListener(caller: object, callback: Function) {
this._onExec.removeListener(caller, callback);
}
public clearListener() {
this._onExec.clearListener();
}
public clearListenerWithCaller(caller: object) {
this._onExec.clearListenerWithCaller(caller);
}
private _onExec: Observable;
private _caller: object;
private _action: Function;
}
export class ValueChangeCommand<T> implements IObservable {
public constructor(value: T) {
this._onValueChange = new Observable();
this._value = value;
}
public get onValueChange() {
return this._onValueChange;
}
public get value() {
return this._value;
}
public set value(newValue: T) {
this._value = newValue;
}
public dispatch(value: T) {
if (value !== this._value) {
const oldValue = this._value;
this._value = value;
this._onValueChange.notify(this._value, oldValue);
}
}
public addListener(caller: object, callback: Function) {
this._onValueChange.addListener(caller, callback);
}
public removeListener(caller: object, callback: Function) {
this._onValueChange.removeListener(caller, callback);
}
public clearListener() {
this._onValueChange.clearListener();
}
public clearListenerWithCaller(caller: object) {
this._onValueChange.clearListenerWithCaller(caller);
}
private _onValueChange: Observable;
private _value: T;
}
}