Documenting my exploration of GateEngine

I have been aware of GateEngine for a while now. Unfortunately, while the demo repository contains a lot of very well documented source code (lots of code-comments!), there isn't much in the way of how-to, or traditional instruction-based documentation.

I'm starting this thread to discuss my attempt to use GateEngine to make a port of one of my earliest 2D games, Go-Tetris. I've long-since renamed the game ActionGo. You can see a trailer for ActionGo (from the last time I was remaking it in Unity) on YouTube.

I've already ported this code a few times (from its original actionscript 2.0 to first Objective-C, and then C#), so this would be a fairly easy project, if the engine were mature and well-documented.

A rough list of objectives that I'll work from:

  • how to set up the project (how to actually get started)
  • how to draw in code – UI elements & 2D game elements
  • how to implement controller support and fall-back to keyboard input
  • how to create an executable for each platform
    • target platforms: mac desktop, & linux desktop first, then web, Windows, & iOS, not necessarily in that order
    • Ideally, this would use github actions (or some similar CI workflow) to create automated builds
  • game architecture (GateEngine seems pretty architecture agnostic, but we'll see)
  • how to manage your window in support of various screen resolutions, maybe?
  • stretch goals: steamworks integration

Why GateEngine?

In short, I'm interested in cross-platform game development, written in Swift. GateEngine claims to support a ton of platforms, including Mac, and Linux desktop, which are at the top of my list. HTML5 and iOS are close behind.

For some context, I'm primarily a contract iOS developer. My first app was in the app store in ~2009. I definitely loved Objective-C, (at first probably because it wasn't PHP), but once I began using Swift (around the time 4.0 was released) I stopped really wanting to write code in any other language. (Incidentally, you can find my games at AbstractPuzzle.com, and my blog – where I may cross-post this – at Chesstris.com.)

I've always been quite interested in cross-platform game development. (Go-Tetris was originally written in Flash because the web was available everywhere!) I've written games (or at least experimental projects) in dozens of languages and environments trying to find a good cross-platform experience. I even did one project in ~2010 with a friend in C++ using raw openGLES calls. Our plan was to target iOS and Android.

Around 2015 I started seeing hints that VR was going to be a big deal, so I went "all in" on learning Unity, and most of my contract work was even in Unity for a few years there. But I never really loved using Unity. I'm a programmer first and foremost, and the GUI for game development is kind of annoying to me. After I started writing and preferring Swift, I spent quite a bit of time exploring code-first game engines, including Swift bindings and/or wrappers for SDL and Raylib, (both of which have many seemingly abandoned Swift projects on github).

I am also aware of SwiftGodot, but I'm not sure it's mature enough yet, and also, while I love the idea of Godot, it's more of a game development GUI (like Unity), and not the code-first panacea I've been searching for.

I have seen a few other purpose-built Swift game engines on Github (FireBlade was the last one I tried out with any seriousness), but I've not seen any that weren't abandoned after an example project or two. From what I can tell, GateEngine is already much farther along than most of them.

I'll split this up by topic. (Hopefully not every post will be as long as these first few.)

9 Likes

Getting Started

The project's README has a section with this title, but it's merely a list of requirements, and doesn't actually document what you would do to create a new project. It says:

Add the package to your project like any other package and you're done.
.package(url: "https://github.com/STREGAsGate/GateEngine.git", .upToNextMinor(from: "0.1.0"))

As near as I can tell, there are two paths forward from here: create a new Xcode project, or use the Swift CLI to create a new swift package. I'll document the Xcode project first (as it's definitely how I would normally create a new project), but then I'll document the CLI version, as it's what I'll probably end up using.

New Xcode Project

