Try to keep XY locations integral

This commit is contained in:
Tangent Wantwight 2020-12-31 14:55:36 -05:00
parent 1ef5565be1
commit 76cde242bf
7 changed files with 70 additions and 49 deletions

View file

@ -2,11 +2,23 @@
import { Layer, SpriteSheet } from "../applet/Render"; import { Layer, SpriteSheet } from "../applet/Render";
import { Component, copyDense, copySparse, EntityState, StateForSchema, Store } from "./Data"; import { Component, copyDense, copySparse, EntityState, StateForSchema, Store } from "./Data";
/**
* Nominal type used to remind users to enforce some level
* of quantization on numbers kept in game state. This reduces
* physics precision in theory, but lets us feel more comfortable
* about determinism for rollback physics.
*/
export type FixedPoint = number & {_type: "fixed point"};
export function Floor(x: number): FixedPoint {
return Math.floor(x) as FixedPoint;
}
export class Box { export class Box {
constructor( constructor(
public x: number, public y: number, public x: FixedPoint, public y: FixedPoint,
public w: number, public h: number public w: FixedPoint, public h: FixedPoint
) {}; ) { };
}; };
/** /**
@ -14,7 +26,7 @@ export class Box {
*/ */
export function Approach(source: number, target: number, speed: number): number { export function Approach(source: number, target: number, speed: number): number {
const delta = target - source; const delta = target - source;
if(Math.abs(delta) <= speed) { if (Math.abs(delta) <= speed) {
return target; return target;
} else { } else {
return source + Math.sign(delta) * speed; return source + Math.sign(delta) * speed;
@ -25,10 +37,10 @@ export function Approach(source: number, target: number, speed: number): number
* pairs of vertex coordinates in ccw winding order * pairs of vertex coordinates in ccw winding order
*/ */
export interface Polygon { export interface Polygon {
points: number[]; points: FixedPoint[];
} }
export class PolygonComponent extends Component<PolygonComponent> { export class PolygonComponent extends Component<PolygonComponent> {
points: number[]; points: FixedPoint[];
constructor(from: Partial<PolygonComponent>) { constructor(from: Partial<PolygonComponent>) {
super(from); super(from);
this.points = from.points?.slice() ?? [] this.points = from.points?.slice() ?? []
@ -39,16 +51,16 @@ export class PolygonComponent extends Component<PolygonComponent> {
} }
export class Location extends Component<Location> { export class Location extends Component<Location> {
X: number; X: FixedPoint;
Y: number; Y: FixedPoint;
Angle: number; Angle: number;
VX: number; VX: number;
VY: number; VY: number;
VAngle: number; VAngle: number;
constructor(from: Partial<Location>) { constructor(from: Partial<Location>) {
super(from); super(from);
this.X = from.X ?? 0; this.X = from.X ?? (0 as FixedPoint);
this.Y = from.Y ?? 0; this.Y = from.Y ?? (0 as FixedPoint);
this.Angle = from.Angle ?? 0; this.Angle = from.Angle ?? 0;
this.VX = from.VX ?? 0; this.VX = from.VX ?? 0;
this.VY = from.VY ?? 0; this.VY = from.VY ?? 0;
@ -90,7 +102,7 @@ export class RenderSprite extends Component<RenderSprite> {
public index: number; public index: number;
public offsetX: number; public offsetX: number;
public offsetY: number; public offsetY: number;
constructor(from: Partial<RenderSprite> & {sheet: SpriteSheet}) { constructor(from: Partial<RenderSprite> & { sheet: SpriteSheet }) {
super(from); super(from);
this.sheet = from.sheet; this.sheet = from.sheet;
this.layer = from.layer ?? 1; this.layer = from.layer ?? 1;

View file

@ -1,4 +1,4 @@
import { Data, Location, Polygon } from "./Components"; import { Data, Floor, Location, Polygon } from "./Components";
import { Join } from "./Data"; import { Join } from "./Data";
export function TransformCx(cx: CanvasRenderingContext2D, location: Location, dt = 0) { export function TransformCx(cx: CanvasRenderingContext2D, location: Location, dt = 0) {
@ -9,20 +9,20 @@ export function TransformCx(cx: CanvasRenderingContext2D, location: Location, dt
export function TfPolygon({points}: Polygon, {X, Y, Angle}: Location): Polygon { export function TfPolygon({points}: Polygon, {X, Y, Angle}: Location): Polygon {
const sin = Math.sin(Angle); const sin = Math.sin(Angle);
const cos = Math.cos(Angle); const cos = Math.cos(Angle);
const result = {points: new Array(points.length)}; const result: Polygon = {points: new Array(points.length)};
for(let i = 0; i < points.length; i += 2) { for(let i = 0; i < points.length; i += 2) {
const x = points[i]; const x = points[i];
const y = points[i+1]; const y = points[i+1];
result.points[i] = x*cos - y*sin + X; result.points[i] = Floor(x*cos - y*sin + X);
result.points[i+1] = x*sin + y*cos + Y; result.points[i+1] = Floor(x*sin + y*cos + Y);
} }
return result; return result;
} }
export function DumbMotion(data: Data, interval: number) { export function DumbMotion(data: Data, interval: number) {
Join(data, "location").forEach(([location]) => { Join(data, "location").forEach(([location]) => {
location.X += location.VX * interval; location.X = Floor(location.X + location.VX * interval);
location.Y += location.VY * interval; location.Y = Floor(location.Y + location.VY * interval);
location.Angle += location.VAngle * interval; location.Angle += location.VAngle * interval;
}); });
} }

View file

@ -7,6 +7,7 @@ import {
CollisionClass, CollisionClass,
ComponentSchema, ComponentSchema,
Data, Data,
FixedPoint,
Location, Location,
PolygonComponent, PolygonComponent,
RenderBounds, RenderBounds,
@ -164,11 +165,11 @@ class LoopTest {
// spinner box // spinner box
Create(this.data, { Create(this.data, {
location: new Location({ location: new Location({
X: 200, X: 200 as FixedPoint,
Y: 200, Y: 200 as FixedPoint,
VAngle: Math.PI VAngle: Math.PI
}), }),
bounds: new PolygonComponent({points: [-50, 50, -60, 250, 60, 250, 50, 50]}), bounds: new PolygonComponent({points: [-50, 50, -60, 250, 60, 250, 50, 50] as FixedPoint[]}),
collisionTargetClass: new CollisionClass({ name: "block"}), collisionTargetClass: new CollisionClass({ name: "block"}),
renderBounds: new RenderBounds({color: "#0a0", layer: 0}), renderBounds: new RenderBounds({color: "#0a0", layer: 0}),
}); });
@ -176,12 +177,12 @@ class LoopTest {
// triangles // triangles
[0, 1, 2, 3, 4, 5].forEach(angle => Create(this.data, { [0, 1, 2, 3, 4, 5].forEach(angle => Create(this.data, {
location: new Location({ location: new Location({
X: 200, X: 200 as FixedPoint,
Y: 200, Y: 200 as FixedPoint,
Angle: angle, Angle: angle,
VAngle: -Math.PI/10 VAngle: -Math.PI/10
}), }),
bounds: new PolygonComponent({points: [70, 0, 55, 40, 85, 40]}), bounds: new PolygonComponent({points: [70, 0, 55, 40, 85, 40] as FixedPoint[]}),
collisionSourceClass: new CollisionClass({ name: "tri"}), collisionSourceClass: new CollisionClass({ name: "tri"}),
renderBounds: new RenderBounds({ renderBounds: new RenderBounds({
color: "#d40", color: "#d40",

View file

@ -1,5 +1,12 @@
import { PlaySfx } from "../applet/Audio"; import { PlaySfx } from "../applet/Audio";
import { Location, Polygon, PolygonComponent, RenderBounds } from "../ecs/Components"; import {
FixedPoint,
Floor,
Location,
Polygon,
PolygonComponent,
RenderBounds,
} from "../ecs/Components";
import { Create, Id, Join, Lookup, Remove } from "../ecs/Data"; import { Create, Id, Join, Lookup, Remove } from "../ecs/Data";
import { Data, Lifetime, Teams, World } from "./GameComponents"; import { Data, Lifetime, Teams, World } from "./GameComponents";
@ -39,20 +46,20 @@ export function CheckLifetime(data: Data, world: World, interval: number) {
export function SmokeDamage(data: Data, world: World) { export function SmokeDamage(data: Data, world: World) {
Join(data, "hp", "location").forEach(([hp, {X, Y}]) => { Join(data, "hp", "location").forEach(([hp, {X, Y}]) => {
// convert dealt damage to particles // convert dealt damage to particles
const puffs = Math.floor(hp.receivedDamage / 3); const puffs = Floor(hp.receivedDamage / 3);
SpawnBlast(data, world, X, Y, 2, "#000", puffs); SpawnBlast(data, world, X, Y, 2 as FixedPoint, "#000", puffs);
hp.receivedDamage = Math.floor(hp.receivedDamage % 3); hp.receivedDamage = Floor(hp.receivedDamage % 3);
}); });
} }
function SpawnBlast(data: Data, world: World, x: number, y: number, size: number, color: string, count: number) { function SpawnBlast(data: Data, world: World, x: FixedPoint, y: FixedPoint, size: FixedPoint, color: string, count: FixedPoint) {
for(let puff = 0; puff < count; puff++) { for(let puff = 0; puff < count; puff++) {
const angle = Math.PI * 2 * puff / count; const angle = Math.PI * 2 * puff / count;
SpawnPuff(data, world, x, y, size, color, angle); SpawnPuff(data, world, x, y, size, color, angle);
} }
} }
function SpawnPuff(data: Data, world: World, x: number, y: number, size: number, color: string, angle: number): Id { function SpawnPuff(data: Data, world: World, x: FixedPoint, y: FixedPoint, size: FixedPoint, color: string, angle: number): Id {
return Create(data, { return Create(data, {
location: new Location({ location: new Location({
X: x, X: x,
@ -65,7 +72,7 @@ function SpawnPuff(data: Data, world: World, x: number, y: number, size: number,
-size, size, -size, size,
size, size, size, size,
size, -size size, -size
]}), ] as FixedPoint[]}),
renderBounds: new RenderBounds({ renderBounds: new RenderBounds({
color, color,
// TODO: work out standard layers // TODO: work out standard layers

View file

@ -1,6 +1,6 @@
import { KeyName } from "../applet/Keyboard"; import { KeyName } from "../applet/Keyboard";
import { DrawSet, Layer } from "../applet/Render"; import { DrawSet, Layer } from "../applet/Render";
import { ComponentSchema, Data as EcsData } from "../ecs/Components"; import { ComponentSchema, Data as EcsData, FixedPoint } from "../ecs/Components";
import { Component, copySparse, Join, StateForSchema, Store } from "../ecs/Data"; import { Component, copySparse, Join, StateForSchema, Store } from "../ecs/Data";
import { DumbMotion } from "../ecs/Location"; import { DumbMotion } from "../ecs/Location";
import { LockstepProcessor, TICK_LENGTH } from "../ecs/Lockstep"; import { LockstepProcessor, TICK_LENGTH } from "../ecs/Lockstep";
@ -14,8 +14,8 @@ export enum GamePhase {
} }
export type RGB = [number, number, number]; export type RGB = [number, number, number];
export class World { export class World {
width = 500; width = 500 as FixedPoint;
height = 400; height = 400 as FixedPoint;
/* /*
* Core Game Status * Core Game Status
@ -147,14 +147,14 @@ export class Boss extends Component<Boss> {
} }
export class Message extends Component<Message> { export class Message extends Component<Message> {
targetY = 0; targetY = 0 as FixedPoint;
layer: number; layer: number;
color: string; color: string;
message: string; message: string;
timeout = 3; timeout = 3;
constructor(from: Partial<Message>) { constructor(from: Partial<Message>) {
super(from); super(from);
this.targetY = from.targetY ?? 0; this.targetY = from.targetY ?? (0 as FixedPoint);
this.layer = from.layer ?? 1; this.layer = from.layer ?? 1;
this.color = from.color ?? "#000"; this.color = from.color ?? "#000";
this.message = from.message ?? ""; this.message = from.message ?? "";
@ -219,7 +219,7 @@ export class Engine implements LockstepProcessor<KeyName[], KeyName[][], Data> {
if (playerInput.indexOf("right") != -1) { if (playerInput.indexOf("right") != -1) {
dir += 1; dir += 1;
} }
location.VAngle = dir * 0.01; location.VAngle = (dir * 0.01) as FixedPoint;
} }
}); });
} }

View file

@ -3,7 +3,7 @@ import subscribe from "callbag-subscribe";
import { KeyControl, KeyName } from "../applet/Keyboard"; import { KeyControl, KeyName } from "../applet/Keyboard";
import { DrawSet } from "../applet/Render"; import { DrawSet } from "../applet/Render";
import { Location, PolygonComponent, RenderBounds } from "../ecs/Components"; import { FixedPoint, Location, PolygonComponent, RenderBounds } from "../ecs/Components";
import { Create } from "../ecs/Data"; import { Create } from "../ecs/Data";
import { RunRenderBounds } from "../ecs/Renderers"; import { RunRenderBounds } from "../ecs/Renderers";
import { LockstepClient, Server } from "../net/LockstepClient"; import { LockstepClient, Server } from "../net/LockstepClient";
@ -46,10 +46,10 @@ export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
playerNumber: 0 playerNumber: 0
}), }),
location: new Location({ location: new Location({
X: 100, X: 100 as FixedPoint,
Y: 200, Y: 200 as FixedPoint,
}), }),
bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40]}), bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40] as FixedPoint[]}),
renderBounds: new RenderBounds({ renderBounds: new RenderBounds({
color: "#a0f", color: "#a0f",
layer: 0 layer: 0
@ -61,10 +61,10 @@ export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
playerNumber: 1 playerNumber: 1
}), }),
location: new Location({ location: new Location({
X: 400, X: 400 as FixedPoint,
Y: 200, Y: 200 as FixedPoint,
}), }),
bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40]}), bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40] as FixedPoint[]}),
renderBounds: new RenderBounds({ renderBounds: new RenderBounds({
color: "#f0a", color: "#f0a",
layer: 0 layer: 0

View file

@ -1,4 +1,5 @@
import { DrawSet } from "../applet/Render"; import { DrawSet } from "../applet/Render";
import { Floor } from "../ecs/Components";
import { Join, Remove } from "../ecs/Data"; import { Join, Remove } from "../ecs/Data";
import { TransformCx } from "../ecs/Location"; import { TransformCx } from "../ecs/Location";
import { Data, GamePhase, World } from "./GameComponents"; import { Data, GamePhase, World } from "./GameComponents";
@ -22,23 +23,23 @@ export function ArrangeMessages(data: Data, world: World, interval: number) {
const messages = Join(data, "message", "location"); const messages = Join(data, "message", "location");
messages.sort(([{timeout: timeoutA}, {}], [{timeout: timeoutB}, {}]) => timeoutA - timeoutB); messages.sort(([{timeout: timeoutA}, {}], [{timeout: timeoutB}, {}]) => timeoutA - timeoutB);
let y = world.height / 3; let y = Floor(world.height / 3);
messages.forEach(([message, location]) => { messages.forEach(([message, location]) => {
message.targetY = y; message.targetY = y;
y += ADVANCE; y = Floor(y + ADVANCE);
const delta = message.targetY - location.Y; const delta = message.targetY - location.Y;
if(Math.abs(delta) < 100 * interval) { if(Math.abs(delta) < 100 * interval) {
location.Y = message.targetY; location.Y = message.targetY;
location.VY = 0; location.VY = Floor(0);
} else { } else {
location.VY = Math.sign(delta) * 100; location.VY = Floor(Math.sign(delta) * 100);
} }
if(location.X >= world.width / 2 && message.timeout >= 0) { if(location.X >= world.width / 2 && message.timeout >= 0) {
location.X = world.width / 2; location.X = Floor(world.width / 2);
message.timeout -= interval; message.timeout -= interval;
location.VX = 0; location.VX = Floor(0);
} else if(world.phase == GamePhase.PLAYING) { } else if(world.phase == GamePhase.PLAYING) {
location.VX = world.width; location.VX = world.width;
} }