1
\$\begingroup\$

I'm making an open-source voxel engine, and there's an architectural problem that I would like an answer to. I have come up with 2 different solutions, and would like your opinions on:

When I'm creating a new class for a feature that uses the voxel engine, should the new class only refer to a big re-director class called VoxelWorld that redirects the calls to whatever component is responsible for it (solution 1), OR should the new class directly refer to only the classes it actually needs. Simply put, should the new class refer to the entire voxel engine, or just the parts it needs to?

This may be similar to a monolithic vs microservice problem?

Solution 1: All interactions go through a single entry point (I shall call it the VoxelWorld class). It's a single class, which would redirect all calls to whichever component is responsible for it. Examples of those calls would be for example VoxelWorld.GetVoxelData(position) or VoxelWorld.UnloadChunk(position). These calls would be coming from wherever, maybe a custom class made by the user such as a TerrainDeformer.

The TerrainDeformer would only have a reference to VoxelWorld, and nothing else (except for all the parameters it needs to deform the terrain, such as the deformation range). The TerrainDeformer would simply call VoxelWorld.EditTerrain(listOfModifications), and that's it. The VoxelWorld would be responsible for redirecting the EditTerrain call to whichever class is responsible for editing the terrain, for example, a VoxelWorldEditor.

The VoxelWorld.UnloadChunk call would be redirected to ChunkManager, which is responsible for managing chunks. Here's a picture to better explain it:

Visualization of solution 1

The public VoxelWorld would be the only public class in assembly A. Everything else is internal, meaning they are only visible for other classes inside assembly A. The assembly A is also like a black box, it is mostly (except for the VoxelWorld) hidden from the outside project. It just works, that's not a good ideology, but sometimes a necessary one. Everything in assembly B is easily extensible and public to the user. It contains the code the user would be messing with.

VoxelWorld would essentially by only a redirector of calls. It would have absolutely no logic, only redirections. A function in VoxelWorld.cs could look like this:

public void LoadChunk(int x, int y, int z){
    ChunkManager.LoadChunk(x, y, z);
} 

Solution 2: Everything that the user wants to add refers to only the classes it really needs. Now, forget all that public/internal/redirecting stuff from solution 1, but keep in mind the different classes. This is kind of like the interface segregation principle, but just without the interface part.

The interface-segregation principle (ISP) states that no client [e.g. TerrainDeformer] should be forced to depend on methods it does not use [e.g. LoadChunk]

In solution 1, everything depends on everything. That's the blessing and the curse of solution 1. In solution 2, that's not a problem because a class only depends on what it actually needs.

Visualization of solution 1

Solution 2 is more traditional and maybe cleaner -> easier to maintain. I feel like I like solution 2 more than solution 1, but both seem good. Here's the main benefits and disadvantages:

Solution 1:

+Easy to use API (VoxelWorld.DoWhatever())

-It contains a black box, so it's harder for the user to extend the very core features. (Of course, it wouldn't be an actual black box, this is open source, it's just something that the user shouldn't mess with)

Solution 2:

+Easier to maintain and extend

-More difficult API, everything is scattered so the user has to explicitly know if some feature already exists (in solution 2, the user can just scroll through the suggested functions for VoxelWorld.___())

So, which solution do you recommend, and why? Or is there some third solution that I haven't thought about?

This was quite a long question, but I hope it can help others who might have the same problem. I tried googling but didn't find anything related to this kind of problem, but I didn't really even know what to search for.

\$\endgroup\$
1
\$\begingroup\$

In first case, you are creating God's Facade (combination of God Object and Facade), which sole purpose is convenience. There are many downsides to it, but I want go straight to the elephant in the room: you'll have to build underlying systems first before you can build your facade on top of these. My conclusion here is that facade should be optional and low priority, assuming user is going to use it at all (I personally would prefer more granular approach).

In second case, you are going for default and sensible approach: principle of minimal power. It has no additional costs (besides not saving a little typing here and there), and not being over-burdened during initial phases of development is just good idea.

But there are better ways. Let me introduce you to:

Service Locator

SL is also known as Dependency Injection in some industries (where it is also a gold standard). There is ton of information about it; I am going to review main points, which should be important for gamedev:

  • SL allows to provide different implementations for each "service" (subsystem or other part of engine's API) depending on execution mode: debug, release, testing, headless dedicated server.
  • SL allows mods and other third-party tools to inject and request their own modules. Let users to make their own tools. Let players keep game alive with mods (see e.g. Skyrim).
  • SL allows to make features optional: some features may be missing on current platforms, other features may be excluded from shipping to save space.
| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

When you have just one god-object for your whole voxel engine, then the API will unlikely end up being very "easy to use". When the user types VoxelWorld. into their IDE, they will get confronted with a huge list of methods, with those they would actually use on a regular base somewhere hidden between obscure methods few people ever need.

For that reason it's likely much cleaner and easier for the user if you move most functionality to sub-objects which are obtained through or created by the VoxelWorld class.

I believe that it would in fact makes your documentation more navigable. If I want to know what your engine can do in regards to voxel data generation, then I just need to look at the documentation of VoxelDataGenerator and I have all the methods and properties about this topic in one place.

Note that what you expose as an external interface for the users does not necessarily need to reflect the inner workings of your engine. It is very well possible that some or most of these classes are also just facades which contain no data and only reference internal data structures. Perhaps your engine does not even use chunks internally but stores the whole game world in a k-d tree with dynamic loading and unloading. Then Chunk is actually just a facade which translates to positions in the k-d tree.

It's also not uncommon to have object-oriented frameworks which only expose interfaces to the external world and have all the implementing classes internal. This allows some neat tricks, like having the same object implement two different interfaces. So there might be a IChunkReader and an IChunkWriter which are both an object of the same class Chunk, just presented under different interfaces.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Do you mean that the VoxelWorld wouldn't have any functions at all (not even for redirecting), but only public references to all the components like this? \$\endgroup\$ – Eldemarkki Jun 18 at 16:41
  • \$\begingroup\$ @Eidemarkki No, I would offer methods to obtain these components, perhaps with parameters. This allows you to decide whether to implement them as simple getters or if you want to implement them as more complex factory methods which hide some logic behind them to set up and initialize these components. And I certainly would not expose them as public variables, because then a clueless user could do VoxelWorld.chunkManager = null. Or try to replace them with their own classes, which will then break with your next update because you changed interfaces which are supposed to be internal. \$\endgroup\$ – Philipp Jun 18 at 16:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.