Das Ein-Feature Framework

2026-02-22
 
Für die Projekte der "Ein-Feature"-Projektreihe werde ich mein eigenes Framework verwenden. 
Im folgenden werde ich den anfänglichen Aufbau einmal ohne direktes Projekt erläutern. 
Tools in Verwendung 
Github 
Den Sourcecode der Projekte synchronisiere ich über git.
Als Online-Repository verwende ich dafür meinen Github-Account.
Git selbst bediene ich oftmals über Sourcetree (eine UI für git), aber bei Zeiten arbeite ich auch direkt über die Console.
Für Anfänger ist meine Empfehlung allerdings die UI zu verwenden, 
die eine fehlerhafte Bedienung erschwert. 
Yarn 
Als Package manager verwende ich Yarn.
Yarn ist ein Package manager der auf npm basiert, 
die Begründung ist diesmal abseits von ein paar Caching-Optimierungen lediglich: "Gewohnheit". 
Typescript 
(wird vom Bundler installiert) 
Über alle Projekte hinweg verwende ich Typescript.
Ich bevorzuge statisch typisierte Sprachen gegenüber dynamisch typisierten Sprachen und da ich die Projekte auf meiner Website teilen möchte, scheint mir Typescript welches zu Javascript transpiliert angebracht. Als alternative hätte mir auch ELM gefallen, aber durch zu wenig Erfahrung, hätte ich mich vermutlich mehr auf das Lernen der Sprache konzentrieren müssen, als auf das entwickeln von kleinen Projekten. 
Parcel 
(wird vom Bundler installiert) 
Um den auf mehrere Dateien aufgeteilte Java/Type-Script auch richtig im Browser verwenden zu können müssen diese noch in eine Datei gebundelt werden. Dafür verwende ich Parcel. Es ist im vergleich zu den alternativen die ich kenne sehr komfortable zu bedienen. Abseits von einigen Problemen, die einen Neustart erfordern, funktioniert es meistens ohne weiteres zutun. Um die Projekte auf meiner Webseite ausspielen zu können muss ich dem Bundler nur sagen, dass die Assets in relativen Pfaden aufgelöst werden sollen. 
Install und Build-Prozess 
Zuerst muss das passende Repository geklont werden, wenn es bereits eines gibt auf das Projekt aufbaut.
Ich werde Projekte, die zur "Ein-Feature"-Reihe gehören, mit "ef--" prefixen, um sie erkennbar zu machen. 
Das geht entweder über die UI oder mittels: 
git clone <<Repository-Url>> <<Projekt-Pfad>>
 
Anschließend werden mit
 
yarn install
 
die benötigten Projekt-Abhängigkeiten installiert.
Dazu gehören beispielsweise die "ts-game-toolbox", 
die ich mit jedem fortlaufenden Projekt, um einige Klassen erweitern werde.
 
Für die Entwicklung reicht dann der Befehl
 
yarn run start
 
Damit sollte bereits ein lokaler server laufen, der das Projekt unter "localhost:1234" im Browser abspielt. 
Änderungen am Code lösen für gewöhnlich eine neue Kompilierung aus,
sodass keine weiteren Befehle nötig sind.
Nur wenn man neue externe Ressourcen (z.B. Bilder, Musik) in sein Projekt lädt, muss man hin und wieder diesen Befehl mit "Ctrl + C" beenden und neu starten.
 
Nebenbei lasse ich gerne in einem anderem Konsolenfenster Typescript den Code statisch überprüfen.
 
yarn run tsc
 
Dieser Befehl nimmt ebenfalls jede Code-Änderung wahr und gibt sofort Typenfehler aus, wenn welche existieren.
 
Schlussendlich gibt es noch einen Befehl, der das Projekt für den Upload bereit stellt.
 
yarn run release
 
Danach befindet sich das gesamte Projekt lauffähig im "/dist"-Ordner.
Wenn dieser Ordner dann über einen Server erreichbar ist, sollte das Program genau so ablaufen, wie es das in der lokalen Umgebung tut.
 
Framework
 
Das Framework basiert auf dem MVC Prinzip, in dem der Code in
 

aufgeteilt ist.
 
Model/View/Controller-Collection
 
Jede dieser Gruppen ist in unterschiedliche Ordner eingeteilt.
"game/models", "game/views", "game/controller".
 
Dort befinden sich dann die jeweiligen Klassen für verschiedenen Models, Views oder Controller.
In allen Fällen sind die Klassen jedoch in Collections instanziert.
 
export interface ModelCollection extends ModelCollectionBase {
    game: GameModel,
    stars: ModelTable<ModelCollection, StarModel>,
}
 
