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,10 +2,22 @@
import { Layer, SpriteSheet } from "../applet/Render";
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 {
constructor(
public x: number, public y: number,
public w: number, public h: number
public x: FixedPoint, public y: FixedPoint,
public w: FixedPoint, public h: FixedPoint
) { };
};
@ -25,10 +37,10 @@ export function Approach(source: number, target: number, speed: number): number
* pairs of vertex coordinates in ccw winding order
*/
export interface Polygon {
points: number[];
points: FixedPoint[];
}
export class PolygonComponent extends Component<PolygonComponent> {
points: number[];
points: FixedPoint[];
constructor(from: Partial<PolygonComponent>) {
super(from);
this.points = from.points?.slice() ?? []
@ -39,16 +51,16 @@ export class PolygonComponent extends Component<PolygonComponent> {
}
export class Location extends Component<Location> {
X: number;
Y: number;
X: FixedPoint;
Y: FixedPoint;
Angle: number;
VX: number;
VY: number;
VAngle: number;
constructor(from: Partial<Location>) {
super(from);
this.X = from.X ?? 0;
this.Y = from.Y ?? 0;
this.X = from.X ?? (0 as FixedPoint);
this.Y = from.Y ?? (0 as FixedPoint);
this.Angle = from.Angle ?? 0;
this.VX = from.VX ?? 0;
this.VY = from.VY ?? 0;

View file

@ -1,4 +1,4 @@
import { Data, Location, Polygon } from "./Components";
import { Data, Floor, Location, Polygon } from "./Components";
import { Join } from "./Data";
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 {
const sin = Math.sin(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) {
const x = points[i];
const y = points[i+1];
result.points[i] = x*cos - y*sin + X;
result.points[i+1] = x*sin + y*cos + Y;
result.points[i] = Floor(x*cos - y*sin + X);
result.points[i+1] = Floor(x*sin + y*cos + Y);
}
return result;
}
export function DumbMotion(data: Data, interval: number) {
Join(data, "location").forEach(([location]) => {
location.X += location.VX * interval;
location.Y += location.VY * interval;
location.X = Floor(location.X + location.VX * interval);
location.Y = Floor(location.Y + location.VY * interval);
location.Angle += location.VAngle * interval;
});
}

View file

@ -7,6 +7,7 @@ import {
CollisionClass,
ComponentSchema,
Data,
FixedPoint,
Location,
PolygonComponent,
RenderBounds,
@ -164,11 +165,11 @@ class LoopTest {
// spinner box
Create(this.data, {
location: new Location({
X: 200,
Y: 200,
X: 200 as FixedPoint,
Y: 200 as FixedPoint,
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"}),
renderBounds: new RenderBounds({color: "#0a0", layer: 0}),
});
@ -176,12 +177,12 @@ class LoopTest {
// triangles
[0, 1, 2, 3, 4, 5].forEach(angle => Create(this.data, {
location: new Location({
X: 200,
Y: 200,
X: 200 as FixedPoint,
Y: 200 as FixedPoint,
Angle: angle,
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"}),
renderBounds: new RenderBounds({
color: "#d40",

View file

@ -1,5 +1,12 @@
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 { 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) {
Join(data, "hp", "location").forEach(([hp, {X, Y}]) => {
// convert dealt damage to particles
const puffs = Math.floor(hp.receivedDamage / 3);
SpawnBlast(data, world, X, Y, 2, "#000", puffs);
hp.receivedDamage = Math.floor(hp.receivedDamage % 3);
const puffs = Floor(hp.receivedDamage / 3);
SpawnBlast(data, world, X, Y, 2 as FixedPoint, "#000", puffs);
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++) {
const angle = Math.PI * 2 * puff / count;
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, {
location: new Location({
X: x,
@ -65,7 +72,7 @@ function SpawnPuff(data: Data, world: World, x: number, y: number, size: number,
-size, size,
size, size,
size, -size
]}),
] as FixedPoint[]}),
renderBounds: new RenderBounds({
color,
// TODO: work out standard layers

View file

@ -1,6 +1,6 @@
import { KeyName } from "../applet/Keyboard";
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 { DumbMotion } from "../ecs/Location";
import { LockstepProcessor, TICK_LENGTH } from "../ecs/Lockstep";
@ -14,8 +14,8 @@ export enum GamePhase {
}
export type RGB = [number, number, number];
export class World {
width = 500;
height = 400;
width = 500 as FixedPoint;
height = 400 as FixedPoint;
/*
* Core Game Status
@ -147,14 +147,14 @@ export class Boss extends Component<Boss> {
}
export class Message extends Component<Message> {
targetY = 0;
targetY = 0 as FixedPoint;
layer: number;
color: string;
message: string;
timeout = 3;
constructor(from: Partial<Message>) {
super(from);
this.targetY = from.targetY ?? 0;
this.targetY = from.targetY ?? (0 as FixedPoint);
this.layer = from.layer ?? 1;
this.color = from.color ?? "#000";
this.message = from.message ?? "";
@ -219,7 +219,7 @@ export class Engine implements LockstepProcessor<KeyName[], KeyName[][], Data> {
if (playerInput.indexOf("right") != -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 { 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 { RunRenderBounds } from "../ecs/Renderers";
import { LockstepClient, Server } from "../net/LockstepClient";
@ -46,10 +46,10 @@ export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
playerNumber: 0
}),
location: new Location({
X: 100,
Y: 200,
X: 100 as FixedPoint,
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({
color: "#a0f",
layer: 0
@ -61,10 +61,10 @@ export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
playerNumber: 1
}),
location: new Location({
X: 400,
Y: 200,
X: 400 as FixedPoint,
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({
color: "#f0a",
layer: 0

View file

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