add catching up mechanic for ticks

This commit is contained in:
2025-01-19 18:03:20 +09:00
parent 04cc3f951e
commit 072204b902
7 changed files with 51 additions and 27 deletions
+1
View File
@@ -423,6 +423,7 @@ function connectWebSocket() {
const bbox = new THREE.Mesh(new THREE.BoxGeometry(width, height, 0.2), bboxMaterial) const bbox = new THREE.Mesh(new THREE.BoxGeometry(width, height, 0.2), bboxMaterial)
bbox.position.set((bboxValues[3] / 100) + (width / 2), (bboxValues[2] / 100) + (height / 2), 0) bbox.position.set((bboxValues[3] / 100) + (width / 2), (bboxValues[2] / 100) + (height / 2), 0)
bbox.layers.set(1)
scene.add(bbox) scene.add(bbox)
} }
} }
+5 -5
View File
@@ -47,7 +47,7 @@ export default class Ability {
const projectile = new Projectile({ const projectile = new Projectile({
onCollide: straightShotCollision, onCollide: straightShotCollision,
owner: caster, owner: caster.id,
position: caster.position.clone(), position: caster.position.clone(),
radius: ability.radius, radius: ability.radius,
speed: ability.speed, speed: ability.speed,
@@ -82,7 +82,7 @@ export default class Ability {
const projectile = new Projectile({ const projectile = new Projectile({
after: rangedAttackAfter, after: rangedAttackAfter,
homingTarget: target, homingTarget: target,
owner: caster, owner: caster.id,
position: caster.position.clone(), position: caster.position.clone(),
radius: ability.radius, radius: ability.radius,
speed: ability.speed, speed: ability.speed,
@@ -124,7 +124,7 @@ export default class Ability {
const ability = this const ability = this
const shieldThrowReturn = function shieldThrowReturn(projectile, homingTarget) { const shieldThrowReturn = function shieldThrowReturn(projectile, homingTarget) {
const returnProjectile = new Projectile({ const returnProjectile = new Projectile({
owner: caster, owner: caster.id,
position: projectile.position.clone(), position: projectile.position.clone(),
radius: ability.radius, radius: ability.radius,
speed: ability.speed, speed: ability.speed,
@@ -136,7 +136,7 @@ export default class Ability {
const projectile = new Projectile({ const projectile = new Projectile({
after: shieldThrowReturn, after: shieldThrowReturn,
owner: caster, owner: caster.id,
position: caster.position.clone(), position: caster.position.clone(),
radius: ability.radius, radius: ability.radius,
speed: ability.speed, speed: ability.speed,
@@ -191,7 +191,7 @@ export default class Ability {
const projectile = new Projectile({ const projectile = new Projectile({
onCollide: exposeCollision, onCollide: exposeCollision,
owner: caster, owner: caster.id,
position: caster.position.clone(), position: caster.position.clone(),
radius: ability.radius, radius: ability.radius,
speed: ability.speed, speed: ability.speed,
+12 -5
View File
@@ -16,7 +16,8 @@ export default class Entity {
health = null health = null
height = 40 height = 40
maxHealth = 1 maxHealth = 1
memory = {} // TODO: hide from reports but keep public memory = {}
pathfindingCooldown = 0
position = null position = null
radius = 0 radius = 0
rotation = 0 rotation = 0
@@ -31,6 +32,7 @@ export default class Entity {
#logic = null #logic = null
#moving = false #moving = false
#path = [] #path = []
#noPathfindingUntil = 0
#spawnPosition = new Vector2() #spawnPosition = new Vector2()
static collider(x, y, radius) { static collider(x, y, radius) {
@@ -495,6 +497,7 @@ export default class Entity {
#move(distanceTraveled = 0) { #move(distanceTraveled = 0) {
if (this.casting != null) { return false } if (this.casting != null) { return false }
const currentTick = this.game?.currentTick ?? 0
if (this.#attacking) { if (this.#attacking) {
const cursor = this.#dest ?? this.position const cursor = this.#dest ?? this.position
@@ -504,8 +507,7 @@ export default class Entity {
if (target != null && this.distanceTo(target.position) < basicAttack.range + this.radius + target.radius) { if (target != null && this.distanceTo(target.position) < basicAttack.range + this.radius + target.radius) {
const cooldown = this.game?.secToTick(basicAttack.cooldown) ?? 0 const cooldown = this.game?.secToTick(basicAttack.cooldown) ?? 0
const lastCast = this.cooldowns[basicAttack.id] const lastCast = this.cooldowns[basicAttack.id]
const timestamp = this.game?.currentTick ?? 0 if (lastCast != null && lastCast + cooldown > currentTick) { return false }
if (lastCast != null && lastCast + cooldown > timestamp) { return false }
this.castAction('a', target.id, false) this.castAction('a', target.id, false)
return true return true
@@ -516,8 +518,9 @@ export default class Entity {
if (!this.#moving || this.#dest == null) { return false } if (!this.#moving || this.#dest == null) { return false }
const fixedDest = this.fixFuturePosition(this.#dest) const fixedDest = this.fixFuturePosition(this.#dest)
const pathfinding = this.#noPathfindingUntil <= currentTick
if (this.#path.length > 0) { if (pathfinding && this.#path.length > 0) {
const sectionDest = this.#path.at(0) const sectionDest = this.#path.at(0)
const lineOfSight = this.isInLineOfSight(sectionDest) const lineOfSight = this.isInLineOfSight(sectionDest)
if (!lineOfSight) { if (!lineOfSight) {
@@ -532,7 +535,7 @@ export default class Entity {
} }
} }
if ((this.#path.length < 1 || (this.#path.at(-1)?.distanceTo(fixedDest) ?? 0) > 0.01)) { if (pathfinding && (this.#path.length < 1 || (this.#path.at(-1)?.distanceTo(fixedDest) ?? 0) > 0.01)) {
const start = SATX.vectorToFloat32Array(this.position) const start = SATX.vectorToFloat32Array(this.position)
const goal = SATX.vectorToFloat32Array(fixedDest) const goal = SATX.vectorToFloat32Array(fixedDest)
const obstacles = [] const obstacles = []
@@ -574,6 +577,10 @@ export default class Entity {
} }
} }
if (pathfinding && this.pathfindingCooldown > 0) {
this.#noPathfindingUntil = currentTick + (this.game?.secToTick(this.pathfindingCooldown) ?? 0)
}
if (this.#path.length > 0) { if (this.#path.length > 0) {
const speed = (this.speed / (this.game?.tickBudget ?? 1000)) - distanceTraveled const speed = (this.speed / (this.game?.tickBudget ?? 1000)) - distanceTraveled
const destination = this.#path.at(0) const destination = this.#path.at(0)
+6 -4
View File
@@ -18,6 +18,7 @@ export default class Game {
tickRate = 30 tickRate = 30
width = 10000 * 1.6 width = 10000 * 1.6
#behindMs = 0
#currentTiming = 0 #currentTiming = 0
#eventEmitter = new EventEmitter() #eventEmitter = new EventEmitter()
#logic = null #logic = null
@@ -103,9 +104,8 @@ export default class Game {
} }
#gameLoop() { #gameLoop() {
const tickBudget = this.#tickBudget
if (this.#nextTickAt != null) { if (this.#nextTickAt != null) {
const tickBudget = this.#tickBudget
const nextTickAt = this.#nextTickAt const nextTickAt = this.#nextTickAt
this.#nextTickAt = null this.#nextTickAt = null
@@ -114,9 +114,11 @@ export default class Game {
const before = performance.now() const before = performance.now()
this.update() this.update()
this.#nextTickAt = start + tickBudget
const after = performance.now() const after = performance.now()
const taken = (after - before) const taken = (after - before)
const prevBehind = this.#behindMs
this.#behindMs = Math.max(0, this.#behindMs + taken - tickBudget)
this.#nextTickAt = start + tickBudget - prevBehind
this.#timings[this.#currentTiming] = taken this.#timings[this.#currentTiming] = taken
if (this.#currentTiming++ > this.#timings.length) { if (this.#currentTiming++ > this.#timings.length) {
@@ -124,7 +126,7 @@ export default class Game {
} }
if (after - before > tickBudget) { if (after - before > tickBudget) {
console.warn(`Can't keep up! A tick took ${taken.toFixed(1)} ms. Entity count: ${this.entities.length}`) console.warn(`Can't keep up! A tick took ${taken.toFixed(1)} ms / ${tickBudget} ms. (Behind ${this.#behindMs} ms)`)
} }
} }
} }
+2 -2
View File
@@ -126,7 +126,7 @@ function laneScenario() {
].map((p) => p.multiplyScalar(1.6)) ].map((p) => p.multiplyScalar(1.6))
if (game.entities.length < 100) { if (game.entities.length < 100) {
if ([(0 * game.tickRate), (1 * game.tickRate), (2 * game.tickRate)].includes(game.currentTick % (6 * game.tickRate))) { if ([(0 * game.tickRate), (1 * game.tickRate), (2 * game.tickRate)].includes(game.currentTick % (30 * game.tickRate))) {
game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: false, route: topRoute }))) game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: false, route: topRoute })))
game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: false, route: midRoute }))) game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: false, route: midRoute })))
game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: false, route: botRoute }))) game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: false, route: botRoute })))
@@ -136,7 +136,7 @@ function laneScenario() {
game.spawnEntity(new Entity(Template.minion(Team.red, { ranged: false, route: botRoute.toReversed() }))) game.spawnEntity(new Entity(Template.minion(Team.red, { ranged: false, route: botRoute.toReversed() })))
} }
if ([(3 * game.tickRate), (4 * game.tickRate), (5 * game.tickRate)].includes(game.currentTick % (6 * game.tickRate))) { if ([(3 * game.tickRate), (4 * game.tickRate), (5 * game.tickRate)].includes(game.currentTick % (30 * game.tickRate))) {
game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: true, route: topRoute }))) game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: true, route: topRoute })))
game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: true, route: midRoute }))) game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: true, route: midRoute })))
game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: true, route: botRoute }))) game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: true, route: botRoute })))
+24 -11
View File
@@ -5,25 +5,35 @@ import Entity from './entity.js'
export default class Projectile { export default class Projectile {
id = crypto.randomUUID() id = crypto.randomUUID()
after = null // TODO: hide from reports but keep public
bbox = new Float32Array(4) bbox = new Float32Array(4)
height = 50 height = 50
memory = {} // TODO: hide from reports but keep public memory = {}
onCollide = null // TODO: hide from reports but keep public owner = null
owner = null // TODO: only keep an ID
position = new Vector2() position = new Vector2()
radius = 5 radius = 5
speed = 1000 speed = 1000
visualRadius = null visualRadius = null
#after = null
#dest = null #dest = null
#homingTarget = null #homingTarget = null
#logic = null
#onCollide = null
#game = null #game = null
get after() { return this.#after }
get game() { return this.#game } get game() { return this.#game }
set game(value) { this.#game = value } get homingTarget() { return this.#homingTarget }
get logic() { return this.#logic }
get onCollide() { return this.#onCollide }
set after(value) { this.#after = value }
set destination(value) { this.#dest = value } set destination(value) { this.#dest = value }
set game(value) { this.#game = value }
set homingTarget(value) { this.#homingTarget = value } set homingTarget(value) { this.#homingTarget = value }
set logic(value) { this.#logic = value }
set onCollide(value) { this.#onCollide = value }
get destination() { get destination() {
return this.#dest ?? this.#homingTarget?.position return this.#dest ?? this.#homingTarget?.position
@@ -53,6 +63,9 @@ export default class Projectile {
this.#move() this.#move()
this.#checkStationaryCollisions() this.#checkStationaryCollisions()
this.#checkIfArrived() this.#checkIfArrived()
if (this.#logic != null) {
this.#logic()
}
} }
#calculateBbox() { #calculateBbox() {
@@ -66,15 +79,15 @@ export default class Projectile {
if (this.destination == null) { return } if (this.destination == null) { return }
if (!this.position.equals(this.destination)) { return } if (!this.position.equals(this.destination)) { return }
if (this.after != null) { if (this.#after != null) {
this.after(this, this.#homingTarget) this.#after(this, this.#homingTarget)
} }
this.despawn() this.despawn()
} }
#checkStationaryCollisions() { #checkStationaryCollisions() {
if (this.onCollide == null) { return } if (this.#onCollide == null) { return }
const bbox = this.bbox const bbox = this.bbox
const entitiesAndTerrains = this.game?.entities ?? [] const entitiesAndTerrains = this.game?.entities ?? []
@@ -82,7 +95,7 @@ export default class Projectile {
if (bboxCheckedObstacles.length > 0) { if (bboxCheckedObstacles.length > 0) {
const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat() const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat()
const collider = this.collider() const collider = this.collider()
colliders.filter((it) => SATX.collideObject(collider, it)).forEach((it) => this.onCollide(this, it)) colliders.filter((it) => SATX.collideObject(collider, it)).forEach((it) => this.#onCollide(this, it))
} }
} }
@@ -99,14 +112,14 @@ export default class Projectile {
this.position.add(step) this.position.add(step)
} }
if (this.onCollide != null) { if (this.#onCollide != null) {
const bbox = Entity.tunnelBbox(position.x, position.y, destination.x, destination.y, this.radius) const bbox = Entity.tunnelBbox(position.x, position.y, destination.x, destination.y, this.radius)
const entitiesAndTerrains = this.game?.entities ?? [] const entitiesAndTerrains = this.game?.entities ?? []
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => SATX.bboxCheck(bbox, it.bbox)) const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
if (bboxCheckedObstacles.length > 0) { if (bboxCheckedObstacles.length > 0) {
const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat() const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat()
const collider = Entity.tunnelCollider(prevPos.x, prevPos.y, this.position.x, this.position.y, this.radius) const collider = Entity.tunnelCollider(prevPos.x, prevPos.y, this.position.x, this.position.y, this.radius)
colliders.filter((it) => SATX.collideObject(collider, it)).forEach((it) => this.onCollide(this, it)) colliders.filter((it) => SATX.collideObject(collider, it)).forEach((it) => this.#onCollide(this, it))
} }
} }
} }
+1
View File
@@ -8,6 +8,7 @@ export default class Template {
height: options.ranged ? 40 : 38, height: options.ranged ? 40 : 38,
logic: this.#minionLogic(options.route), logic: this.#minionLogic(options.route),
maxHealth: options.ranged ? 300 : 450, maxHealth: options.ranged ? 300 : 450,
pathfindingCooldown: 0.2,
position: options.route?.at(0) ?? new Vector2(0, 0), position: options.route?.at(0) ?? new Vector2(0, 0),
radius: 48, radius: 48,
speed: 325, speed: 325,