The "New->Project..." dialog box gives you a lot of options. I chose a MacOS app, with a SwiftUI interface, (knowing that wouldn't include any .storyboard files). Then I deleted several files by selecting them in the navigator and pressing delete. Namely, I deleted:

  • the ActionGoUITests/ folder and contents
  • the ActionGo/ContentView.swift SwiftUI file
  • the ActionGo/Preview Content/ folder and its contents

Then I added the GateEngine dependency by navigating to the project and clicking the + button in the "Package Dependencies" tab and pasting https://github.com/STREGAsGate/GateEngine into the search box. While I was there, I deleted the UI Tests target.

Then I replaced the contents of ActionGoApp.swift with the following:

import Foundation
import GateEngine

@main
final class ActionGoApp: GameDelegate {
    func didFinishLaunching(game: GateEngine.Game, options: GateEngine.LaunchOptions) async {
        print("it worked!")
    }
}

I was then able to build and run the project, and happy to see "it worked!" in the log.

New Swift Package

Using the Swift CLI to create a new project is pretty easy, but I do it so seldom that I always have to run swift --help, then swift package --help, and then swift package init --help before I finally feel confident I know what I'm doing. And then I still forget to run it in an empty folder. Anyway, the final commands I used look like this:

$ mkdir ActionGo
$ swift package init --type executable --name ActionGo
$ open Package.swift

After this, then you can open the Package.swift. add the package dependency as specified in the README. It doesn't actually tell you how to do that either, but here's what I did.

The default Package definition in Package.swift (created from the above command) looks like this (after deleting some comments):

let package = Package(
    name: "ActionGo",
    targets: [
        .executableTarget(
            name: "ActionGo"),
    ]
)

I added the following between the name: and targets: lines:

    platforms: [.macOS(.v10_15)],
    dependencies: [
        .package(url: "https://github.com/STREGAsGate/GateEngine.git", .upToNextMajor(from: "0.1.2")),
    ],

I was able to build the project successfully at this point, but after modifying the Sources/main.swift file to look exactly like the ActionGoApp.swift definition above, I got an error at import GateEngine that said No such module 'GateEngine'. I'd forgotten to specify that the ActionGo target uses the GateEngine dependency.

Then I got the error 'main' attribute cannot be used in a module that contains top-level code. Deleting @main got rid of the error, but the project still wouldn't compile, and the following warnings were present at the top of the file:

1. Top-level code defined in this source file
2. Pass '-parse-as-library' to compiler invocation if this is intentional

I re-named main.swift to ActionGoApp.swift, and finally got a window, and "it worked!" in the log.

Worth noting, somewhere in there I also got this nasty looking error while trying to build and run:

Failed to install the app on the device.
Domain: com.apple.dt.CoreDeviceError
<...snipped lots of lines...>
The item at ActionGo is not a valid bundle.
Failure Reason: Failed to read the bundle.

...which was resolved by just picking the "My Mac" run target. (I think it had defaulted to my iPhone or something.)

After all this headache, you might be wondering why I would lean toward this method of creating the project. There's one main reason, and it's that this is clearly how the demo repository projects were created. And I'm going to need to look at those pretty closely when I get into supporting other platforms. TBH, I'm not even sure if I can use the Xcode Project technique to compile for other platforms. (Maybe someone more knowledgeable can chime in with that answer.)

Up next... actually making something render in my new project!

1 Like

Architecture and rendering an image

architecture rambling

Looking through the GateEngine examples, what I want to make is most similar to the Pong example. Without any documentation, it's hard to know what pieces of the architecture I'll need for sure, but it looks like at minimum I'll need a System (to modify my game's state in an update() function), and a RenderingSystem to do the actual... rendering.

As an experienced game developer, I know I want a model object that represents the game's state, and I'll want the update() function to modify that state based on user input, and I'll want the renderer to know about it so it can draw as appropriate. I'll also (eventually) want some menus (screens), and I think I'll want each one to do its own rendering.

I could make this model object a component, but I'm not sure what the benefit is there, tbh. I'm just going to have one of them at any given time, so it seems kind of silly. I'm not a big fan of ECS in general, but I knew this is what I was going to be dealing with going in. I could always resort to some global variables... I haven't decided yet.

As an aside, I do not understand why System and RenderingSystem are both classes. Maybe there is some technical reason, but it seems to me things would be easier if they were protocols, and then I could implement them both in the same class.

So I'm thinking:

  • a System to manage overall application state, including which screen is shown, and probably hold the Game's State struct.
  • a RenderingSystem for menu drawing, (or maybe one for each menu/screen, not sure yet)
  • another RenderingSystem and System combo for the game itself

