Open-source Unity Tools and Demo Project (MIT-License)
BGC_Tools and CleanBGCProject
I recently posted to share news about BGC_Tools, our internal utility repository for Unity projects, going public. However, that post was perhaps a little premature and a little brief. In order to facilitate use I knew we really needed to accompany it with a demo project showing some of the features in in use. This post is to introduce CleanBGCProject, a small unity project showing a number of the utilities in use.
But allow me to back up...
Who the fuck do we think we are?
The UC Riverside Brain Game Center (or BGC for short) is a psychology and neuroscience research lab at University of California, Riverside. The BGC's Director is Dr. Aaron Seitz, and we have several post-docs and graduate students. We have a team of 3 full-time programmers (myself and two others), and a few undergrad volunteers, working on support and development for about 8-10 projects. The BGC projects, both current and past, include but are not limited to:
- Vision and Hearing Rehabilitation
- Neuroplasticity and the improvement (or "training") of working memory
- The Design, Development, and Validation of automated cognitive assessments (tests) of memory, inhibitory control, and fluid intelligence
- Electrophysiology studies (EEG and ERP) of the neurocorrelates of some of our training tasks and assessment
- The creation of a psychoacoustics toolbox (P.A.R.T.) for developing and deploying completely shareable and reproducible hearing tests
Development for research is different from working in the games industry in some ways that I find interesting. A given project is almost never "complete". You're always either implementing new features, making highly lateral or incompatible changes, significantly scaling up how many subjects your project can support concurrently, or it's retired because you've created something brand new that does what it did, but better.
Why should you care?
This shit's got the MIT License - who doesn't like free stuff?
Less facetiously, our Unity projects undergo a lot of constant evolution, and we have a number of them, so we've needed to maintain a flexible, shared set of generalized tools. BGC_Tools has been built to work well when added to a Unity project as a submodule in order to make it easy maintain for all the projects it's being used in, and make sure some of the effort we put in for one project can transfer to others. See the "WTF is a Git Submodule?" footnote at the bottom if you use git but don't know what a submodule is.
We've had the opportunity to work with some really clever people, and I'd be delighted if that cleverness could be useful.
NOTE: Behavior in Editor
It has always been important to us to support rapid prototyping and easy testing. If a developer is working on some internal game scene, we want it to be convenient and easy to just press Run and test it. As such, all the systems that will be mentioned have been designed with either well-behaved default fallback behavior, or trivial initialization that can just be slapped into a scene and forgotten about.
The Good Stuff
Once again, the demo project is CleanBGCProject. The releases are here, as well.
What to look forward to:
Player Profiles (BGC.Users.PlayerData)
It's got a system for easily managing multiple user accounts, complete with serialization to JSON. The default behavior in the editor and on desktop is to store player profile data in the project directory, under ./SaveData/, and in an appropriate alternative location on iOS and Android. The format and location make it really easy to inspect, modify, and share. It's not at all obfuscated because that's not a problem for us, though it could be for you. Cheaters... *SMH*
The SettingsMenu (next) as well as both TestA and TestB in CleanBGCProjects makes use of PlayerData. Take a look at the persistent values accessed and written in /TestA/SettingsDemo.cs and /TestB/TestBPanel.cs, specifically the PlayerData references in the Properties defined towards the top of each of the mentioned classes.
Settings Menu (CleanBGCProject: /Core/Settings/SettingsMenu.cs)
This one doesn't live in BGC_Tools yet largely because a number of projects have made nontrivial, individualized changes to it, and it's so easy to add new settings that it's nearly criminal. Settings can be defined with scopes of either Global or User, default values, etc.
TestA in CleanBGCProjects makes a lot of use of the settings controlled by the settings menu. Take a look /TestA/SettingsDemo.cs, specifically SettingsDemo.FocusAcquired() to see where the settings are applied.
SystemInitializer Idiom (CleanBGCProject: /Core/SystemInitializer.cs)
Just a convenient method of slicing up the linear initialization of system elements and being able to use the same initialization method in either less disruptive coroutine chunks (ie Standard launch through the splash screen), or immediately inplace before anything else wakes up in an internal scene (ie when directly launching a scene for testing in the editor).
The Audio System (BGC.Audio.*)
I've had to design and build a fairly sophisticated AudioStream system to support the real-time mathematical generation of auditory stimuli that we use in several of our projects. Working with Unity's AudioClips and AssetBundles to load audio from file was far too constraining, being limited to the main thread and gobbling up memory as they do. Then gaining access to the underlying sample data required passing in a large buffer, doubling up our memory inconveniences. So I read up on the WAV specs and built BGC.Audio.WaveEncoding to load (and save) audio from file in a background thread and have direct access to the data, handling a number of the most common formats WAV encoding formats. Then I began layering Audio filters and effects...
Then, since I was able to synthesize some simple instruments, I figured I might as well go for it and I implemented a simple MIDI renderer (trust me, this was so much more work than I expected going into it). Check out BGC.Audio.Midi.MIDIEncoding to see the entry point, or MidiEncodingTests.cs to see what it looks like to use. You may need to download some midis to substitute in the source for some tests, since I didn't explicitly have a license to redistribute them.
How much code does it take to render "./Test/toccata1.mid" and save the output to a WAV file "./Test/toccata1.wav"?
MidiEncoding.LoadFile(
filePath: DataManagement.PathForDataFile("Test", "toccata1.mid"),
midiFile: out MidiFile midiFile);
WaveEncoding.SaveStream(
filepath: DataManagement.PathForDataFile("Test", "toccata1.wav"),
stream: new MidiFileStream(midiFile).SafeCache().SlowRangeFitter()),
overwrite: true);
I haven't spent much time defining how each instrument should be synthesized, but the tools here are pretty powerful, so BGC.Audio.Midi.Synth.InstrumentLookup should be easy to fill out.
Both TestA and TestB in CleanBGCProject utilize the audio system in fairly obvious ways. Take a look at /TestA/SettingsDemo.cs and /TestB/TestBPanel.cs
Programmatic State Machine (BGC.StateMachine)
We have built a fairly robust and powerful programmatic StateMachine, connecting custom States, Transitions, and TransitionConditions. Some features in one of our projects started off implemented in Unity's animation state machines, but they were too restrictive and slow. A maximum one state transition per frame was causing issues for design.
TestB in CleanBGCProject uses a StateMachine to manage game flow. Check out /TestB/TestBPanel.cs, specifically TestBPanel.ConstructStateMachine(ITestBMessenger messenger), where we simulate constraining access to members through a simple interface.
Action Channels (BGC.MonoUtility.Interaction and BGC.MonoUtility.Interpolation)
What started as an attempt to better manage Coroutines that continuously modify gameobjects (hah!) became BGC.MonoUtility.Interpolation.LerpChannel<T>. Each instance of a LerpChannel is kicked off with a specific duration and can support up to 1, each, of an IContinuousAction<T>, ILerpAction<T>, an Action<T> finishedCallback, and an Action<T> interruptedCallback.
These guys are responsible for the craziness in TestA (SettingsDemo). Take a look at SettingsDemo.KickOffMessAround() to follow the logic. Admittedly, the use case here is simple enough that the instances basically disregard the templated argument (because all the action channels were just attached to SettingsDemo), but I'm confident you'll get the point.
Maths (BGC.Mathematics.*)
Fourier transforms, complex numbers, Bessel functions, prime number factorization, LCM computation. If you need it, here it is. If you don't need it... figure out a way to need it. Math is great. Don't overlook BGC.Audio.Visualization.Spectrogram, which can generate spectrograms, and some other visualizations.
Other Small Utilities
BGC.IO.DataManagementhas been surprisingly useful for streamlining file access across platformsBGC.Utility.ApplicationVersionis a struct handling sematic versioning logicBGC.Utility.Epochgives you the Posix time in Milliseconds- FileBrowser scene in BGC_Tools lets you navigate the local files and view text files. On mobile this is super convenient. (See CleanBGCProject)
But this is so ugly...?
Be that as it may, what we're showing off here is tech completely decoupled from the presentation.
Style and Unity Idioms
This one is going to be more contentious, but I've developed a number of particular preferences for how to work with Unity in ways that minimize errors and unnecessary work while maximizing clarity. Just as in any other programming context, it's important to understand how your various systems are coupled together. Unfortunately, when complex instructions are implemented in serialized delegates in the Unity inspector (even just simple UI callbacks like UnityEngine.UI.Button.onClick()) information and interactions are obfuscated, refactors are obnoxious at best, and rebuilding them after a scene collision or anomolous scene hiccup can be nightmarish. For this reason, we limit inspector-populated delegates to a very limited set of designer-oriented tools.
To build interaction between scripts on static scene elements (like fixed UI), we generally include private serialized fields to capture all the references we'll need in the script, and assign callbacks in the Awake() method. Like so:
public class SettingsInnerPanel : ModePanel
{
[Header("Engine References")]
[SerializeField]
private ModePanelManager settingsPanelManager = null;
[SerializeField]
private SettingsMenu generalSettingsMenu = null;
[SerializeField]
private UserIDMenu userIDMenu = null;
[Header("Settings Menu Tab Buttons")]
[SerializeField]
private Button generalSettingsButton = null;
[SerializeField]
private Button userSelectButton = null;
[Header("Settings Menu UI")]
[SerializeField]
private Button lockButton = null;
// ---Snip---
void Awake()
{
generalSettingsButton.onClick.AddListener(() =>
TabButtonClicked(SettingPanelMode.General));
userSelectButton.onClick.AddListener(() =>
TabButtonClicked(SettingPanelMode.UserSelect));
lockButton.onClick.AddListener(LockPressed);
}
// ---Snip---
}
Using this approach, it is generally really obvious what reference belongs in each serialized field in the inspector, it's easy to confirm that you haven't missed anything, and most refactors just work, or are effectively compiler-checked.
Conclusion
I don't know why I included this section. I don't really have a lot else to say... If you have any questions, feel free to drop them here or shoot me an email.
We are open to pull requests that improve functionality and/or clarity.
Thank you for your time, and I hope something here might be useful for some of you.
Footnotes
WTF is a Git Submodule?
I started writing a summary of what git submodules are and how to use them, but it was exploding into a post bigger than the whole of this one. So I made it into a blog post. I'll be pleased if at least someone finds this useful, but at the very least I'll be able to use it as reference materials for trainees.
The abridged version is this: A git submodule is an excellent and flexible way of managing code that is shared by multiple projects. It is a core feature of git.
Edit: tpyos
Posted Apr 20, 2019, 12:57 AM UTC · edited
Git submodule is really handy. We use that ourselves in our own internal toolkit. I am in the process of moving it over to using the package manager though, that way its even easier to set up and use for the team. Have you had the chance to look at that?
I haven't really looked into using Unity's Package Manager recently. Last I did, admittedly a while ago, it seems the overhead in the form of the extra effort to deploy and maintain them was more than made sense for my team, just 3 developers who all use the git CLI to do everything. It's impossible to overstate how little work is involved in sharing changes to a submodule.
What advantages of the Package Manager motivated you guys to make the switch? There are clear advantages externally to having a UnityStore package to distribute the features, but what of the effects internally?
The deploy and maintainence is really similar. to your current approach. You just link in a package with the git url in the package manifest and it’ll automatically download the git repo and add it as a folder under packages (instead of adding a folder under assets) it’ll automatically lock the latest commit hash as well for you. What you need in your package git repo is that it is set up correctly with assembly definition files and a package manifest.
So difference is mainly that instead of adding a reference to a git submodule in your git settings you add it to your package manifest. Bonus is that it appears outside your assets folder and is read only so people dont unintentionally tamper with things that might affect other projects.
Ahh, cool. That does actually sound like it's become much more convenient. But I do look at the fact that the submodule can be edited within the project as a benefit, providing the context of the project while working on changes. Granted, we also do code reviews for our undergrad interns, and my eyes generally look over changes to our tools repo.
When working with the package manager, do you generally just configure a temporarily project where it's imported as a submodule instead when working on more nuanced changes that require some real integration testing?
Yeah I have a test project with all the packages as sub modules hehe :)