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! 
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. 
Next post, I hope to have finished porting the game model, and maybe then I'll tackle user input.