Es hat sich als vorteilhaft herausgestellt, diese Collection in einer Funktion direkt unter ihrer Definition zu befüllen. 
Jedes Model/View/Controller hat zugriff auf seine eigene Collection (in der sich eine Instanz der selben Klasse befindet), hier müssen wir Typescript etwas austricksen, um das zu erreichen.
 
export function create_models(): ModelCollection {
    const collection: ModelCollection = {} as ModelCollection;
    return Object.assign(collection, {
        game: new GameModel(collection),
        stars: new ModelTable(collection, StarModel),
    });
}
 
Models
 
Models sind der flexibelste Part des Frameworks.
Ein Model kann beliebige Properties und Funktionen besitzen.
Alle Properties sollten allerdings public sein.
 
export class StarModel extends Model<ModelCollection> {
    public position: Vector2 = new Vector2(0, 0);
    public z: number = 0;
    public color: RgbColor = new RgbColor(255, 255, 255);
    public size: number = 0;

    public update(delta_seconds: number) {
        ...
    }
}
 
Controller und andere Models sollen vollen Zugriff auf das Innenleben der Models haben für bestmögliche Flexibilität.
 
export class GameModel extends Model<ModelCollection> {
    public update(delta_seconds: number) {
        this.models.stars.for_each((star) => {
            star.z += delta_seconds;
            if (star.z > 100) star.z = 0;
        });
    }
}
 
Wichtig ist, nie weitere Argumente im constructor eines Models zu erwarten.
Ein Model sollte immer simple konstruiert werden können.
 
ModelTable
 
Weiter oben, bei den ModelCollections habe ich diese Klasse bereits verwendet. Sie dient als Helfer für, wenn ein Model mehr als einmal instanziert werden soll. Sie ist dem Umgang mit einer sql-tabelle nachempfunden. So können Beispielsweise schnell alle mit einem bestimmten Wert als Liste erhalten werden.
 
const small_stars = this.models.stars.where('size', 1);
 
Dieses Konzept hat noch viel Potential nach oben, bisher ist es nur wenig ausgereift.
 
ModelAdapter
 
Um eine Überzahl an Funktionen in der Model Klasse zu verhindern, gibt es noch ModelAdapters. Zuerst wird dafür ein Interface erstellt, für welche Model dieser Adapter funktioniert.
 
export interface PhysicsAdaptable {
    position: Vector2;
    z: number;
    size: number;
}
 
Im Zweifel bietet es sich an den Adapter selbst zu einem Singleton zu machen, um die Performance zu optimieren. Jedenfalls benötigt der Adapter Zugriff auf das Model, um mit diesem zu arbeiten.
Die Funktionen in dem Adapter sollten nun thematisch zusammenhängende Funktionalitäten anbieten. 
Dadurch können diese Funktionen über mehrere Models gemeinsam verwendet werden. Ähnlich wie ein php "Trait".
 
export class PhysicsModelAdapter {
    protected constructor(public target: PhysicsAdaptable) {
    }

    public update(delta_seconds: number) {
        this.target.z += delta_seconds * this.target.size;
        if (this.target.z > 100) this.target.z = 0;
    }

    /**
     * Static Functions
     */
    private static instance?: PhysicsModelAdapter;

    public static for(target: PhysicsAdaptable): PhysicsModelAdapter {
        if (!PhysicsModelAdapter.instance) {
            PhysicsModelAdapter.instance = new PhysicsModelAdapter(target);
        } else {
            PhysicsModelAdapter.instance.target = target;
        }
        return PhysicsModelAdapter.instance;
    }
}
 
Später können wir folgendermaßen den Adapter verwenden.
Zugegebenermaßen im Beispiel noch nicht sehr Hilfreich, 
wenn allerdings die physikalische Berechnung komplizierter wird 
und weitere Helfermethoden dafür nötig sind, die sonst die Model Klasse füllen würden.
 
this.models.stars.for_each((star) => {
    PhysicsModelAdapter.for(star).update(delta_seconds);
});
 
Views
 
Für mich ist ein wichtiger Teil der Views, dass ihr input losgelöst ist von den existierenden Models. Das heißt alle properties sind entweder primitives, oder entsprechende structs sind in derselben Date wie die View definiert.
 
interface ViewStarAttr {
    position: Vector2I;
    z: number;
    size: number;
    color: RgbColor;
}
 
Das Interface kann auch dem Model sehr ähneln, um direkt das Model übergeben zu können, ohne es zu manipulieren.
 
