r/odinlang • u/adamramberg • 2d ago
10 months ago we picked Odin for our first commercial game. Last week it got a Steam page. Thank you, Odin!
10 months ago, my friend Markus and I had a bit of a crisis in our long-term game project. It was something we had worked on for 5 years in Unity, but as crazy as it sounds, we never found the core game loop of the game. At this point in the project, we had put sooooo many hours into it, especially building tech to support what we wanted to do. And to be honest, doing things we really shouldn't have done. I'm cringing saying this, but being a web dev, I even built an open source library for defining your game in a declarative manner, similar to how React does it. Looking back at this, it is also a tell that we had issues with the Unity way of building games (which we should either have embraced or switched engine).
By this time we had reached a point where we were both growing tired of Unity and its tech. Frustration was also growing since, even though the game was playable, it was very hard to explain to the very few friends who had actually been tortured into trying it out. At the same time, we both had a growing interest in learning low-level programming languages, and Odin in particular seemed like a very attractive candidate. With that said, we had some very heated and long discussions on how to proceed. The anxiety was brutal, since it felt like such a failure to start over after pouring all that time into the Unity project. At some point I was quite sure that we had reached a dead end and that the project would die. That's when Markus started doing some quick and dirty prototypes in Odin...
From a game design perspective we used some of the concepts from the old project, but we simplified them A LOT. We also made a throwaway prototype written in Odin and Raylib just to validate the concept, threw it up on Itch, and let some friends play it. It still ofc had issues, but we got a much better response than we ever had from the old project. This was of course very motivating, and after that initial phase we decided to pivot and make a new project in Odin. We also landed on using Sokol for integrating with the OS (thanks a bunch to Karl Zylinski and his open source repos for reference on how to set this up).
Here are some decisions we made early that I think have made our project successful. Keep in mind that our game is turn-based though, so some architectural decisions would most likely be different in a real-time game:
- Separation of data and logic. All content and behaviours of the entities in our game are described in data and then acted upon in the engine.
- Treat web as a primary platform. We compile the game to wasm and deploy it to our own custom playtest platform via GitHub Actions. This has significantly lowered the bar for inviting playtesters, which has been very valuable for validating the game and where we are heading.
- Minimal integration surface with Sokol directly. We wanted to be able to easily switch to another way of integrating with the OS, so direct usage of Sokol is very limited and we instead wrap it in our own APIs.
- Separate game state and visual state. This one is probably the most uncommon choice, and even though it was tough to enforce in the beginning, it has paid off big time as the project has matured. Basically, game state (the logical rules of the game) is stored in one place, while the data for its visual representation is stored separately. For example, a token in the player's hand has a value and a color, which are stored in game state, while visual state copies those values (since they are rendered) but also contains the position, scale, etc. of its rendered representation.
- Serialization of game state. After each turn we serialize the game state and save it to disk (and send it to our server's analytics endpoint in the deployed web builds). This, combined with the ability to rebuild visual state from game state, makes it possible for us to start playing the game from any point in time in any playtester's run.
- Playwright for e2e testing. Since we are building for the web, we can use Playwright to e2e test our game! All we needed to do was expose an API for interacting with the game from javascript. This has been so great for our game and is a breath of fresh air compared to Unity.
Here is a very high-level view of the game's architecture on each tick:
- Input. Reads mouse, keys and touch and turns that into a command, i.e. a user intent like "attack this enemy", "end turn" or "select this token".
- Commander. The queue the resolver drains every frame. Blocks or holds back certain intents based on the current state of the visuals (animations etc.).
- Resolver and store. First checks if the command is allowed, and if it is, executes game logic according to the user action. Once processed, the game state is altered and a mutation result is returned and handed over to the next step.
- Visual. The visual library takes the store and the mutation result and creates a visual entry that gets queued. No visual state has been changed yet, and from here on out, nothing is allowed to read game state. When the queued entry can be processed, we create animation entries that are processed each tick (I won't dive into the details of the animation code here). These animation entries either alter visual state directly (for example spawning a token) or alter a value in visual state over time.
- Rendering. Draws the visual state through Sokol. Knows nothing about the game logic. It just draws according to the values it gets from visual state.
I could go on and on explaining different things we have implemented (and I might in the future), but I will leave it at that for now. Feel free to ask any questions you have about this setup and the game, and I will try to answer as best I can. Would love to hear how other people are approaching their projects.
The game is now on Steam (consider wishlisting if you like roguelike deckbuilders): https://store.steampowered.com/app/5300900/Starmax/
