Since we covered what the user facing language experience looks like in dev log #5, it's a good opportunity to talk about the user facing API experience. The luxe API is designed a little differently to what users might be used to, so we'll look at how and why that is.
Note that this dev log might be slightly more technical than usual, but hopefully still approachable! We cover the reasoning behind the engine design here though, so hopefully it helps. I intend to start a tech specific thread of dev logs soon btw, going into deeper details.
Also note that the 1.0 version of luxe discussed below is not available yet, we're working on getting it usable! Subscribe for more news.
Before we discuss the API itself, I want to mention some things on the human side of programming and making games. I know a lot of people feel this way, but it's important sometimes to say things out loud too.
A lot of the culture (unfortunately) around making games - around programming especially - tries to needlessly put other people, other tools, or other approaches down. This is wrong. Don't do that. Please don't be like that in the world around you.
I find it useful to recognize when something I encounter is just different from what I'm used to. This is a normal human reaction thing we all have to deal with (change is a weird thing!) and being aware is really helpful in our communities.
There are so many amazing ideas, approaches and tools that we really are spoiled for choice when making games these days...
But more critically, there are so many amazing people already around us, and even more growing up around us. They're just as interested in expressing themselves and creating things. Some of them have new, unheard of ideas that would blow our minds - if we let them. We miss out on so much when we (maybe unconsciously) push people away. Be conscious!
on API design
When building luxe, pragmatic and user centric choices are made with careful intent. That means I can assure you up front that many of these decisions were not made in a vacuum, not done to be contrarian, and were made with compromises and concessions in every direction. This is how programming goes, so nothing new here!
A lot of these choices will probably never stop being deliberated on in my head, but in order to actually finish something - at some point you have to decide and move on. This post is about what luxe has decided on.
I aim for luxe to be accessible, legible, and practical above all. This directly impacts the API, fluidity of working on projects, adaptability in being portable and more. I genuinely believe the design choices are in line with these goals.
APIs and intent
When considering APIs as a concept, it's also useful to consider which level you're using it at. In the context of a game written in luxe, the game code is your API. It's your own design choices so the code that you, your team, or your game interacts with can look however you want it to.
Somewhere there is code that will talk to the luxe API, and if you prefer a different paradigm you're totally able to abstract any details because your own API as needed.
APIs aren't all or nothing. They're a tool to express intent.
Data Oriented Design
In programming there's a concept called data oriented design (DOD) and it's a paradigm (like most of them in programming) that spans several concepts all at once.
This has the downside that a lot of people mean different things when they say "data oriented" but on the low level, there are concepts that are always there, like, designing your code with the flow of data through your program at the forefront.
For our discussion today, we're talking about code that is oriented around the data that it operates on. This includes designing the data around our hardware and target platforms, which we'll see why later, but also the APIs being designed to facilitate this.
A lot of people are taught "Object Oriented Programming" (OOP) which is another paradigm that spans several concepts, and has over time evolved to various meanings. This approach to programming is what a lot of people are familiar with, so we'll be comparing to that to see how the luxe API looks when designed in a data oriented way.
We won't get too far into the details of either because this post is about the luxe API. I also didn't include any specific links here because that implies a specific definition - we don't have space for going over every detail.
As a very simple comparison let's take the example of an entity, like a player. An entity is just an object in our game world. We'll probably have a bunch of these, like trees and stuff.
Object oriented is designed around objects:
var entity = new Entity(...) entity.set_pos(x, y) entity.destroy()
Data oriented is designed around operating on data:
var entity = Entity.create(...) Entity.set_pos(entity, x, y) Entity.destroy(entity)
In this case the data oriented API is operating on the entity, rather than the entity operating on itself. This approach has several benefits that we'll get into as we go along.
My first program
When I was young and I was learning to program, I didn't have the internet and I didn't have a lot of references to go on. I had Turbo Pascal and a drive to create things, and enough details to make a program do something. I was fortunate to get started so early, but my tools were functions, variables, and structures of data.
What this led to however is my code being simple and obvious, which is far from a bad thing. It laid bare the intent and there was nothing architected and complex about it. I had some data, and I wrote some functions that changed or did stuff, based on that data.
I learned programming as a flow of input data => transformation => output data.
Whenever I needed to program something, I drew that input->change->output diagram on paper, and filled in the blocks and arranged them into what I felt was the right place. Often I would be wrong! I would rearrange them based on the data, and how I needed it to behave.
side note: I still do that... The UI shown later was fixed up yesterday on stickynotes.
Fast forward 18 years
I've spent a long time programming and I love it. I explore every corner of languages, and every paradigm because I like learning. It's interesting to later realize that this model of input -> output transformation is basically what programming is. All code is doing, is operating on data.
I've experimented with many forms of API design and systems design. I've tried complex OOP trees, prototypical inheritance and everything in between. The alpha version of luxe was actually exploring the idea of minimal OOP with restrictions, where the only inheritance is "specialization" and the rest is flat classes.
This is also the type of stuff the exploration design diagram was referring to. I was forming a picture of what I need/want going forward.
Returning to data oriented approaches
For the long term version of luxe with a focus on the future, I couldn't help but return to data oriented thinking. Not just because it's a clearer mental model (for me) but also because it gives the engine and it's APIs several advantages that align really well with the project goals.
The engine code is simple and to the point, operates on data in ways that make it easier to operate in parallel, and goes back to being simple functions, variables and structures of data. This is great!
It also happens to model really well for how modern hardware behaves, like an API of pure functions and abstractions with strong data ownership all come in very handy.
quick CPU / Memory primer
Computers have progressed incredible amounts obviously, and the reasons data oriented programming is gaining popularity the last few years has is related to how computers are built today. My phone has 6 CPU cores, all in the Ghz range, and 2GB of memory for example. We have a lot of CPU power and cores, but memory access speed is still currently bound by the laws of physics.
This topic is pretty deep, but here is quick view of how a typical CPU is built and how it sees your ram.
Here we can see that we have the CPU which does work, but it needs memory to operate on (data). The memory is staggered in multiple caches, a little bigger each time, holding onto small chunks of memory.
This allows it to read faster for what it's doing right now, and also fetch stuff into the other caches in advance. Fetching memory has a delay to read (latency), so whenever you need something far away, it sometimes has to wait for that. This waiting makes programs much slower. It is worse when it fetched stuff it didn't actually need, so it has to reset all of these caches in the middle of doing work. This has a huge cost, relative to the code itself.
This is mitigated by these caches, and it works on a small blob of data called a "cache line" (usually 64 bytes). If you can design your code to be aware of this, your code is going to work well with the hardware, and work a lot faster.
Here's a short clip from the talk showing the relative access speeds for L1, L2 and RAM access. Note this slide is from a talk in 2014, over 3 years ago. CPUs and memory has continued to progress, the fundamental challenge hasn't - memory access is a bottleneck for code, and that concept applies to all languages because they all run on the same CPU/memory.
For more details
The talk above is about data oriented design in C++ by Mike Acton. If this topic interests you, this is decent talk to watch on it.
If you'd like a nice clear visual representation of what the CPU does for memory I'd suggest watching this great talk by Sergiy Migdalskiy called Performance Optimization, SIMD and Cache. The talk shows how CPUs try to optimize when waiting for memory, and how you can program to optimize for that.
And if you want to understand a really deep look at cache coherency in the context of multicore CPUs this talk from Christian Gyrling called "Multi-core programming and cache coherency" is worth the time.
Why luxe cares
Well, it's a game engine. It obviously cares about performance, but it also does so within it's own goals and needs. I spoke about the "good enough" metric and the engine goals in dev log #4 in relation to parallelism. Today we're looking at it from the API perspective, and how designing for modern hardware affects us.
It's not all about performance though
There's a lot to like about this design paradigm in the context of luxe, workflow wise.
call site consistency
//oop entity.destroy() //data oriented Entity.destroy(entity)
Can we find where every single entity is destroyed in our game code by a simple search? This is less easy in the object style -
tree.destroy are distinct, and the
destroy part is not specific to entities (probably everything has a destroy function). Yet
Entity.destroy is shared by every single call site where an entity is destroyed in the data oriented API. I like that.
This applies to refactoring too. If a function changes name or arguments, finding every single place it is used is also trivial.
I also like that all the APIs related to a particular type of data are in the same place.
Entity.* is where to look for functions that manipulate an entity.
World.* for functions that operate on a world instance, and so on.
I find exploring an API like this to be easier, since I can check the docs, use code completion or read existing code and know where it comes from. I always know where to look because the location is described directly in the code.
There's more reasons like accessibility, easier to learn APIs for beginners, explicit intent and more - but there's more to talk about the API implementation details first.
side note: Installing luxe will soon be one click...!
An instance is...
So here's a specific technical question, if we know how we want to operate on data via the API function calls, what is "the data" that we operate on, and where does it live? In other words:
//oop var entity = new Entity(...)
entity actually contain, i.e what is the data here?
Well if you're familiar with OOP this is an instance, with methods and properties. It contains everything inside itself, it is the data, as well as the functions that operate on it, and all internal/extra data.
entity variable, is a reference to operate on the data, the data lives somewhere in memory (often inside the language level, being checked on by the garbage collector). This is if we consider the
Entity class to be written in wren (or whatever language we're writing in).
We could say: the instance variable is a handle pointing to the data.
engine core <-> API
What this approach doesn't account for is having an engine core in c++ (or any different language). It doesn't account for more complex nuanced scenarios like serialization or networking - because the data is not in our control, the language owns it. We have extra steps to take to do anything with it.
What we'd really like is to have control over the data, so we can manipulate it in the most appropriate way for the system that is operating on it.
For example, we can rearrange our entity data for performance, or for dealing with all entities at once, so we can make choices about them as a group. We can arrange them by the order in which they last moved, and send only a handful over the network. We can treat the data in ways that work well on modern hardware.
If we disconnect the data from the instance this is easy.
Taking ownership of the data
We just split the data from the instance! We take control of the data and change our API to be modelled differently. Since we can control memory access patterns on an entity we have a lot of flexibility, ones we don't have from tying to an object.
So in our data oriented approach, the instance is no longer the data itself, and it does not operate on itself. The instance is still a handle, but the handle doesn't have to be an object, it can be something much simpler. What if our entities could be a number, just stored in an integer?
Handles as primitives
Sending integers around in memory is pretty quick, they're tiny (4 or 8 bytes). It's also easy to send them over a network, or to a scripting language, because they're a primitive value. We can do a lot of things with an instance if it's not tied to it's data, and not tied to our language data model!
An integer is also really quick to allocate. This has a direct impact on workflow and user experience - load times. If we have to load a level that has 5000 entities, how long does that take? Making an integer a handle is super fast, and since we control the data, we can load/initialize them much more efficiently.
allocation and GC overhead
In the OOP example, every
new Entity call has another cost, not only because it has to allocate memory for the functions/data as well, but also because it has to be stored in the garbage collector - at least in most languages you'd write games in.
The garbage collector tends to want to have a look at things occasionally, and when there are many things, looking can take longer and longer. The more loose objects we create, the more objects it has to think about.
The engine API should not be the one doing that. The engine should leave as much room for the game as it can. This frees up the game code to be the important part (on the scripting or game code level), without the engine API calls getting in the way.
By treating instances as primitives we can avoid this too.
What we really want is the API to be aware of memory,
both in access patterns and allocation concerns.
luxe is data oriented
In spirit of being a light weight, rapid iteration engine for making games - this paradigm fits the engine design goals perfectly.
It allows us a lot of flexibility internally for the core to be portable, performant, and future proof, while also providing clear and easy to use APIs to the user.
More benefits of this will be discussed in future posts.
A data oriented entity system for luxe
In the next post we'll go into more details about the entity model in luxe, and how a data oriented design helps us there.
We'll also finally start seeing the new workflow in luxe, how it fits together, and how it benefits us making games.
Here's some random functions from the new APIs to give you some more insight into the above. You'll notice that some static API is common to all languages (like
Math), we're just making it explicit and consistent across a whole API.
var mx = Input.mouse_x() var my = Input.mouse_y() var fov = 60 var cam = Entity.create(world, "outside_camera") Camera.create(cam) Camera.perspective(cam, fov, app.width/app.height, 1, 800) var ray = Camera.screen_point_to_world(cam, mx, my) var world_pos = Math.ray_intersect_plane(...
We're getting closer than ever to the new version being ready.
If you'd like to be notified when these posts are made, sign up here. I'll only send an email when a notable dev log is posted, that's it.
Feedback and questions on this post can be found and shared in this discussion.