The Big, the Bad and the Data
- Theodoros Doukoulos
- Apr 20, 2024
- 10 min read
Updated: Apr 22, 2024
As a matter of fact I never really fancied running a blog in general. The biggest issue was always the daily workload that I had, thus after work I was always exhausted.
Having said that, I think that a mentality change is always very much welcome.
--
In the current side project that I am developing when the weather is rainy (what a liar, the sun was shining outside) we have quite a bit of data to store.
The biggest issue that we wanted to solve with my partner in crime (yeah it is you Dimitri) was to be able to have a variety of different tools that would be Unity independent since Unity would be our game engine. Also we wanted a solution that would not be violating our architecture and would not cause any breaking changes (cannot stress enough how many times I have seen such) on the domain code.
So here were the requirements that we set:
The project should follow Clean Architecture. .NET version is 8!
There should never be breaking changes for the Domain.
All changes should be communicated into the database.
We need to have a non Unity approach for the data editor.
The non game data editor should be able to run on the Web or Desktop totally independent from the Unity runtime. (Funny thought, that would be amazing to allow the gamers actually edit their data without running the game) SOUNDS AWESOME!!!
Yeah that was already too much to digest as requirements (oh and they are!!!).
But lets dive into these a little more.
I will start with number 4 and I will try to elaborate why building our editor tool within the Unity engine would be a bad decision for us.
First of all I want to say that I definitely not discredit all of the people that use Unity to build some data editor tools. I have build quite a bunch myself in the past and surely they have worked wonderfully. Secondly, everything that we build as programmers has its own requirements. And following these requirements made us want to check a different approach.
This is the structure of our project where we follow clean architecture. This will be a runtime that will be able to run the business logic of our game.
If you want to learn more about the hows and whys of Clean Architecture then please check https://github.com/amantinband/clean-architecture . I highly recommend to pay a look on the videos and different material that Amichai composed, since the material there is of high quality and really well explained.
In our Domain is where you add the business logic and rules of your application. This includes entities (or models lets say), use cases, and any domain-specific logic that represents the core functionality of your software. As you can see we have a couple of folders that are corresponding to features inside our Domain (yeah ok except the Factories and Interfaces folder but that is a discussion for a later blog post).
Lets pick up the TeamEntry from the League folder. One small subrequirement is that we need to be able to reuse the Domain models.
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
#nullable enable
using System;
using System.Collections.Generic;
#endif
using System.ComponentModel.DataAnnotations.Schema;
using ThePool.Domain.UnitEntries;
//// Unity proofed version
namespace ThePool.Domain.League {
public sealed class TeamEntry {
public const int MAXIMUM_TEAM_MEMBERS = 17;
public Guid Id { get; set; }
public bool IsForTest { get; set; }
public string Name { get; set; }
public string ShortCode { get; set; }
public string Description { get; set; }
[NotMapped] public TeamEntrySettings Settings;
[NotMapped] public TeamEntryContents Contents;
[NotMapped] public int TotalTeamMembers => Contents.teamMemberEntries.Count;
[NotMapped] public List<CharacterUnitEntry> TeamMemberEntries => Contents.teamMemberEntries;
[NotMapped] public List<CharacterUnitEntry> StartingTeamMembers => Contents.StartingLineup;
[NotMapped] public TeamEntryStrategy TeamEntryStrategy => Contents.teamStrategy;
public TeamEntry() {
}
public TeamEntry(Guid id, bool isForTest, string name, string shortCode, string description) {
Id = id;
Name = name;
ShortCode = shortCode;
IsForTest = isForTest;
Description = description;
}
}
}
As a matter of fact some may already think "What kind of Frankenstein is this man?". And I would not say they are wrong. Moreover lets start from the beginning.
We need to be able to reuse the Domain models.
In the using statements since we will target our game for the Windows platform we are defining some required libraries. System and System Collections Generic are required by Unity.
Then System.ComponentModel.DataAnnotations.Schema; is required so that we can address which properties we want to be able to flag as [NotMapped] when using EntityFrameworkCore. You may ask "But is EntityFrameworkCore available to be used inside Unity?". Surely not. But the library to be imported so that you can let this model also to also exist inside the Unity project is different. And also we will show later on how we can install it. Furthermore on that, I really hated the idea of having this preprocessor directive above each property to make the model usable inside Unity, not to mention how much I hated the idea of creating NEW Unity Dtos (this would practically mean that any computed property related code should be rewritten EEWWWWWWWwww!!!).
Then we just have the using for a different part of the Domain which is letting us facilitate the CharacterUnitEntries that are practically the characters of the game.
Nothing fancy so far!
So in practice, how would we use this model inside the Unity world? Would we want to have some sort of library? I would rather say no. Building a library with this kind of code is always cumbersome, and I was never fond of dlls that can easily lead to confussions while in development. The answer would be more simple than you think.
So lets go back to the Unity world now.
The very first step that someone has to do is to install https://github.com/GlitchEnzo/NuGetForUnity which can come really really handy when the need to handle different nuget packages is present. One of them is
Always though pay attention to the version of the package since due to the different .net versions anything above 5.0 will not work. Please follow the installation instructions and then you would be ready to install any packages that your project may require. Totally worth it!!!
Next item on the list and fairly important one is https://www.csutil.com/projects/cscore library. It has a big selections of very useful patterns implemented in it and very very useful tools that can be found handy for any Unity project. Once installed we can move on with creating SymLinks. Once we install the library we are able to continue to the next steps.
According to google "A symlink, short for symbolic link, is a special type of file that acts as a reference or pointer to another file or directory in a filesystem. Unlike a hard link, which points directly to the inode of a file or directory, a symbolic link contains the path of the target file or directory.
Symbolic links are often used to create shortcuts or aliases to files or directories, making it easier to access them from different locations in the filesystem. They can be especially useful for organizing files, managing dependencies, or providing convenient access to commonly used resources."
So that is exactly what we are going to do. Both of our projects are living in their own respective repositories. In practice this means that our Domain is living on a different branch of the tree in the filesystem than the unity project.
So as you see from the picture we are able to create SymLink-ed folders. Firstly for the sake of structure we created a folder named LinkedFolders and for part of the game business runtime project that we wanted to include ( in our case the Domain ) we created the folder inside the LinkedFolders "ThePool.Domain" and inside this folder we linked the folders that exist in the Domain Folder of our game server. So this would mean that everything should be working perfectly (said no Theo ever. There were a couple of struggles when tried this approach in the beginning but later on, it is really really smooth).
Important note is that we created the assembly definition required for ThePool.Domain so that we will be able to reference the code we added.
PersonFromTheInternet: All of this setup to just not create an editor in Unity?
Well kinda yeah! As mentioned above there is no compatibility with EFCore and lots of other .net goodness. We want to not start Unity every time we want to edit the game data, and most important of all we want to be able to give our gamers the ability to edit the data as well!
And as a matter of fact you will only need to do this setup once! With a small exception on the linking part since if you have new folders that you want to support inside your unity project you will have to SymLink them as well.

