Symulator czasu rzeczywistego
symulator jest właściwie automatem skończonym , a dokładniej automatem Moor'a
Definicja Automatu Moor'a
Automat Moore’a jest to rodzaj deterministycznego automatu skończonego, reprezentowany przez uporządkowaną szóstkę:
⟨ Z , Q , Y , Φ , Ψ , q 0 ⟩
gdzie:
- Z = { z 1 , z 2 , … , z n } – zbiór sygnałów wejściowych,
- Q = { q 1 , q 2 , … , q n } – zbiór stanów wewnętrznych,
- Y = { y 1 , y 2 , … , y n } – zbiór sygnałów wyjściowych,
- Φ – funkcja przejść, q(t+1) = Φ(q(t), z(t)),
- Ψ – funkcja wyjść, y ( t ) = Ψ(q(t)) zależy tylko od stanu w którym znajduje się automat,
- q0 – stan początkowy, należy do zbioru Q.
Bardziej czytelnie można by przedstawić to w kodzie jako:
interface Stan {...} //zbiór stanów wewnętrznych
interface Wejscie {...} //zbiór sygnałów wejściowych
interface Wyjscie {...} //zbiór sygnałów wyjściowych
const automatMoora = (
aktualnyStan: Stan,
funkcjaPrzejscia: (aktualnyStan: Stan, sygnałWejsciowy: Wejscie) => Stan,
funkcjaWyjscia: (aktualnyStan: Stan) => Wyjscie,
getWejscie: () => Wejscie,
setWyjscie: (wyjscie: Wyjscie) => void,
)=>{
//uzyskuje wejscie z zewnątrz np. stan myszy
const sygnalWejsciowy = getWejscie();
const nowyStan = funkcjaPrzejscia(aktualnyStan, sygnalWejsciowy);
const wyjscie = funkcjaWyjscia(nowyStan);
setWyjscie(wyjscie); //np. render
autmatMoora(nowyStan, funkcjaPrzejscia, funkcjaWyjscia, getWejscie, setWyjscie);
}
Implementacja w tym symulatorze
WorldElement
symulator jest zbudowany z obiektów które implementują interfejs WorldElement
export interface WorldElement {
update(): void;
destroy(): void;
}
metoda update
jest wywoływana w każdej iteracji (funkcji przejścia). Definiuje w jaki sposób aktualizować dany element świata. Np. dla klasy FrictionInteraction
:
update(): void {
// sila tarcia zalezy od predkosci wzgledem obiektów i jest stała
let velocityDeferace = this.dynamicElement2.velocity.clone().sub(this.dynamicElement1.velocity);
let force: Vector2 = velocityDeferace.clone().normalize().multiplyScalar(this.frictionRate);
let negativeForce: Vector2 = force.clone().multiplyScalar(-1);
this.dynamicElement1.force.add(force);
this.dynamicElement2.force.add(negativeForce);
}
Dla każdej klasy implementującej WorlElement
istnieje 'kontener' tych obiektów, który jest globalnym obiektem klasy WorldElements
. ustaliłem że nazwa kontenera będzie jak nazwa klasy z literką 's' na końcu np. dla DynamicElement
kontener to DynamicElements
. Każda klasa z int. WorldElement
w konstruktorze dodaje się do swojego kontenera:
export class FrictionInteraction implements WorldElement {
dynamicElement1: DynamicElement;
dynamicElement2: DynamicElement;
frictionRate: number;
constructor(dynamicElement1: DynamicElement, dynamicElement2: DynamicElement, frictionRate: number) {
this.dynamicElement1 = dynamicElement1;
this.dynamicElement2 = dynamicElement2;
this.frictionRate = frictionRate;
//tutaj dodaje sie do globalnego kontenera
frictionInteractions.addElement(this);
}
...
}
Dzieki temu że kontener jest globalny mogę w wygodny sposób tworzyć nowe obiekty, które są zagnieżdżone w innych i nie potrzebuje przekazywać przez nie wszystkie referencji do kontenera. W innych językach programowania taki kontener mógłby być atrybutem statycznym, ale w TS niema takiej możliwości.
Tak wygląda klasa WorldElements
:
export class WorldElements {
protected elements: WorldElement[] = [];
update() {
this.elements.forEach((element) => {
element.update();
})
}
removeElement(element: WorldElement) {
this.elements = this.elements.filter((e) => e != element);
}
addElement(element: WorldElement) {
this.elements.push(element);
}
clear() {
this.elements = [];
}
}
Wywołanie metody update
na obiekcie WorldElements
powoduje wywołanie metody update
na każdym obiekcie w kontenerze.
Funkcja przejścia
Końcowa funkcja przejścia (transitionFunction
) wywołuje metodę update na każdym kontenerze:
private transitionFunction() {
const realWorldDt = 10;
const dt = realWorldDt * timeSpeed.value;
let SimulationMaximumDT = springInteractions.getSimulationMaximumDT();
SimulationMaximumDT = 0.3;
const iterations = Math.floor(dt / SimulationMaximumDT);
for (let i = 0; i < iterations; i++) {
userInteractors.update();
// this.collisionSystemDuration += mesureTime(() => collisionSystem.update(), 1);
// this.dynamicCollidingPolygonsDuration += mesureTime(() => dynamicCollidingPolygons.update(), 1)
// this.dynamicCollidingTrianglesDuration += mesureTime(() => dynamicCollindingTriangles.update(), 1);
// wraper mesureTime służy do pomiaru wydajności
this.springInteractionsDuration += mesureTime(() => springInteractions.update(), 1);
this.frictionInteractionsDuration += mesureTime(() => frictionInteractions.update(), 1);
this.dynamicElementsDuration += mesureTime(() => dynamicElements.update(SimulationMaximumDT), 1);
this.fluidInteractorsDuration += mesureTime(() => fluidInteractors.update(), 1);
this.trianglesDuration += mesureTime(() => triangles.update(), 1);
pointers.update();
}
}
ta funkcja jest przesłana do interwału aby była wywołana co 10[ms] :
this.intervals.push(setInterval(() => this.transitionFunction(), 10));
Stan
Stan początkowy automatu/symulatora jest reprezentowany przez klasę World
:
export class World {
constructor() {
pointer.pointer = new Pointer();
const windDynamicElement = new DynamicElement(new Position(), 9999999999);
windDynamicElement.velocity = wind.velocity;
const clouds = new ViewTexture(new PositionRotation(windDynamicElement.position), 'clouds.png', { height: 1000000, width: 1000000 }, 100, { x: 500, y: 500 });
const viewOcean = new ViewTexture(new PositionRotation(), 'water.jpg', { height: 1000000, width: 1000000 }, -10, { x: 500, y: 500 });
const ship = new Ship2();
const DynameicElementOcean = new DynamicElement(new Position(), 9999999999);
const friction = new FrictionInteraction(ship.hull.dynamicCollidingPolygon.centerDynamicElement, DynameicElementOcean, 0.01)
...
}
}
kolejne stany są przechowywane w globalnych kontenerach (obiektach klasy WorldElements
)