Alle Properties der View sind zudem Chainable, um eine angenehmere Zuweisung der entsprechenden Daten zu ermöglichen.
 
this.views.main.stars.set(this.models.stars.all());
 
Für die Views suche ich noch ein besseres "Partials"-System.
Ich möchte Views wiederverwenden können, ohne die Werte für die partiellen Views explizit definieren zu müssen.
 
export class MainView extends CanvasView<ViewCollection> {
    public bg_color = new ChainProperty<this, RgbColor>(this, tools.commons.Colors.BLACK);
    public fg_color = new ChainProperty<this, RgbColor>(this, tools.commons.Colors.WHITE);
    public stars = new ChainProperty<this, Array<ViewStarAttr>>(this, []);

    public draw(): void {
        this.reset_canvas_state();
        this.stars.get().forEach((star) => {
            const real_position = new Vector2(star.position).mul(33 / (star.z )).add({x: 400, y: 300});
            const size = star.size * 100 / star.z;
            this.context.strokeStyle = star.color.to_hex();
            this.context.fillStyle = star.color.to_hex();
            this.context.fillRect(real_position.x - size / 2, real_position.y - size / 2, size, size);
        });
    }

    protected reset_canvas_state() {
        super.reset_canvas_state();
        this.context.fillStyle = this.bg_color.get().to_hex();
        this.context.fillRect(0, 0, 800, 600);
        this.context.imageSmoothingEnabled = false;
    }
}
 
Controller
 
Controller sind der wohl am schwierigsten zu erklärende Part.
Grob gesagt sind sie dafür Zuständig das Zusammenspiel zwischen Models und Views zu regeln.
 
Controller sind die einzigen Elemente die Zugriff auf alle 3 Collection-Typen haben. Damit können sie den Inhalt der Models lesen und ihn den Views zuordnen. Außerdem können sie auf andere Controller zugreifen, um deren Methoden zu verwenden.
 
Die Funktionsnamen der Controller beschreiben oft den Zustand, in dem sich das Program gerade befindet, so gibt es fast immer eine "new_game"-Methode im MainController. In ihr wird im Prinzip die Spiellogik geladen.
 
export class GameController extends BaseController {

    public new_game(): ControllerRouteResponse {
        for (let i = 0; i < 50; ++i) {
            let star = this.models.stars.insert_new();
            const intensity = Math.random() * 100 + 155;
            star.color = new RgbColor(
                Math.max(intensity + 50 * Math.random(), 255),
                Math.max(intensity + 50 * Math.random(), 255),
                Math.max(intensity + 50 * Math.random(), 255),
            );
            star.z = Math.random() * 100;
            star.position.set({
                x: Math.random() * 400 * 2 - 400,
                y: Math.random() * 300 * 2 - 300,
            });
        }
        const response: ControllerRouteResponseType = {
            view: this.views.main,
            controller: this.controllers.for_event.game_controller,
        };
        return response;
    }
}
 
EventController
 
Neben den normalen Controller gibt es noch EventController,
die Methoden bereitstellen für Events wie "key_down", "update", "mouse_down" oder Custom Events.
 
export class GameEventController extends BaseController implements EventControllerInterface {

    public update(delta_seconds: number): ControllerRouteResponse {
        this.models.game.update(delta_seconds);
        this.views.main.stars.set(this.models.stars.all());
        return null;
    }

}
 
Es kann immer nur ein EventController zur Zeit die Events abhandeln.
 
ControllerRouteResponseType
 
Bedeutend ist bei den Controllern auch der Rückgabewert.
Sie kann entweder "null" sein, was bedeutet der Controller hatte nur Auswirkungen auf die interne Logik, oder sie gibt eine "View", "Controller" und/oder "Event" Eigenschaft zurück.

Eine zurückgegebene View bestimmt von nun an die Visualisierung für das Programm. Andere Views können weiterhin neuen Input bekommen,
aber nur diese View wird angezeigt.

Der Controller aus dem Rückgabewert wird der neue EventController,
alle Event werden nun von diesem Controller verarbeitet.
 
Zurückgegebene Events werden von dem aktivem EventController abgefangen und ausgewertet, dieses Feature, ist noch nicht ganz durchdacht.
 
The Code
 
Der oben beschrieben Code, wird sich vermutlich über das kommende Jahre stark verändern und bei Zeiten nicht mehr mit dem hier beschriebenem in seiner neuesten Version übereinstimmen.
Beispiele aus der Beschreibung beziehen sich auf das Windows-Stars-Screensaver Projekt, welches nun offiziell der erste Eintrag in der "Ein-Feature"-Reihe wird.
 

Comments

captcha