mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 10:54:37 +00:00
packing tiles for more efficient transfer
This commit is contained in:
@@ -124,6 +124,8 @@ export class ClientGameRunner {
|
||||
this.renderer.initialize()
|
||||
this.input.initialize()
|
||||
this.worker.start((gu: GameUpdateViewData) => {
|
||||
const size = gu.packedTileUpdates.length * 4 / 1000
|
||||
console.log(`game update size: ${size}kb`)
|
||||
this.gameView.update(gu)
|
||||
this.renderer.tick()
|
||||
})
|
||||
|
||||
@@ -36,6 +36,10 @@ export class TerritoryLayer implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.game.recentlyUpdatedTiles()
|
||||
.forEach(t => this.enqueue(t))
|
||||
|
||||
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
return
|
||||
}
|
||||
|
||||
+29
-6
@@ -5,7 +5,7 @@ import { WinCheckExecution } from "./execution/WinCheckExecution";
|
||||
import { Game, MutableGame, MutableTile, PlayerID, Tile, TileEvent } from "./game/Game";
|
||||
import { createGame } from "./game/GameImpl";
|
||||
import { loadTerrainMap } from "./game/TerrainMapLoader";
|
||||
import { GameUpdateViewData, PlayerViewData } from "./GameView";
|
||||
import { GameUpdateViewData, packTileData, PlayerViewData } from "./GameView";
|
||||
import { GameConfig, Turn } from "./Schemas";
|
||||
|
||||
export async function createGameRunner(gameID: string, gameConfig: GameConfig, callBack: (gu: GameUpdateViewData) => void): Promise<GameRunner> {
|
||||
@@ -19,7 +19,8 @@ export async function createGameRunner(gameID: string, gameConfig: GameConfig, c
|
||||
}
|
||||
|
||||
export class GameRunner {
|
||||
private updatedTiles: MutableTile[]
|
||||
private updatedTiles: Set<MutableTile> = new Set()
|
||||
private borderOnlyUpdated: Set<MutableTile> = new Set()
|
||||
private tickInterval = null
|
||||
private turns: Turn[] = []
|
||||
private currTurn = 0
|
||||
@@ -34,7 +35,14 @@ export class GameRunner {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(TileEvent, (e) => { this.updatedTiles.push(e.tile as MutableTile) })
|
||||
this.eventBus.on(TileEvent, (e) => {
|
||||
this.updatedTiles.add(e.tile as MutableTile)
|
||||
if (e.borderOnlyChange) {
|
||||
this.borderOnlyUpdated.add(e.tile as MutableTile)
|
||||
} else {
|
||||
this.updatedTiles.add(e.tile as MutableTile)
|
||||
}
|
||||
})
|
||||
this.game.addExecution(...this.execManager.spawnBots(this.game.config().numBots()))
|
||||
if (this.game.config().spawnNPCs()) {
|
||||
this.game.addExecution(...this.execManager.fakeHumanExecutions())
|
||||
@@ -55,19 +63,34 @@ export class GameRunner {
|
||||
return
|
||||
}
|
||||
this.isExecuting = true
|
||||
this.updatedTiles = []
|
||||
this.updatedTiles.clear()
|
||||
this.borderOnlyUpdated.clear()
|
||||
|
||||
|
||||
this.game.addExecution(...this.execManager.createExecs(this.turns[this.currTurn]))
|
||||
this.currTurn++
|
||||
this.game.executeNextTick()
|
||||
|
||||
|
||||
|
||||
|
||||
this.updatedTiles.forEach(t => this.borderOnlyUpdated.delete(t))
|
||||
const updatedData = Array.from(this.updatedTiles).map(t => t.toViewData())
|
||||
updatedData.forEach(t => t.borderOnlyChange = false)
|
||||
|
||||
const borderOnlyData = Array.from(this.borderOnlyUpdated).map(t => t.toViewData())
|
||||
borderOnlyData.forEach(t => t.borderOnlyChange = true)
|
||||
|
||||
updatedData.concat(borderOnlyData)
|
||||
|
||||
|
||||
|
||||
this.callBack({
|
||||
tick: this.game.ticks(),
|
||||
units: this.game.units().map(u => u.toViewData()),
|
||||
tileUpdates: this.updatedTiles.map(t => t.toViewData()),
|
||||
packedTileUpdates: updatedData.map(d => packTileData(d)),
|
||||
players: Object.fromEntries(
|
||||
this.game.players().map(p => [p.id(), p.toViewData()])
|
||||
this.game.allPlayers().map(p => [p.id(), p.toViewData()])
|
||||
) as Record<PlayerID, PlayerViewData>
|
||||
})
|
||||
|
||||
|
||||
+69
-15
@@ -15,15 +15,16 @@ export interface ViewData<T> {
|
||||
export interface TileViewData extends ViewData<TileViewData> {
|
||||
x: number
|
||||
y: number
|
||||
owner: PlayerID,
|
||||
smallID: number,
|
||||
hasFallout: boolean
|
||||
hasDefenseBonus: boolean
|
||||
isBorder: boolean
|
||||
borderOnlyChange: boolean
|
||||
}
|
||||
|
||||
export class TileView {
|
||||
|
||||
constructor(private game: GameView, private data: TileViewData, private _terrain: TerrainTile) { }
|
||||
constructor(private game: GameView, public data: TileViewData, private _terrain: TerrainTile) { }
|
||||
|
||||
type(): TerrainType {
|
||||
return this._terrain.type()
|
||||
@@ -32,10 +33,10 @@ export class TileView {
|
||||
if (!this.hasOwner()) {
|
||||
return new TerraNulliusImpl()
|
||||
}
|
||||
return this.game.player(this.data?.owner)
|
||||
return this.game.playerBySmallID(this.data?.smallID)
|
||||
}
|
||||
hasOwner(): boolean {
|
||||
return this.data?.owner != undefined
|
||||
return this.data?.smallID !== undefined && this.data.smallID !== 0;
|
||||
}
|
||||
isBorder(): boolean {
|
||||
return this.data?.isBorder
|
||||
@@ -50,16 +51,15 @@ export class TileView {
|
||||
return this._terrain
|
||||
}
|
||||
|
||||
neighbors(): TileView[] {
|
||||
throw new Error("Method not implemented.");
|
||||
neighbors(): Tile[] {
|
||||
return this._terrain.neighbors().map(t => this.game.tile(t.cell()))
|
||||
}
|
||||
|
||||
|
||||
hasDefenseBonus(): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
return this.data?.hasDefenseBonus ?? false
|
||||
}
|
||||
cost(): number {
|
||||
throw new Error("Method not implemented.");
|
||||
return this._terrain.cost()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface PlayerViewData extends ViewData<PlayerViewData> {
|
||||
name: string,
|
||||
displayName: string,
|
||||
id: PlayerID,
|
||||
smallID: number,
|
||||
type: PlayerType,
|
||||
isAlive: boolean,
|
||||
tilesOwned: number,
|
||||
@@ -117,6 +118,9 @@ export interface PlayerViewData extends ViewData<PlayerViewData> {
|
||||
|
||||
export class PlayerView implements Player {
|
||||
constructor(private game: GameView, private data: PlayerViewData) { }
|
||||
smallID(): number {
|
||||
return this.data.smallID
|
||||
}
|
||||
lastTileChange(): Tick {
|
||||
return 0
|
||||
}
|
||||
@@ -221,7 +225,7 @@ export class PlayerView implements Player {
|
||||
return false
|
||||
}
|
||||
info(): PlayerInfo {
|
||||
return null
|
||||
return new PlayerInfo(this.name(), this.type(), this.clientID(), this.id())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,32 +233,47 @@ export interface GameUpdateViewData extends ViewData<GameUpdateViewData> {
|
||||
tick: number
|
||||
units: UnitViewData[]
|
||||
players: Record<PlayerID, PlayerViewData>
|
||||
tileUpdates: TileViewData[]
|
||||
tileUpdates?: TileViewData[]
|
||||
packedTileUpdates: Uint16Array[]
|
||||
}
|
||||
|
||||
export class GameView {
|
||||
private data: GameUpdateViewData
|
||||
private tiles: TileViewData[][] = []
|
||||
private tiles: TileView[][] = []
|
||||
private smallIDToID = new Map<number, PlayerID>()
|
||||
|
||||
constructor(private _config: Config, private _terrainMap: TerrainMap) {
|
||||
// Initialize the 2D array
|
||||
this.tiles = Array(_terrainMap.width()).fill(null).map(() => Array(_terrainMap.height()).fill(null));
|
||||
|
||||
// Fill the array with new TileView objects
|
||||
for (let x = 0; x < _terrainMap.width(); x++) {
|
||||
for (let y = 0; y < _terrainMap.height(); y++) {
|
||||
this.tiles[x][y] = new TileView(this, null, _terrainMap.terrain(new Cell(x, y)));
|
||||
}
|
||||
}
|
||||
this.data = {
|
||||
tick: 0,
|
||||
units: [],
|
||||
tileUpdates: [],
|
||||
packedTileUpdates: [],
|
||||
players: {}
|
||||
}
|
||||
}
|
||||
|
||||
public update(gu: GameUpdateViewData) {
|
||||
this.data = gu
|
||||
this.data.tileUpdates = this.data.packedTileUpdates.map(tu => unpackTileData(tu))
|
||||
Object.entries(gu.players).forEach(([key, value]) => {
|
||||
this.smallIDToID.set(value.smallID, key);
|
||||
});
|
||||
gu.tileUpdates.forEach(tu => {
|
||||
this.tiles[tu.x][tu.y] = tu
|
||||
this.tiles[tu.x][tu.y].data = tu
|
||||
})
|
||||
}
|
||||
|
||||
recentlyUpdatedTiles(): TileView[] {
|
||||
return this.data.tileUpdates.map(tu => new TileView(this, tu, this._terrainMap.terrain(new Cell(tu.x, tu.y))))
|
||||
return this.data.tileUpdates.filter(d => true).map(tu => new TileView(this, tu, this._terrainMap.terrain(new Cell(tu.x, tu.y))))
|
||||
}
|
||||
|
||||
player(id: PlayerID): Player {
|
||||
@@ -263,6 +282,14 @@ export class GameView {
|
||||
}
|
||||
throw Error(`player id ${id} not found`)
|
||||
}
|
||||
|
||||
playerBySmallID(id: number): Player {
|
||||
if (!this.smallIDToID.has(id)) {
|
||||
throw new Error(`small id ${id} not found`)
|
||||
}
|
||||
return this.player(this.smallIDToID.get(id))
|
||||
}
|
||||
|
||||
playerByClientID(id: ClientID): Player | null {
|
||||
return null
|
||||
}
|
||||
@@ -273,7 +300,7 @@ export class GameView {
|
||||
return []
|
||||
}
|
||||
tile(cell: Cell): Tile {
|
||||
return new TileView(this, this.tiles[cell.x][cell.y], this._terrainMap.terrain(cell))
|
||||
return this.tiles[cell.x][cell.y]
|
||||
}
|
||||
isOnMap(cell: Cell): boolean {
|
||||
return this._terrainMap.isOnMap(cell)
|
||||
@@ -312,4 +339,31 @@ export class GameView {
|
||||
terrainMap(): TerrainMap {
|
||||
return this._terrainMap
|
||||
}
|
||||
}
|
||||
|
||||
export function packTileData(tile: TileViewData): Uint16Array {
|
||||
const packed = new Uint16Array(4);
|
||||
packed[0] = tile.x;
|
||||
packed[1] = tile.y;
|
||||
packed[2] = tile.smallID;
|
||||
|
||||
// Pack booleans into bits
|
||||
packed[3] = (tile.hasFallout ? 1 : 0) |
|
||||
(tile.hasDefenseBonus ? 2 : 0) |
|
||||
(tile.isBorder ? 4 : 0) |
|
||||
(tile.borderOnlyChange ? 8 : 0)
|
||||
|
||||
return packed;
|
||||
}
|
||||
|
||||
export function unpackTileData(packed: Uint16Array): TileViewData {
|
||||
return {
|
||||
x: packed[0],
|
||||
y: packed[1],
|
||||
smallID: packed[2],
|
||||
hasFallout: !!(packed[3] & 1),
|
||||
hasDefenseBonus: !!(packed[3] & 2),
|
||||
isBorder: !!(packed[3] & 4),
|
||||
borderOnlyChange: !!(packed[4] & 8)
|
||||
};
|
||||
}
|
||||
@@ -227,6 +227,7 @@ export interface TerraNullius {
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
smallID(): number
|
||||
info(): PlayerInfo
|
||||
name(): string
|
||||
displayName(): string
|
||||
@@ -347,6 +348,7 @@ export interface MutableGame extends Game {
|
||||
player(id: PlayerID): MutablePlayer
|
||||
playerByClientID(id: ClientID): MutablePlayer | null
|
||||
players(): MutablePlayer[]
|
||||
allPlayers(): MutablePlayer[]
|
||||
addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer
|
||||
executions(): Execution[]
|
||||
units(...types: UnitType[]): MutableUnit[]
|
||||
@@ -356,7 +358,7 @@ export interface MutableGame extends Game {
|
||||
}
|
||||
|
||||
export class TileEvent implements GameEvent {
|
||||
constructor(public readonly tile: Tile) { }
|
||||
constructor(public readonly tile: Tile, public readonly borderOnlyChange: boolean = false) { }
|
||||
}
|
||||
|
||||
export class PlayerEvent implements GameEvent {
|
||||
|
||||
@@ -40,6 +40,8 @@ export class GameImpl implements MutableGame {
|
||||
allianceRequests: AllianceRequestImpl[] = []
|
||||
alliances_: AllianceImpl[] = []
|
||||
|
||||
private nextID = 1
|
||||
|
||||
|
||||
constructor(
|
||||
private _terrainMap: TerrainMapImpl,
|
||||
@@ -211,6 +213,10 @@ export class GameImpl implements MutableGame {
|
||||
return Array.from(this._players.values()).filter(p => p.isAlive())
|
||||
}
|
||||
|
||||
allPlayers(): MutablePlayer[] {
|
||||
return Array.from(this._players.values())
|
||||
}
|
||||
|
||||
executions(): Execution[] {
|
||||
return [...this.execs, ...this.unInitExecs]
|
||||
}
|
||||
@@ -245,7 +251,8 @@ export class GameImpl implements MutableGame {
|
||||
}
|
||||
|
||||
addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer {
|
||||
let player = new PlayerImpl(this, playerInfo, manpower)
|
||||
let player = new PlayerImpl(this, this.nextID, playerInfo, manpower)
|
||||
this.nextID++
|
||||
this._players.set(playerInfo.id, player)
|
||||
this.eventBus.emit(new PlayerEvent(player))
|
||||
return player
|
||||
@@ -365,6 +372,7 @@ export class GameImpl implements MutableGame {
|
||||
tile.neighbors().forEach(t => tiles.push(t as TileImpl))
|
||||
|
||||
for (const t of tiles) {
|
||||
this.eventBus.emit(new TileEvent(t, true))
|
||||
if (!t.hasOwner()) {
|
||||
t._isBorder = false
|
||||
continue
|
||||
|
||||
@@ -46,7 +46,8 @@ export class PlayerImpl implements MutablePlayer {
|
||||
|
||||
private relations = new Map<Player, number>()
|
||||
|
||||
constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, startPopulation: number) {
|
||||
|
||||
constructor(private gs: GameImpl, private _smallID: number, private readonly playerInfo: PlayerInfo, startPopulation: number) {
|
||||
this._name = playerInfo.name;
|
||||
this._targetTroopRatio = 1
|
||||
this._troops = startPopulation * this._targetTroopRatio;
|
||||
@@ -61,6 +62,7 @@ export class PlayerImpl implements MutablePlayer {
|
||||
name: this.name(),
|
||||
displayName: this.displayName(),
|
||||
id: this.id(),
|
||||
smallID: this.smallID(),
|
||||
type: this.type(),
|
||||
isAlive: this.isAlive(),
|
||||
tilesOwned: this.numTilesOwned(),
|
||||
@@ -73,6 +75,10 @@ export class PlayerImpl implements MutablePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
smallID(): number {
|
||||
return this._smallID
|
||||
}
|
||||
|
||||
name(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
@@ -27,10 +27,11 @@ export class TileImpl implements MutableTile {
|
||||
return {
|
||||
x: this._cell.x,
|
||||
y: this._cell.y,
|
||||
owner: this._owner?.id(),
|
||||
smallID: this._owner.isPlayer() ? this._owner.smallID() : 0,
|
||||
hasFallout: this._hasFallout,
|
||||
hasDefenseBonus: this.hasDefenseBonus(),
|
||||
isBorder: this.isBorder()
|
||||
isBorder: this.isBorder(),
|
||||
borderOnlyChange: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user