Next part is the editor itself. We will use AvaloniaUI to create an editor for our data.
Wait wait wait ! We are going to create another project again ?
Yes we will need to install on our machine the appropriate tools to be able to develop AvaloniaUI based projects. So please first ensure that you have run the Avalonia installation process first and then you can try to start developing.
Some similarities that you may notice already is the existence of 3 projects from the previous solution. And you would be absolutely right.
The first thing that we need to do is to add the references to the corresponding projects from the game server.
First of all if you decided to follow the same approach and opened your tools to code and setup a project we will need to upgrade the versions of .net on Avalonia project. .net8 we go!
Rebuild the project. Keep in mind that from the referenced projects that you may have, it is required to rebuild them after you have done significant changes.
Once we do, we are kinda ready to move on to a very important step which is dependency injection. Note on that, it would be very beneficial if you would check how dependency injection is being handled in the project from the Clean Architecture template. We will use it in a very similar way. In practice we need to make understand what is our startup project and then we would be ready to go.
So what exactly is happening here and why?
This is the App.axaml.cs file and it is the first place you can start adding your code.
Since the rest of the projects that we had added in our app are utilizing the Microsoft dependency injection framework we will walk in the Avalonia world the same way.
there is also a similar file for the ThePool.Infrastructure but for the sake of simplicity and for the post not to go way to long I will include it in a later post.
So in line 19 of the App.axaml.cs we are calling what to be injected and how. It is there we inject our ViewModels.
What we want to do next is to create a static class that will be able to intake the IServiceProvider and we are doing so on line 21 of the App.axaml.cs.
I have added a small navigation temporary code (definitely not the fancier but it gets the job done... ...for now!)
So in our MainWindow.axaml we show only an AppNavigation view.
Which is a fairly simple axaml.cs file that shows a couple of buttons and renders on the right side whatever is assigned on the ContentViewModel property of its ViewModel. Cool!!
So in the ViewModel what is actually happening is fairly simple.
We have a couple of ViewModels that we would like to work with and a ViewModelBase type property that is going to trigger which view we will see depending on which ViewModel we will assign to it.
In the constructor we initialize the ReactiveCommands that are used to react to the button clicks.
Lets not mind for the NameGeneratorButtonClicked for now (cough erm... it is not ready yet to show it)
But the LeagueGeneratorButtonClicked is what we care for at the moment. So it will be instantiated (through the dependency injection) and injected with the required service that its constructor may need. Then passed on to the ContentViewModel of the AppNavigationViewModel to show its view.
Definitely not the prettiest I may agree, I have a list of things that I want to improve in a later post about this part of the code.
So we would be very close to actually start using our Views and ViewModels.
Lets take a small look on the LeagueGeneratorViewModel.cs.
This is the place where all of the League specific code is living in.
What is the LeagueGenerator as a feature though ?
In our game we have a league where 36 teams of RPG characters are competing against each other. These teams are split into 6 divisions of 6 each, much like the NBA style. And then they will be able to move on with play offs and so on.
The composition of the teams and their characters, on which divisions, and all available information that may exist should be handled in this LeagueGeneratorViewModel.cs. With some smart usage of what we already have in place in our game server Application layer.
That is why we see that the only parameter that we are passing on the constructor is the ISender interface which is our Mediator instance in practice. So this means that every Command and every Query that we may have already introduced in our ThePool.Application layer is definitely applicable for usage in our AvaloniaUI editor.
So this is the command we are using to create a new team. If you are already familiar with Clean Architecture template from before, most likely you know why we are using ErrorOr https://github.com/amantinband/error-or (thanks Amichai for this) and what is the current structure of it.
When we want to actually create a team what is triggered inside our LeagueGeneratorViewModel.cs is the CreateNewTeam method.
We are utilizing in here the Command that we already may have created in our codebase through the Mediator instance to create a new TeamEntry.
This in practice means that we will be able during development to see any kind of validation errors we have hooked in for the aforementioned TeamEntry model. Or even show specific validation error message along the way with a friendly pop up to the user. Super duper Cool!
Then we update the ObservableCollection instance with new EditableTeamEntry entries (we will see on the next post why we need this structure).
AAaaaanddd VOILA!
It is extremely not pretty at the moment. But the scope of the post was not to show how to make things prettier. Also you may notice that the names and the Short names descriptions are not the ones seen in the code. And for sure you have already seen that there is an Edit Team button on each of the Teams. We already have a template in place to render the TeamEntry and its elements. On the next post we will dive a little deeper as of what we did to achieve this. And it is definitely super simple.
And thus our Presenter layer has a new candidate, WOOHOOOO !!!
PersonFromTheInternet: So wait wait, Does this mean our presenter layer here is not the ThePool.API ??
As the Clean Architecture advocates (at least in the template that I have shared) in practice any kind of Initiator of the Application layer functionality could be considered a Presenter client. So our AvaloniaUI editor fulfills this scenario.
And as a matter of fact, do you remember our requirements that we set in the beginning of the post? We just managed to tick one more box. 1 The project should follow Clean Architecture. That was kinda big of a post with a lot of information. Though we are definitely not done yet. Yes surely we showed how we can tick number 4 and number 1. But we have not went into deep dive as of how exactly. Plans for the next posts:
Showcase the entire Avalonia project code for the Teams aka The Big, the Bad and the Data no 2
Showcase how we can integrate the Teams and show them inside of Unity. The Big, the Bad and the Data no 3
Describe next steps. Some more iterations after some Domain Driven Design patterns.
Comments