clean up
This commit is contained in:
parent
aa02d0ab6f
commit
b9274a42c7
4 changed files with 480 additions and 457 deletions
|
|
@ -7,6 +7,7 @@ import {
|
|||
MoveActionCommand,
|
||||
CharacterTeam,
|
||||
Id,
|
||||
rotate90Deg,
|
||||
} from 'shared';
|
||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||
import { PlayerContainer } from './player-container';
|
||||
|
|
@ -100,23 +101,9 @@ export class NPC extends PlayerBase {
|
|||
this.step(0);
|
||||
}
|
||||
|
||||
private timeUntilRespawn = 0;
|
||||
public step(deltaTimeInSeconds: number) {
|
||||
if (this.character) {
|
||||
this.center = this.character.center;
|
||||
|
||||
if (!this.character.isAlive) {
|
||||
this.sumDeaths++;
|
||||
this.sumKills = this.character.killCount;
|
||||
this.character = null;
|
||||
this.timeUntilRespawn = settings.playerDiedTimeout;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
|
||||
this.createCharacter();
|
||||
this.center = this.character!.center;
|
||||
}
|
||||
const character = this.stepLifecycle(deltaTimeInSeconds);
|
||||
if (!character) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -143,16 +130,16 @@ export class NPC extends PlayerBase {
|
|||
this.dodgeCommitRemaining -= deltaTimeInSeconds;
|
||||
this.dodgeCooldownRemaining -= deltaTimeInSeconds;
|
||||
const movement = this.decideMovement(this.nearObjects);
|
||||
this.character.handleMovementAction(new MoveActionCommand(movement));
|
||||
character.handleMovementAction(new MoveActionCommand(movement));
|
||||
|
||||
if (
|
||||
!this.isComingBack &&
|
||||
this.character.groundPlanet &&
|
||||
character.groundPlanet &&
|
||||
vec2.length(movement) > 0 &&
|
||||
Random.getRandom() <
|
||||
npcTuning.leapChancePerSecond * this.aggression * deltaTimeInSeconds
|
||||
) {
|
||||
this.character.leap();
|
||||
character.leap();
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
@ -178,18 +165,15 @@ export class NPC extends PlayerBase {
|
|||
const nearObjects = this.observe(npcTuning.planScanRadius);
|
||||
const enemies = this.enemiesByDistance(nearObjects);
|
||||
|
||||
if (enemies.length > 0) {
|
||||
const nearest = enemies[0];
|
||||
const nearest = enemies[0];
|
||||
if (nearest) {
|
||||
const fleeRange =
|
||||
npcTuning.fleeBaseRange * (npcTuning.fleeAggressionFalloff - this.aggression);
|
||||
if (nearest.distance < fleeRange) {
|
||||
vec2.subtract(this.direction, this.center, nearest.character.center);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (enemies.length > 0) {
|
||||
const nearest = enemies[0];
|
||||
const chaseRange =
|
||||
npcTuning.chaseBaseRange + npcTuning.chaseAggressionRange * this.aggression;
|
||||
if (nearest.distance < chaseRange) {
|
||||
|
|
@ -275,7 +259,7 @@ export class NPC extends PlayerBase {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const perpendicular = vec2.fromValues(-threat.direction.y, threat.direction.x);
|
||||
const perpendicular = rotate90Deg(threat.direction);
|
||||
const toMe = vec2.subtract(vec2.create(), this.center, threat.center);
|
||||
if (vec2.dot(perpendicular, toMe) < 0) {
|
||||
vec2.negate(perpendicular, perpendicular);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Random,
|
||||
settings,
|
||||
sanitizeName,
|
||||
boundRadius,
|
||||
} from 'shared';
|
||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
||||
|
|
@ -23,6 +24,7 @@ export abstract class PlayerBase extends CommandReceiver {
|
|||
|
||||
protected sumKills = 0;
|
||||
protected sumDeaths = 0;
|
||||
protected timeUntilRespawn = 0;
|
||||
|
||||
constructor(
|
||||
protected readonly playerInfo: PlayerInformation,
|
||||
|
|
@ -53,6 +55,44 @@ export abstract class PlayerBase extends CommandReceiver {
|
|||
|
||||
public abstract step(deltaTimeInSeconds: number): void;
|
||||
|
||||
/**
|
||||
* The death/respawn lifecycle both a connected player and an NPC go through:
|
||||
* bank the score off a body that just died, hold the corpse for the respawn
|
||||
* timeout, then build a new one. Returns whether there is a living character
|
||||
* to act with this tick — returned as the character itself, so a subclass can
|
||||
* `const character = this.stepLifecycle(dt); if (!character) return;` and go
|
||||
* on to use it without re-narrowing.
|
||||
*/
|
||||
protected stepLifecycle(deltaTimeInSeconds: number): CharacterPhysical | undefined {
|
||||
if (this.character) {
|
||||
this.center = this.character.center;
|
||||
|
||||
if (this.character.isAlive) {
|
||||
return this.character;
|
||||
}
|
||||
|
||||
this.sumDeaths++;
|
||||
this.sumKills = this.character.killCount;
|
||||
this.onCharacterDied(this.character);
|
||||
this.character = null;
|
||||
this.timeUntilRespawn = settings.playerDiedTimeout;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
|
||||
this.onBeforeRespawn();
|
||||
this.createCharacter();
|
||||
this.center = this.character!.center;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Hooks for what a subclass wants to do with the body it just lost, and just
|
||||
// before a new one is built. Both are no-ops by default.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected onCharacterDied(character: CharacterPhysical) {}
|
||||
protected onBeforeRespawn() {}
|
||||
|
||||
private findSpawnCenter(): vec2 {
|
||||
const planets = this.objectContainer
|
||||
.findIntersecting(
|
||||
|
|
@ -96,10 +136,7 @@ export abstract class PlayerBase extends CommandReceiver {
|
|||
radius * Math.sin(rotation) + preferredCenter.y,
|
||||
);
|
||||
|
||||
const playerBoundingCircle = new Circle(
|
||||
playerPosition,
|
||||
CharacterPhysical.boundRadius,
|
||||
);
|
||||
const playerBoundingCircle = new Circle(playerPosition, boundRadius);
|
||||
|
||||
const playerBoundingBox = getBoundingBoxOfCircle(playerBoundingCircle);
|
||||
const possibleIntersectors =
|
||||
|
|
|
|||
|
|
@ -135,7 +135,11 @@ export class PlayerContainer {
|
|||
}
|
||||
|
||||
public step(deltaTimeInSeconds: number) {
|
||||
this.players.forEach((p) => p.step(deltaTimeInSeconds));
|
||||
// Iterates the two backing arrays rather than the merged `players` getter:
|
||||
// this runs once per 200 Hz physics substep, and the getter builds a new
|
||||
// array every call.
|
||||
this._players.forEach((p) => p.step(deltaTimeInSeconds));
|
||||
this._npcs.forEach((p) => p.step(deltaTimeInSeconds));
|
||||
}
|
||||
|
||||
public stepCommunication(deltaTimeInSeconds: number) {
|
||||
|
|
|
|||
|
|
@ -52,9 +52,11 @@ const maximumBufferedBytes = settings.maxBufferedBytesPerClient;
|
|||
export class Player extends PlayerBase {
|
||||
// default, until the clients sends its real value
|
||||
private aspectRatio: number = 16 / 9;
|
||||
private timeUntilRespawn = 0;
|
||||
private timeSinceLastMessage = 0;
|
||||
private objectsPreviouslyInViewArea: Array<GameObject> = [];
|
||||
// What this client has already been told about, in the order it was told.
|
||||
// A Set because the per-snapshot diff below is membership-heavy; iteration
|
||||
// order is insertion order, so the streamed object order is unchanged.
|
||||
private objectsInViewArea = new Set<GameObject>();
|
||||
private lastInputClientTimeMs = 0;
|
||||
private lastInputReceiptMs = 0;
|
||||
private lastLeapClientTimeMs = 0;
|
||||
|
|
@ -190,7 +192,7 @@ export class Player extends PlayerBase {
|
|||
protected createCharacter() {
|
||||
super.createCharacter();
|
||||
|
||||
this.objectsPreviouslyInViewArea.push(this.character!);
|
||||
this.objectsInViewArea.add(this.character!);
|
||||
this.queueCommandSend(new CreatePlayerCommand(this.character!));
|
||||
}
|
||||
|
||||
|
|
@ -205,26 +207,17 @@ export class Player extends PlayerBase {
|
|||
private dyingCharacter?: CharacterPhysical | null;
|
||||
|
||||
public step(deltaTimeInSeconds: number) {
|
||||
if (this.character) {
|
||||
this.center = this.character?.center;
|
||||
this.stepLifecycle(deltaTimeInSeconds);
|
||||
}
|
||||
|
||||
if (!this.character.isAlive) {
|
||||
this.sumDeaths++;
|
||||
this.sumKills = this.character.killCount;
|
||||
protected onCharacterDied(character: CharacterPhysical) {
|
||||
this.dyingCharacter = character;
|
||||
}
|
||||
|
||||
this.dyingCharacter = this.character;
|
||||
this.character = null;
|
||||
this.timeUntilRespawn = settings.playerDiedTimeout;
|
||||
}
|
||||
} else {
|
||||
if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
|
||||
if (this.dyingCharacter) {
|
||||
this.sumKills = Math.max(this.sumKills, this.dyingCharacter.killCount);
|
||||
this.dyingCharacter = null;
|
||||
}
|
||||
this.createCharacter();
|
||||
this.center = this.character!.center;
|
||||
}
|
||||
protected onBeforeRespawn() {
|
||||
if (this.dyingCharacter) {
|
||||
this.sumKills = Math.max(this.sumKills, this.dyingCharacter.killCount);
|
||||
this.dyingCharacter = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -234,26 +227,30 @@ export class Player extends PlayerBase {
|
|||
bb.topLeft = viewArea.topLeft;
|
||||
bb.size = viewArea.size;
|
||||
|
||||
const objectsInViewArea = Array.from(
|
||||
new Set(this.objectContainer.findIntersecting(bb).map((o) => o.gameObject)),
|
||||
const inViewArea = new Set(
|
||||
this.objectContainer.findIntersecting(bb).map((o) => o.gameObject),
|
||||
);
|
||||
|
||||
// The owning character must always be in its own snapshot, regardless of the
|
||||
// view-area query, so the client predictor never loses its authoritative
|
||||
// anchor (the body can ride a fast spinner to the very edge of the box).
|
||||
if (this.character && !objectsInViewArea.includes(this.character)) {
|
||||
objectsInViewArea.push(this.character);
|
||||
if (this.character) {
|
||||
inViewArea.add(this.character);
|
||||
}
|
||||
|
||||
const newlyIntersecting = objectsInViewArea.filter(
|
||||
(o) => !this.objectsPreviouslyInViewArea.includes(o),
|
||||
// Set membership rather than Array.includes: this diff runs per player per
|
||||
// snapshot over everything on their screen, and was quadratic in that.
|
||||
// Set membership rather than Array.includes: this diff runs per player per
|
||||
// snapshot over everything on their screen, and was quadratic in that.
|
||||
const newlyIntersecting = [...inViewArea].filter(
|
||||
(o) => !this.objectsInViewArea.has(o),
|
||||
);
|
||||
|
||||
const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter(
|
||||
(o) => !objectsInViewArea.includes(o),
|
||||
const noLongerIntersecting = [...this.objectsInViewArea].filter(
|
||||
(o) => !inViewArea.has(o),
|
||||
);
|
||||
|
||||
this.objectsPreviouslyInViewArea = objectsInViewArea;
|
||||
this.objectsInViewArea = inViewArea;
|
||||
|
||||
if (noLongerIntersecting.length > 0) {
|
||||
this.queueCommandSend(
|
||||
|
|
@ -267,13 +264,15 @@ export class Player extends PlayerBase {
|
|||
|
||||
this.queueCommandSend(new UpdateMinimap(this.getMinimapPlayers()));
|
||||
|
||||
const propertyUpdates: Array<PropertyUpdatesForObject> = [];
|
||||
for (const object of this.objectsInViewArea) {
|
||||
const update = object.getPropertyUpdatesForFrame();
|
||||
if (update) {
|
||||
propertyUpdates.push(update);
|
||||
}
|
||||
}
|
||||
this.queueCommandSend(
|
||||
new PropertyUpdatesForObjects(
|
||||
this.objectsPreviouslyInViewArea
|
||||
.map((o) => o.getPropertyUpdatesForFrame())
|
||||
.filter((u) => u) as Array<PropertyUpdatesForObject>,
|
||||
performance.now() / 1000,
|
||||
),
|
||||
new PropertyUpdatesForObjects(propertyUpdates, performance.now() / 1000),
|
||||
);
|
||||
|
||||
// Tell the client how much of its own input is reflected in the snapshot it
|
||||
|
|
@ -310,11 +309,17 @@ export class Player extends PlayerBase {
|
|||
}
|
||||
|
||||
public stepCommunications(deltaTime: number) {
|
||||
const remoteCalls = this.objectsPreviouslyInViewArea
|
||||
.map((g) => new RemoteCallsForObject(g.id, g.getRemoteCalls()))
|
||||
.filter((c) => c.calls.length > 0);
|
||||
// Runs at the physics rate, and on most ticks nothing has fired — so test
|
||||
// first and only build the command when there is something to send.
|
||||
let remoteCalls: Array<RemoteCallsForObject> | undefined;
|
||||
for (const object of this.objectsInViewArea) {
|
||||
const calls = object.getRemoteCalls();
|
||||
if (calls.length > 0) {
|
||||
(remoteCalls ??= []).push(new RemoteCallsForObject(object.id, calls));
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteCalls.length > 0) {
|
||||
if (remoteCalls) {
|
||||
this.queueCommandSend(new RemoteCallsForObjects(remoteCalls));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue