Engine-Game Dichotomy

Posted on 2024-04-28

[2024-04-27 Sat]

  1. Unbundling Tools for Thought

    There’s a joke in game development that there’s two kinds of game devs: those who write engines, and those who make games. The people who make the engines do it for the intellectual pleasure of discovering a beautiful algebra of vectors, scenes, entities, and events; and watching a beautiful, crystalline machine in operation. The actual game—which is never finished, rarely started—is an afterthought. Of course you wouldn’t make a game. That would be parochial. The engine was the point. People who want to make games just download Unity and push through the horror.

  2. As I said in my own comment down thread, despite being a huge rust advocate, …

    Rust is not a good language for actually writing games, and the fact that it is being sold as such is really detrimental to it in my opinion, because it is holding the ecosystem back. Rust is being pushed as a language for game logic, so people try out and realize it isn’t very good at that, and so they just give up on Rust in the game development industry at all and leave, understandably! If Rust were more strategically positioned, it could get a lot farther. Where it should be focusing in the games industry is on game engines, where flexibility and quick iteration and easy prototyping and being able to just reach out and directly touch and control things isn’t as important, but where concerns like the clarity and maintainability of the code base, stability of the software, resource ownership and management, and eeking out every ounce of performance all become important, and so the type system and static analysis guarantees of Rust are actually useful.

    This is where, I’m disappointed to say, I think things like Bevy and Amethyst have severely hurt the Rust game development ecosystem. They aren’t really game engines in the traditional sense, they are more like game frameworks like Love2D except written in Rust: they force you to statically link your game code to the engine code, and write your game logic in the same language your engine is written in. This means that game developers who just want to quickly prototype game mechanics and want to be able to iterate on them in order to refine them are forced to use a language that is far too focused on correctness, safety, static verifiability, and concerns like that to actually be usable as a programming language, and worse, it forces them to compile their game logic and the entire engine together and link them together in order to build their actual game and test it, massively increasing the weight of the process and basically ruling out hot reloading or making your game independent of any specific version of the engine, or its license. It puts them between a rock and a hard place, between using some other ecosystem, or using a language that simply unsuitable for a game development.

    1. But people want to write Rust and a game seems like a fun way to do it. They …

      But people want to write Rust and a game seems like a fun way to do it. They can already use Godot or Unity with this approach.

  3. Rapid prototyping == exploration, benefits a lot from stuff that minimizes the cost for exploration, this means stuff like Python (ML research), Common Lisp (if you are doing something solo and there’s no existing popular language supporting it), PHP / JavaScript / Ruby (web dev)

    1. When you don’t care about exploration as much, you probably can benefit from tools that are optimized for supporting you at the cost of exploration

      1. Game engines are written in C++ (why?)

      2. Linux kernels are written in C (because of high quality tooling to avoid the footguns)

      3. Even the popular kernels used by Linux and BSDs are not fully formally verified (like sel4 is)

        1. This makes sense for Linux, given that there still seems to be a lot of exploration done on the design space, and because the specifications change as the world changes

  4. This Problem Changes Your Perspective On Game Dev - YouTube

    A prototype is a very quick and scrappy version of whatever you want to try. So, should you build perfect, production-ready code when prototyping? Hell no. Cause I’ve been asked this before: the only thing you care about when sitting in a speed boat – in a scouting boat – is going fast and making a halfway decent measurement that’s roughly in the correct ballpark. Those are the only two things you care about when prototyping. So take shortcuts. Go as fast as possible. Do not focus on writing clean code. That does not matter at this point. […] The rule of thumb is you do whatever is fastest. Whatever is fastest, that’s what you do. […] Another thing you can do to reduce exploration costs is to parallelize. Send multiple scouts at once. Especially in indie teams[, they] ever only put one person into a scouting boat. It’s better to have more scouting boats than to have bigger scouting boats. If you ever have idle team members sitting around, put them into a scouting boat immediately. Speed up the evaluation of your prototypes. Take shortcuts wherever you can when scouting. And speed up the decision-making in your team. I have observed this with my own eyes. A lot of teams love discussing. They love the decison-making process. They’re like, “Great! If we talk we don’t have to work.” And then they spend hours and hours and hours discussing where to send their scouting boats, and it would have arrived two days ago if they had just sent it.

    1. This is a pretty big deal for me – I seem to have historically had a very… unusual(?) orientation to programming and projects and alignment research

      1. I guess it comes from a misinterpretation of security mindset? Or aversion to wasted effort?

      2. Debugging stupid bugs makes me really annoyed, and I think I confuse the local goal of writing good code with the more holistic goal, one that takes into account more of the context, which then serves to show why we are where we are, and whether it really makes sense to consider “hey let’s just use a better system”

        1. Hmm. So perhaps there’s a trade-off between prototyping/exploration and reliability in systems?

          1. I mean, there obviously is always a trade-off. Perhaps I just didn’t notice it? Or is it the tendency to not use spaghetti towers mainly because I seem to enjoy the flow of building such systems and find the seeming ad-hockery of other systems as annoying?

  5. Leaving Rust gamedev after 3 years

    1. “Once you get good at Rust all of these problems will go away”

      1. I notice that I have implicitly had this model in my head when it comes to Haskell. The notion that the time invested in learning it is going to be more than worth it

        That being said, there is an overwhelming force in the Rust community that when anyone mentions they’re having problems with Rust the language on a fundamental level, the answer is “you just don’t get it yet, I promise once you get good enough things will make sense”. This is not just with Rust, if you try using ECS you’re told the same thing. If you try to use Bevy you’ll be told the same thing. If you try to make GUIs with whichever framework you choose (be it one of the reactive solutions or immediate mode), you’ll be told the same thing. The problem you’re having is only a problem because you haven’t tried hard enough.

      2. I have believed (and I recall that Haskellers on /r/haskell have also kept claiming this) that there’s a ‘shift’ in mindset you need to learn to be able to use Haskell well, and supposedly that it would be worth the effort

        1. This is a claim that your sacrifice of expressivity is significantly worth the supposed benefits

        2. I now have started to believe that this is not really true

        3. It seems likely that allowing yourself to use the way you think about the problem as the way you write code would lead to massive benefits, especially for prototyping.

        4. And Lisp seems to be the best tool for this

          Lisp languages are immensely flexible and permis­sive in how their pieces can be connected. This means that the way you think about a programming problem can be quite close to the way you actu­ally program it. (This is also why Lisps have traditionally excelled for proto­types and exploratory work.)

          To be fair, getting the most out of a Lisp means learning to think more in the Lisp idiom of lists and expressions. For that reason, I agree with Seibel—trying it yourself is the best way to be convinced of the benefits. As you get a feel for lists and expressions, it does pay increasing dividends throughout the language. You see how tiny lines of code can produce epic amounts of work. You also start to appreciate that even in a well-designed language like Python, you’re spending a lot of time shaping your ideas to fit its limitations, like shaving an invisible yak. – Beautiful Racket: Why Racket? Why Lisp?

    2. The author points out a potential trade-off between maintainability and iteration speed

      I’d argue as far as maintainability being the wrong value for indie games, as what we should strive for is iteration speed. Other languages allow much easier workarounds for immediate problems without necessarily sacrificing code quality. In Rust, it’s always a choice of do I add an 11th parameter to this function, or add another Lazy<AtomicRefCell<T>>, or do I put this in another god object, or do I add indirection and worsen my iteration experience, or do I spend time redesigning this part of code yet again.

      1. Related: a friend claimed that CL is maintainable in the large, while Python isn’t so. His reasons:

        Part of it is better tooling, which Python is attempting solve with type annotations and Visual Studio Code. But a bigger part of it is that because Common Lisp allows you to program in a more functional style and has a macro system which allows you to do seamless code generation, you can just avoid having to write large amounts of what, in Python would be boilerplate functions for e.g. serializing and deserializing code.

        1. This seems plausible enough that I’m willing to invest efforts into learning Common Lisp

    3. “Indirection only solves some problems, and always at the cost of dev ergonomics”

      One fundamental solution that Rust really likes and that very often works is adding a layer of indirection. A canonical example of this is Bevy’s events, which are the go-to suggested solution for anything related to “my system needs to have 17 parameters to do its thing”. I’ve tried to be on both sides of this, even specifically in the context of Bevy of trying to use events more heavily, and trying to just put everything in a single system. That being said, this is just one example.

      Many issues with the borrow checker can simply be worked around by doing something indirectly. Or by copying/moving something out, then doing the thing, then moving it back. Or by storing it in a command buffer and doing it later.

      1. And category theory (including Haskell) inherently involves indirection to solve issues due to its strictness (monads to get IO, for example)

        1. This reminds me of AViD’s rule of usability: “Security at the expense of usability comes at the expense of security.”

      2. Sidenote: one practical consequence of this is that I might want to switch from Haskell and Hakyll to Common Lisp for my website

        1. Not racket, given what Butterick experienced with the guy who bullied him out of the community

        2. Not python, given what I expect to be its shortcomings compared to a lisp

        3. Also given that nobody else will touch this project, I’m free to use a relatively esoteric language.

      3. Also note that G.’s frustrations with his website is mainly because Pandoc and Haskell forces him to use a solution similar to what is described in the parent heading quote – passing around ridiculous numbers of parameters

      4. I also like how the author describes the psyop-ness of the Rust community

        Anyone who’s been in the community long enough have had the experience of being told that this is actually a good thing, separation of concerns, code is “cleaner”, etc. You see, Rust was designed in a smart way, and if something can’t be done, it’s because the design is wrong, and it just wants to force you down the right path … right?

        What would be 3 lines of code in C# suddenly becomes 30 lines of Rust split into two places. The most canonical example here is something like: “while I’m iterating over this query I want to check a component on this other thing and touch a bunch of related systems” (spawn particles, play audio, etc.). I can already hear people telling me well duh, this is obviously an Event, you shouldn’t be writing that code inline.

      5. Other interesting quotes

        This is a relatively simple example, but it is something one might want to write. And especially when implementing a new mechanic and testing things, it is something that you can just write. There is no maintainability to think about, I just want to do very simple things, and I want to do them in the place where they are supposed to happen. I don’t want a MobHitEvent, because maybe there’s 5 other things I may want to check the raycast against.

        I also don’t want to check “is there a Transform on the Mob”? Of course there is one, I’m making a game. All of my entities have a transform. But Rust won’t let me have a .transform, let alone in a way that would never crash with a double borrow error if I’m accidentally inside queries with overlapping archetypes.

        I also maybe don’t want to check if the audio source is there. Sure I could .unwrap().unwrap(), but the more observant 🦀’s will notice the lack of world being passed around, are we just assuming a global world? Aren’t we using dependency injection to write out our query as another parameter in the system with everything laid out up front? Is .Choose assuming a global random number generator? What about threads??? And where exactly is the physics world, are we seriously assuming that to be a global too?

        If you’re thinking “but this won’t scale” or “it might crash later” or “you can’t assume global world because XYZ” or “what if multiplayer” or “this is just bad code” … I hear you. But by the time you finished explaining to me that I’m wrong I’ve already finished implementing my feature and moved on. I wrote my code in single pass without thinking about the code, and as I was writing it I was thinking about the gameplay feature I was implementing and how it affects the player. I wasn’t thinking “what’s the right way to get a random generator in here” or “can I assume this being single threaded” or “am I in a nested query and what if my archetypes overlap”, and I also didn’t get a compiler error afterwards, and I also didn’t get a runtime borrow checker crash. I used a dumb language in a dumb engine and just thought about the game the whole time I was writing the code.

    4. I now feel like it was a mistake for me to have an aversion to working on montemac’s activationadditions repository

      1. I did make it clear what I felt about things and why I disliked it, but I guess it would have been useful if my mentor had been able to sit me down and walk me through these flawed reasoning

        1. But honestly, C. couldn’t do that, even as he was my mentor. S. didn’t do that, L. didn’t do that. I don’t think I even encountered a good enough argument in FFF to help me wrap my head around this

        2. Observing people’s reasoning about things, experience with Haskell, stuff like this seems to have been important for me to figure this out, because I guess I was inundated by a lot of psyop by people online

        3. And it took me to encountering this case study to be able to ‘connect the dots’, essentially speaking

        4. I’ve disabled all my Haskell ‘notes’. I guess that’s the first step, and the biggest indicator of change

          1. Perhaps over-focus on ‘notes’ is also a surrogation error, much like the Haskell psyop?

            1. Probably not, but it seems sensible to track how I feel about this as the days pass

    5. Generalization and category-theoretic elegance as an indicator of impracticality

      Generalized systems don’t lead to fun gameplay

      A very commonly offered solution to many issues preventing here is more generalization through systems. If only components were more granularly split up and proper systems were used, surely all those special-cased problems would’ve been avoided, right?

      Strong argument that is tough to say much against, other than “general solutions lead to boring gameplay”. Having been quite active in the Rust gamedev community I’ve seen a lot of projects others were building, of course often the suggestions they offer do actually correlate with the game they’re working on. People who tend to have neatly designed systems that operate in complete generality tend to have games that aren’t really games, they’re simulations that will eventually become a game, where often something like “I have a character that moves around” is considered gameplay, and where the core focus is on having one or more of the following:

      Procedurally generated world, planets, space, dungeons. Voxel based anything, with deep focus on voxels themselves, rendering voxels, world size and performance. Generalized interactions where “anything can do X with anything else”. Rendering in the most optimal way possible, if you’re not using draw indirect are you even making a game? Having good types and “framework” for building games. Building an engine for making more games like the one that is about to be built. Multiplayer. Lots of GPU particles, the more particles the better the VFX. Well structured ECS and clean code. … and many more

      1. What would this imply for the provable safety agenda?

    6. Rust users (and Haskell users?) seem to equivocate between the fun of some tool and the usefulness of some tool

      All of these are fine goals in terms of playing around with tech and learning Rust, but I want to re-iterate what was said at the top of this article. I’m not evaluating Rust from the perspective of technical curiosities or “this scratches the right brain itch”. I want to make real games that will get shipped to real people (not developers) in reasonable amount of time, that those people will pay for and play, and have an actual chance of hitting the front page of Steam. To clarify, this isn’t a cold blooded “make money at all costs” scheme, but it’s also not a “I’m just doing this for the lulz”. The whole article is written from a perspective of wanting to be a serious game developer who cares about games, gameplay and players, and not just tech enthusiasm.

      Again, nothing wrong with tech enthusiasm, but I think people should be very careful about what their actual goals are, and above all be honest with why they’re doing what they’re doing. Sometimes I feel like the way some projects present themselves are the way people talk about those projects is false advertising that creates an illusion that commercial goals can be attained with said approaches, instead of making it more clear that “I’m just doing this for the tech itself”.

      1. This is basically the psyop I’m talking about

      2. But I don’t know why this occurs, not yet

    7. There’s a trade-off between clear/simple/elegant/reliable/maintainable/sensible-abstractions-based systems and practical games

      Now back to generalized systems. Here’s a few things that I think create good games, that are going directly or indirectly against generalized ECS approaches:

      Mostly hand-designed playthrough of a level. This does not mean “linear” or “story”, but it does mean “lots of control over when the player sees what”. Carefully crafted individual interactions throughout the levels. VFX that are not based on having lots of same-y particles, but time synchronized events (e.g. multiple different emitters firing on a hand-designed schedule) working across all the game’s systems. Iterated playtesting with multiple passes on gameplay features, experimentation and throwing away what doesn’t work. Shipping the game to players as fast as possible so that it can be tested and iterated on. The longer nobody sees it, the bigger chance nobody cares about it when it comes out. Unique and memorable experience.

    8. “Game development isn’t building a physics simulation”. Tight focus on value provided to customer. Pragmatism and the eschewing of surrogates.

      What I think most people get wrong is mistaking carefully thinking through player interactions and designing them as something artistic. I’d argue that this is what game development actually is. Game development isn’t building a physics simulation, it’s not building a renderer, or building a game engine, or designing a scene tree, or a reactive UI with data bindings.

    9. Davidad’s agenda is looking less and less plausible to me now

      A good example game here would be The Binding of Isaac, which is a very simple roguelike with hundreds of upgrades that modify the game in very involved, interactive and deeply complex ways. It’s a game with many systems that play into each other, but it’s also something that’s not at all generic. It’s not a game with 500 upgrades of the “+15% damage” variety, but many upgrades are of the “bombs stick to enemies” or “you shoot a laser instead of projectiles” or “first enemy you kill each level will never spawn again”.

      Looking at a game like this retrospectively may make it look like it’s something you could design up front with general purpose systems, but I think this is also something where most people go completely wrong with game development. You don’t make a good game like this by sitting in a dungeon for a year, thinking through all the edge cases and building a general system and then PCGing all the upgrades. You build a prototype with some small amount of mechanics and have people play it, see that the core things work, and then add more things and have people play again. Some of these interactions have to be discovered through deep knowledge of the game after playing a lesser version of the game for many hours and trying many different things.

    10. Rapid iteration seems to give you more high quality information to shape the system with inchoate specifications (just like our inchoate understanding about alignment and minds!), while taking a safety first route to building a system seems likely to result in worse outcomes

      Rust is the type of language where wanting to do a new type of upgrade might lead you down a path of refactoring all of the systems, and many would even say “that’s great, now my code is much better and can accommodate so many more things!!!”. It sounds like a very convincing argument, one that I’ve heard many times, and one that has also caused me to waste a lot of time chasing down solutions to the wrong problems.

      A more flexible language would allow the game developer to immediately implement the new feature in a hacky way, and then play the game, test it and see if the feature is actually fun, and potentially do a bunch of these iterations in a short amount of time. By the time the Rust developer is finished with their refactoring, the C++/C#/Java/JavaScript developer has implemented many different gameplay features, played the game a bunch and tried them all out, and has a better understanding of which direction should their game be taking.

      Jonas Tyroller explains this extremely well in his video on game design as a search, which I’d 100% recommend every game developer to watch, because it feels like the best explanation of why so many games people make (myself included) are profoundly terrible. A good game is not made in a lab where careful types are crafted, it is made by a developer who is a grandmaster player at the genre, and who understands every aspect of the design and has tried and failed many things before reaching upon the final design. A good game is made through scraping a lot of bad ideas, through a non-linear process.

    11. Sidenote: if you have unit tests, constraints, and types as the major styles for safety, then what is the alignment equivalent to constraint?

    12. Formal spec seems to be mainly useful when you know exactly what the problem is

      Rust fits very nicely in the low level algorithmic areas where one knows exactly what the problem is and just needs to solve it. Unfortunately, a lot of gamedev requires more dynamic approaches, and this becomes especially painful around level editing, tooling and debugging.

    13. Are steep learning curves a sign that the system is not fit for prototyping based approaches?

      1. Not true, Common Lisp has a steep learning curve relative to a noob Python programmer, I think

    14. The spec stuff gets in the way of thinking about the problem itself

      Even years into using Rust I still sometimes use too much of my brain thinking about the UI or game, and too little thinking about how I should be structuring my code, and end up with a problem like this. The Rust-y instinct would say “clearly you need to separate your state and not pass around a big struct”, but this is a great example of how Rust clashes with the most natural way of doing things.

      Because in this case we’re building a single UI window. I don’t want to spend any of my brain cycles thinking about what parts of the UI need what parts of the state, I just want to pass my state around, it’s not that big. I also don’t want to spend extra time passing around more fields when I add more fields 15 minutes down the line, which I’m almost certain I’ll do. I also don’t want to be separating things into more than one struct, because there’s more than one thing I might want to do an if on, and having gone down the “splitting structs” path before, it rarely works out on first try.

    15. More psyop mentions

      A lot of the Rust ecosystem has a property of making the user feel like they’re doing something fundamentally wrong, that they shouldn’t be wanting to do a certain thing, that the project they want to build is undesirable or incorrect. It’s a feeling similar to using Haskell and wanting to do side effects … it’s just not a thing “you’re supposed to want”.

    16. Rust gets in the way

      Looking back at all of gamedev over the years, I’ve written a lot more parallel code in Unity using Burst/Jobs than I have ever achieved in Rust games, both in Bevy and in custom code, simply because most of the work on games ends up being the game, with enough mental energy left to solve interesting problems. While in almost every Rust project I feel most of my mental energy is spent fighting the language, or designing things around the language, or at least making sure I don’t lose too much developer ergonomics because something is done in a specific way because Rust requires it to be that way.

    17. “I’m building this thing in order to use this technology” versus “I am using technology in order to build this thing”

      In my experience, fundamentally when you’re starting a software project, you need to make a strong up-front decision between two things:

      1. I am using technology in order to build this thing.

      2. I am building this thing in order to use this technology.

      Developers often fall in the (2) camp but don’t admit it. There’s an allure to using the new, sexy tech that will solve all their problems, whether that’s Rust, Kubernetes, LLMs, etc.

      If you’re in the (1) camp, you should stick with what you know; and if you know that what you know isn’t enough to build the thing, you should use whatever is most common and straightforward, not something off the beaten path.

      Games seem to be the biggest trap, because solo devs often end up building a game engine when they set out to build a game. If you really want to build a game, just use Unity/Unreal/Godot, I promise it’ll go better for you.

  6. Oh god, how does this relate to the fact that people get captured by agent foundations stuff

[2024-04-28 Sun]

  1. Let’s think about agent foundations and evaluate how likely it is that it involves this sort of nerdsnipe

    1. On one hand, I do agree with Eliezer’s assertion that AIXI (for example) is sensible way to start off doing alignment research, because you are focused on simple models and analyzing their consequences.

      AIXI is a perfect rolling sphere that you use to teach sincere students to think about a case of cognition where everything has in fact been fully specified; if you’re an honest technical thinker, it teaches you to stop pulling out hypothetical complications that might save you. – https://nitter.poast.org/ESYudkowsky/status/1783822735564783993#m

  2. Connor has written about this too, in context of davidad’s agenda:

    It’s just very easy to convince oneself to work on what is fun/interesting/grandiose rather than grinding out the nearest bottlenecks one by one – https://nitter.poast.org/NPCollapse/status/1784228984177222031#m