Today I wanted to share my advancements on Frame
, a game engine I began almost two years ago, for Apple devices
only.
In December 2020 I played a bit with Swift, SwiftUI, and Metal, on a custom game engine called “Frame
”.
At that time I was not yet a father, and I had a lot of spare time (too much maybe) to spend on this project.
I dropped the project a year after, in December 2021, as it became very difficult for me to focus and spend my
spare time on this kind of difficult project.
As making games is hard, making game engines is 100x harder, trust me.
Recently, during an incredible sunny day, I re-opened the Frame
project, looked at the code and, using the strength
of a powerful and deep voice, said “WhAt ThE F#$% Is ThIs CoDe?!”.
After one year and a half I became a much better Swift programmer, and I noticed quickly the core mistakes
I made, which “forced me” to step back from the project.
So, during two week-ends and a few days of vacations, I get back on “Frame
” to deliver a much better experience to
the game developer (actually ‘me’), more features, better performance, and… more platforms support!
The first thing I made was to make my Swift code simpler to understand and better to maintain.
I removed a lot of duplicated things, introduced new code abstraction for better code management, make the Scene
object
as a real code representation of a game scene, etc.
The mistakes I made in the first version of the engine was the consequence of how I learned Swift and SwiftUI, and how I understood the concepts at this time.
Now, as I became better using those tools, I am able to detect some anti-patterns and to fix them quickly.
Pretty nice!
In the first version of Frame
, which was a simple 2D rendering tool, this is my codebase
(using cloc):
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Swift 18 170 249 741
Metal 2 8 19 28
-------------------------------------------------------------------------------
SUM: 20 178 268 769
-------------------------------------------------------------------------------
For the new version of Frame
, which is basically a 2D/3D rendering tool with proper 3D objects and multi-platforms support:
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Swift 14 142 204 791
Metal 1 14 13 59
C/C++ Header 1 7 11 24
-------------------------------------------------------------------------------
SUM: 16 163 228 874
-------------------------------------------------------------------------------
which is “only” 100 more lines in total, not bad at all!
The C/C++ header is to use only one representation of data structures to pass in the shaders, instead of duplicating the data structures each time.
In the previous version of Frame
, there was no proper camera - only a fixed “eye” at origin (0, 0, 0) that displays
everything around him with a limit of (1, 1, 1).
The camera is now an object, and contains a proper Field of View (FOV) property, and a limit (the far limit
) to avoid declaring and rendering very far objects from the camera (something that could exists at runtime, but so tiny in
the scene that you can’t see it very well).
The camera position and angle can be updated using native gestures through SwiftUI:
Actually, the camera is a special Swift 3D object (see next section) that does not contain any mesh or texture, and not rendered.
An example of the interaction in a very basic 3D scene that contains 5 differents objects:
For 3D objects I can now render dynamically any .obj
file for an abstract 3D object, as soon as it packaged in the app (which is not really useful for a game engine, I admit it).
The 3D object can move its own position in the scene, its euler angles, etc. There is no limit for it.
Any 3D object derived from a BaseObject3D
protocol, which derived itself from ObservableObject
:
protocol BaseObject3D: ObservableObject {
/// The position of the object in the world space
var position: simd_float3 { get }
/// The visualization angle of the object in the world space
var angle: simd_float3 { get set }
/// A name to describe the object
var tag: String { get }
/// Function to update the attributes of the BaseObject3D, each
/// time the render function must be called
func update()
}
The BaseObject3D
is very simple, but I will make it a bit more comfy to add new attributes and
information, like the meshes to use, the texture to load and to render, etc…
I added support for textures, and created another shader to display them correctly.
I still have some issues to render a texture on rounded 3D objects (like a sphere), but this will be fixed
anytime soon.
Also, I implemented a first version of a Light
object, to light up (and down) 3D objects that belong to
the rendered scene.
There are still big improvements to make for the Light
object but the basic version works quite well.
This can be part of the first section but the scene and the
rendering code were really intricated and very difficult to maintain without touching of each other.
Normally, in a modern and “generic” engine, this should not happens: the renderer should run without any internal
knowledge of the scene to render: just vertices, colors, and textures to draw.
The first version of the renderer was not great, and difficult to maintain.
I also made some abstractions to avoid containing the renderer in the internal Coordinator
object of the engine view,
which should be a NSViewRepresentable
derived object.
Now, running the full engine, creating and rendering a simple 3D scene is taking 24MB of RAM maximum and, of course, the rendering is done 60 frames per second.
Simplifying the code and the rendering reduced drastically the RAM taken by the app, from 100MB to 24MB. There are new things to be done in order to increase the performances, but I am happy about the improvements for now.
The renderer takes, in average, 4ms in debug mode, so I can render this basic scene at 250 FPS… in debug mode, and for each platform.
The main drawback of my initial project was the support of multiple Apple platforms.
At the beginning I imagined Frame
as a game engine to run on macOS, to developer games for macOS.
However, more and more I worked on Frame
and more and more I was convinced to run the engine on the iPad and develop games on the iPad directly could be a really nice feature.
Now, Frame
is supported natively1 on iOS, iPadOS, and macOS.
The devices I test all along the changes were:
The debugger of the engine (the right part of the renderer) is not displayed correctly in the simulator, but it renders well on real hardware.
I use Apple devices as a developer since more than 12 years now, and I am noticing each year the decline of Apple to develop appealing and stable tools for developers… like Xcode.
Xcode is a messy developer tool, greedy for computing resources, to write code for Apple devices.
Some annoying features from Xcode are still present since a few years now, like Xcode Preview, which does not reload correctly, or some random build errors (clean + build again fixes the issue), or some cache issues, and very annoying simulator issues to emulate the iPad app on the macbook through Catalyst (pinch-in and pinch-out works half of the time).
Those issues are present since I worked on the first version of Frame
- almost two years ago - and I am frustrated to use
a buggy (developer) tool to develop a big project.
I never thought I would say this but, concerning developer tools, Microsoft delivers much better developer tools for their
platform, like Visual Studio 2019 or Visual Studio 2022…
I made big improvements over two weeks of (hard) work on Frame
, and I am very happy with the changes for this
new version.
Of course Frame
is absolutely not ready to create a very simple game using it, but it’s a new beginning for me and
I am excited to see what I will do next on it.
I know it will take me, at least, another two years to make very simple games using it, building and delivering
as packages on the iOS and macOS App Store.
The list of features to implement keeps to grow:
The code of Frame
will be kept private until I am done with it, and ready for interested people
to take a look and use it.
Using the same Swift / SwiftUI code, and one click (on Xcode) to build and deploy on the platform of your choice. ↩︎