I thought I was going to maybe be able to do a first-pass mockup of the game by drawing primitives, and without any images, but looking a bit closer at all the examples, the only primitive that GateEngine currently supports is a rectangle. Sprites it is! (Fortunately, I have a bunch of really nice images to use from the last time I tried to remake this project.)

But this means figuring out how to add assets sooner than later. Interestingly, there is no single-image example in the demos either. Only animated sprites.

using images

To add an image, you have to add a resources folder, and then you have to make sure that you add that folder to your Package.swift's executable target definition:

        .executableTarget(
            name: "ActionGo",
            dependencies: ["GateEngine"],
            resources: [.copy("Resources")]
        ),

I have a single background image for the entire game (although I had some ambitious plans to slice it up and animate it... I do have the sliced images already, but that's definitely in "stretch goal" territory). I found it was a kind of annoying amount of code to add that background image.

First the System:

final class ActionGoMenu: System {
    static let referenceResolution: Size2 = .init(width: 1920, height: 1080)
    override func setup(game: Game, input: HID) async {
        let background = Entity(name: "bg")
        background.insert(SpriteComponent.self) { component in
            component.spriteSize = ActionGoMenu.referenceResolution
            component.spriteSheet = SpriteSheet(path: "Resources/ag_background2.png")
        }
        game.insertEntity(background)
    }
    override class var phase: System.Phase {.simulation}
}

And the RenderingSystem:

final class ActionGoMenuRendering: RenderingSystem {
    override func render(game: Game, window: Window, withTimePassed deltaTime: Float) {
        var canvas = Canvas()
        let windowCenter = Rect(size: window.size).center
        guard let bg = game.entity(named: "bg"),
              let bgSpriteComponent = bg.component(ofType: SpriteComponent.self),
              let bgSprite = bgSpriteComponent.sprite() else {return}
        canvas.insert(bgSprite, at: windowCenter)
        window.insert(canvas)
    }
}

So far so good. I have two quibbles:

  1. It seems like it's taking a second or two between launching the app and showing this image. I'm not sure what's going on there, but for such a simple application (so far), it feels like this should be snappy.
  2. My image needs to resize with the size of the window. It's currently centered, and hard-coded to my "reference size", so I just need to do some math to make sure it's the right size when the window is something different.

I'm going to ignore the first one for now, but the second one is easily solved. There is a bunch of stuff in GameMath, but I'm not seeing what I want exactly, so I'll bring in an extension I've used before on CGSize that I'll modify to use with GateEngine's Size2 instead:

extension Size2 {
    func smallerRatioTo(otherSize: Size2) -> Float {
        let widthRatio = otherSize.width / self.width
        let heightRatio = otherSize.height / self.height
        return Swift.min(widthRatio, heightRatio)
    }

    func largerRatioTo(otherSize: Size2) -> Float {
        let widthRatio = otherSize.width / self.width
        let heightRatio = otherSize.height / self.height
        return Swift.max(widthRatio, heightRatio)
    }
}

Now I can just modify my canvas.insert with:

        let ratio = ActionGoMenu.referenceResolution.largerRatioTo(otherSize: window.size)
        canvas.insert(bgSprite, at: windowCenter, scale: .init(ratio, ratio))

...and the image is resized to fill the window.

I'm not in love with allowing the user to freeform resize the window. Maybe this time would have been better spent trying to see if there's a way to clamp the ratio of the window entirely.

That's a task for next time, maybe.

3 Likes

Looks like a fun project! I'm happy to help whenever I can.

They are classes because System originally had internal engine related variables.
This is no longer the case and these will be migrated to protocols eventually.

This is a bug that was introduced recently. I haven't figured out exactly what's causing it but I believe it's related to concurrency changes. Loading simple images should and will be instant.

You cannot lock the window size or aspect ratio.
I did it this way on purpose to ensure games will work on every platform at any aspect ratio.
There is no way to know if a user will have a 4:3 or 21:9 screen for example, or if a platform is even capable of drawing at any given aspect ratio/resolution.
I will be adding a minimum window size option, but no size or aspect locks.

For my game Super Swift Smash! I did the math to center the play area.

You could also use a RenderTarget to create a virtual window of a fixed size or aspect ratio and then draw that into the window. The AnimatedSprite example uses a render target to draw at lower resolution, which is similar to how you could do it for a locked aspect ratio.

3 Likes

Awesome! Glad to hear it!

This too! Let me know if there's anything I can do to help.

Totally makes sense. If I change my call to largerRatioTo() to smallerRatioTo() instead, the whole game "fits" in the window size. That doesn't look too terrible, and is probably the way to go for now.

This is obviously a complex issue, and more thinking will be needed if I want (which I eventually do) to support not just different resolutions/ratios, but totally different orientations and layouts. :thinking:

I like the idea of using a render target. I actually had one in there for a bit (just because I was working from the demo scenes), but removing it didn't change anything from my perspective, so I took it out for the post. Maybe I'll add it back in now for some more flexibility later.

Thanks for releasing GateEngine!!! I'm sure I'll have many more questions for you.

1 Like

Thank you for this writeup and making me aware of this engine! :pray:t2:

SpriteKit is my default game engine, but is Apple platforms only. A cross platform one while still allowing me to use Swift seems like a match made in heaven.

1 Like

Systems within Systems

So I started porting the game logic for my game, and it's actually a lot more code than I remember. It's ~2150 lines of ActionScript, but a little over 3000 lines of C# split up over 11 files. Really, the bulk of what I'm working on porting at the moment is the model code, and that's only ~1200 lines.

I did already convert a Tetromino struct, and I started porting a "parent" model class, but then instead decided to use one from another project, (one where I'd even written a few tests). I put both of those (and a small Coordinate struct that the Tetromino depends on) in a utility package and threw it up on github as SwiftGameUtils. I can't imagine anyone will want to use these, especially undocumented, but maybe I'll write more later about how useful I find this kind of generic game model. If I think of more things I might want to use in future projects I'll probably add them to this package. We'll see.

So anyway, since the model is going to take a bit longer, I figured tonight I'd switch gears a bit and get the game grid itself rendering. A nice first pass, I thought, would just be to fill it with piece sprites, so that's what I did.

First I needed to do some measuring of my reference images, and figure out what coordinates the grid needed to start from. I figured this could be pretty rough, and I'd tweak it later, which it was, and I did.

    /// this assumes 0,0 is upper-left
    /// upper-corner is 244,110 + 21 (half-width/height) on each dimension
    let gameBoardPosition = Position2(265, 131)

    /// 798 = 19x42
    let gameBoardSize = Size2(798, 798)

    /// 798 = 19x42
    let gamePieceSize = Size2(42, 42)

I created a new ActionGoGameSystem.swift, and along with the above, it also holds my model:

    /// internal game logic
    var model = ActionGoGameModel()

...and an array of entities, one for each spot on the gameboard.

    /// An entity for each spot on the gameboard?
    var pieces = [Entity]()

Dustin mentioned in Discord that it would be faster to just have a single entity for the entire game board, but I figured I'd try it the naive way first, and see how it goes. I think it'll probably be fine. Usually the pieces aren't really moving around anyway, so I'm going to call that premature optimization for now.

Here's how I initialize that array:

    override func setup(game: Game, input: HID) async {
        for x in 0..<19 {
            for y in 0..<19 {
                let entity = Entity()
                entity.insert(SpriteComponent.self) { component in
                    component.spriteSize = .init(width: 100, height: 100)
                    let even = (y % 2 == 0) ? (x+1) % 2 == 0 : x % 2 == 0
                    component.spriteSheet = even ? SpriteSheet(path: "Resources/stone_white-tv.png") : SpriteSheet(path: "Resources/stone_black-tv.png")
                }
                entity.insert(Transform2Component.self) { component in
                    let px = self.gameBoardPosition.x + (Float(x) * self.gamePieceSize.width)
                    let py = self.gameBoardPosition.y + (Float(y) * self.gamePieceSize.height)
                    component.position = .init(px, py)
                }
                pieces.append(entity)
            }
        }
    }

Once this is "real" and driven off the game model, I'll be adding & removing the SpriteComponent as pieces lock into position on the game board. (The falling stones will have their own 4 "special" entities, as those will animate between empty spaces on the board, and also move around with player input.)

At first, I was going to give the game system its own rendering class like the menu's. But I ran into a problem because I had implemented Dustin's suggestion that I use a render target, and the render target has a black background, which meant my piece images were sitting on top of that instead of the menu background as they should be. Here's what I saw when I ran the project. (I'll get to some of the other "issues" here in a second):

There might be a way to specify a transparent render target, but I decided to just rename my menu's RenderingSystem, and for now I'll have it render everything in the game. This isn't that huge a game, so I don't anticipate it growing too long, and I can always split it up later if I want anyway.

Those images obviously look wrong as well. There were two issues there. First, I had been setting the SpriteComponent's spriteSize property to my gamePieceSize (42x42), which is what size I want it to render as, not what size the actual image is (100x100). So I changed that, and then I needed to add a scale when I add the sprite to the Canvas.

Here's the latest version of RenderingSystem:

final class ActionGoRendering: RenderingSystem {

    lazy var renderTarget = RenderTarget()

    override func render(game: Game, window: Window, withTimePassed deltaTime: Float) {
        renderTarget.size = ActionGoMenu.referenceResolution

        var canvas = Canvas()
        let center: Position2 = .init(
            x: ActionGoMenu.referenceResolution.width * 0.5,
            y: ActionGoMenu.referenceResolution.height * 0.5
        )

        // add the static menu images
        for imageName in ["bg", "board"] {
            guard let bg = game.entity(named: imageName),
                  let bgSpriteComponent = bg.component(ofType: SpriteComponent.self),
                  let bgSprite = bgSpriteComponent.sprite() else {continue}
            canvas.insert(bgSprite, at: center)
        }

        // add the game board images
        let gameSystem = game.system(ofType: ActionGoGameSystem.self)
        for entity in gameSystem.pieces {
            guard let spriteComponent = entity.component(ofType: SpriteComponent.self) else { continue }
            // scale is because we want these to appear 42x42 pixels, but the original images are 100x100
            canvas.insert(spriteComponent, at: entity.transform2.position, scale: 0.42)
        }

        // TODO: figure out how to lock the aspect ratio of the render target
        renderTarget.insert(canvas)
        window.insert(renderTarget, sampleFilter: .nearest)
    }
}

Which results in:

...success! :tada:

You'll note that I'm getting the entities to render from the game system. I wasn't even sure that would work, but it seems to be great, and I like that a lot, because I really dislike the (seemingly common) ECS pattern where you loop through all the entities looking for ones that have the components you want. I also don't love the stringly-typed "bg" and "board" way I'm getting the background images either. The part I don't love there is mostly my fault though. (If this was a client project those would definitely be static String properties somewhere. I might still fix that.)

You may also notice that I'm inserting the spriteComponent for the pieces, as opposed to the sprite itself, which is because I created this kind of ridiculous Canvas extension:

extension Canvas {
    public mutating func insert(
        _ spriteComponent: SpriteComponent,
        at position: Position2,
        rotation: any Angle = Radians.zero,
        scale: Size2 = .one,
        depth: Float = 0,
        opacity: Float = 1,
        flags: CanvasElementSpriteFlags = .default
    ) {
        guard let sprite = spriteComponent.sprite() else {return}
        self.insert(
            sprite, 
            at: position,
            rotation: rotation,
            scale: scale,
            depth: depth,
            opacity: opacity,
            flags: flags
        )
    }
}

This was really just a proof of concept, and I might even get rid of it now that I know I can do it that way if I want to.

Getting back to the RenderTarget for a minute, the main drawback, as I see it, is that the window can be resized to any aspect ratio, which will make my game look all scrunched in some sizes. For instance:

I'll admit, I do kind of want to try playing it at that size though. :joy:

Next post, I hope to have finished porting the game model, and maybe then I'll tackle user input.

5 Likes

There’s a clearColor property.
renderTarget.clearColor = .clear
I will add an initializer that sets the color for version 0.2.0

You can use the render target as a texture.
Something like this would keep it square.

canvas.insert(
    renderTarget.texture, 
    at: .zero, 
    scale: Size2(window.pointSize.width / renderTarget.size.width)
)
2 Likes