For some genres, keeping the core of your game as pure code, and using the 3D engine only as a presentation layer and UI system can have big advantages.
If you've been following the development of Hexahedra so far on Twitch, you'll know that I've just been opening up Visual Studio and coding pure C#. The client will eventually be built in Unity, but the core puzzle simulation - setting up a factory with multiple workstations, adding devices and commands to them, and running the simulation to see if the setup solves the puzzle - is built into a DLL called HexSim. Although not all the gameplay features of Hexahedra have been built yet, you can "play" it as long as you're prepared to write your solutions in JSON or as NUnit tests.
The first #Hexahedra milestone has gone on the Issue Spike! We can now set up a puzzle and feed in the solution from JSON from a standing start. Now it's time to start working on the game client so you can play the game without running Visual Studio! #gamedev #indiedev pic.twitter.com/nYusv37SYR— Chris Knowles (@Tarrenam) February 26, 2020
Yes, sticking completed issues on the spike is very satisfying
So, why am I taking this approach? Although it's not a solution that will work for every game (more on that later), there are several benefits:
Clearer logic flow. If part of the fabric weaving your game together is a set of
GameObjects with attached scripts, it can make reasoning about the flow of information through your code difficult. If communication between different elements of your game is mediated by Unity (or another engine), then issues can be much harder to track down.
Indeed, a lot of Unity tutorials can end up teaching bad practices where too much hangs on attaching multiple scripts to one object, and using lots of
GameObject.GetComponent<Thing> calls to get them to talk to each other rather than thinking about writing encapsulated, decoupled code.
Unity doesn't have to be that way, of course, but even a diligent approach to using it cleanly won't have all the benefits of a pure C# codebase.
Easier unit testing. Another major advantage of not wiring your game together in the engine is that writing code tests is much easier. Mocking other parts of the game is much more straightforward.
Integration testing is much easier, too. I've been able to write tests that set up a puzzle and a solution from JSON, and run the simulation to solve the puzzle, as a full end-to-end test without going anywhere near Unity. This kind of testing would be much trickier if the engine was involved.
Supporting a simulation-only backend server. Part of Hexahedra will be a backend server that will track records for the most efficient solutions (in multiple categories) for each level, as well as aggregated stats on personal bests so that it can show players how their solution lines up with those submitted by others. To be able to do this it needs to be able to receive a solution from a client and run it itself to make sure that the solution is actually valid - does it actually solve the puzzle, does it honour the particular limitations of the puzzle (rather than, say, using a device that's not supposed to be available) - and check the solution's stats. Allowing players to self-certify their performance is asking for trouble.
By having the core simulation run outside Unity, the server itself can be a simple .NET Core program (with some extra bits to handle requests, talk to the database, etc), and it can run the simulation multiple times in parallel if necessary as solutions come in, and it can do so as quickly as it's able rather than waiting for the solution to play out inside Unity.
Writing the core of your game outside the engine allows you to reuse it in places where the engine is unnecessary.
All that said, this approach works well for a puzzle game but wouldn't work for other genres. Depending on your game, this might not be the approach for you.
No physics. This is the major one. Hexahedra doesn't use the Unity physics engine for any part of the puzzle simulation. No doubt I'll end up using it for visual cues, but the question of "does this setup solve the puzzle?" doesn't require any physics at all. If you're making a racing game or something, you're probably going to be depending pretty hard on your engine to do your physics and update your game state, so you'll need to plug much more of your game into the engine directly.
Unity in particular doesn't make it easy to get into the guts of the physics engine separately to the rest of the framework. But even if you overcome that with a different physics library, you'll still have lost much of the benefit of clear logic flow.
No realtime input. In Hexahedra you set up your solution and watch it run. If the game required any input during a run, modelling it in this way wouldn't be impossible, but it would take a bit more effort, we'd need a way to feed it inputs at the appropriate time. It's doable, but you might want to consider whether this is creating more work than it saves. If nothing else, the question of whether your realtime controls feel right is going to involve a lot of actual playtesting, not running test suites.
Getting information out. Encapsulation is a good thing, of course, with inner workings of things kept hidden away from areas of the code that don't need to know about them. But on some level, Unity is going to need to know about almost every part of a Hexahedra factory, because it's going to need to show something on the screen about it. So I need some way to extract information from the C# codebase that Unity can understand so that it knows how to show what's going on in there. The trick is to do so without just exposing every last detail as a public field.
My approach for Hexahedra is to get HexSim to return a list of
HexEvents for every state update the factory goes through. These are things like
TriggerDevice which will tell Unity when a device has been used and needs to run animations, and
RemovePanel which means that a cube's face has been removed by a device. These things allow Unity to keep up with the game state without simply scouring every public property for the state after every update. This means I can change how HexSim works internally without affecting Unity as long as the
HexEvents keep flowing.
Since changes to the factory state by the player will happen through the UI in the Unity client, knowing how to change the visual state of the game in response to player input (e.g. adding a device) is straightforward, we just might look for HexSim to give a
false response to the requested change to make sure it's valid. And I can build up the initial state of Unity's representation of the factory from the same JSON I use for the initial state of the simulation.
So, if you decide to go with the pure code simulation approach, make sure you don't undo all your good work for the cause of clean code when you come to pull things out for the engine.
This approach won't work for everything, but if you're making a game that doesn't need to be wired directly into its 3D engine, consider building as much as you can outside of it.