Search Unity

Feedback TERROR SQUID

Discussion in 'Works In Progress - Archive' started by mandarin, Jan 16, 2020.

  1. mandarin

    mandarin

    Joined:
    Dec 8, 2009
    Posts:
    21


    Trailer:


    TERROR SQUID is a game where you create your own bullet hell.

    We turned the genre convention of providing tightly choreographed bullet patterns on its head, and instead put the player in charge of choreographing the bullet patterns. Projectiles orbit the sphere and will always return to the player. How you move dictates the pattern you'll meet a few seconds after. Different patterns requires different ways of planning and reacting. When multiple patterns come together, it can quickly get quite hectic.

    Back in January 2019, we won the Norwegian Championship of Gameplay with TERROR SQUID. One of the prizes was to get the game installed on a physical arcade cabinet at Tilt, a local arcade bar. TERROR SQUID is also the first Norwegian arcade game.

    Today we're working on a release for Nintendo Switch and Steam. Everything is going fine. The game runs stable on Windows, MacOS and the Switch and is getting close to being feature complete.

    We just released beta 3 on our Discord server, and are seeking feedback. The feedback on the previous two betas has been positive. We have a few testers who has been playing the game for 12+ hours each, and have been streaming it on Twitch.

    We're specifically looking for feedback on the bullet patterns.
    - Do you like them? Or do you hate them? Which patterns, and why?
    - How do you like the sequence of the patterns? Could the sequence be improved, how and why?

    Of course, if you find bugs or things that needs fixing, let us know.

    For convenience, I've put the downloads for Beta 3 here.
    Win32 - https://developer.cloud.unity3d.com/share/share.html?shareId=WknUZDpiwB
    Win64 - https://developer.cloud.unity3d.com/share/share.html?shareId=bJI5QHTjDB
    Mac - https://developer.cloud.unity3d.com/share/share.html?shareId=ZJhZ_9pivB

    There is an issue with hot-swapping of game controllers while in the game. The icons in the UI doesn't update. Other than that, hot-swapping should work.

    For beta 4, we're working on:
    • Balancing
    • Improved visuals.
    • New soundtrack.
    • New voice overs.
    • New sound effects.
    • Steam integration. To begin with, only highscores.

    Website: https://terrorsquid.ink

     
  2. mandarin

    mandarin

    Joined:
    Dec 8, 2009
    Posts:
    21
    Strange about that animated gif. It shows up in the preview, but not when posted.
     
  3. mandarin

    mandarin

    Joined:
    Dec 8, 2009
    Posts:
    21
    How I use code generation to auto generate the localization system for TERROR SQUID

    TERROR SQUID is a game where you create your own bullet hell. It’s made in Unity, and will be released on Nintendo Switch and Steam in 2020. The game has been localized for 26 languages. Our localization agency put all of the localized strings in a Google Sheet. The sheet was set up with one column per language, and one row per localized string.

    Design goals
    The design goals for the system are:

    • Must be possible to switch language at runtime, without having to restart the game.
    • Localized strings must support variables. E.g.: “You scored x points”.
    • We should be able to get new content from the Google Sheet into the game with as little effort as possible.
    • The system should be as performant as possible. It needs to load as fast as possible, and switch language as fast as possible.
    • The system should produce as little garbage in memory as possible.

    I have made 5-6 localization systems for other games, and they all had mostly the same requirements. Some of them had to support localization of assets, such as textures, animations and GameObjects. For TERROR SQUID we only had to support localization of strings. While it is possible to make a generic system for handling both strings and assets, I’ve found that generalized systems aren’t very good at anything. They do whatever they’re set to do, but often at the cost of performance and/or API design. If I were to add support for localized assets to TERROR SQUID’s localization system, I would make a sister system that exists side-by-side with the string system, in order to get the best of both worlds.

    Before I started planning, I thought about what worked and what didn’t work with the other localization systems I made, and came to a few conclusions for what to do with this one.

    • I didn’t want to put all the strings in assets, like French strings in french.txt, and English in english.txt. It takes time to load an asset, and it has to be deserialized, which takes even longer, and causes a lot of garbage. The old asset has to be unloaded and any remains cleaned up. That’s not ideal at runtime.
    • If I had put strings in an asset, I would get a new problem; variables in strings. The asset can’t contain any logic, so I would have to load a string, parse it, replace any tags with values, and lastly join the strings. That’s expensive to do at runtime. When supporting variables in strings, you can’t escape the fact that you need to join strings before showing them, but the effects of the problem can be reduced.

    The system
    I looked at what it takes to support variables in strings. I figured that if the localized asset could embed logics, I would write each string like a function, like this:

    Code (CSharp):
    1. string GetScore(int score) => $"You scored {score} points!";
    That way I would separate the API from the contents. It wouldn’t matter what language the system returned, I could still call
    GetScore(123)
    and get the correct localized string. The calling code would never need to know what the current language is, or any other specific details about the localization system or the state it’s in. All it needs to know is which method to call.

    Merging text with logics like this could be done by code generation. That’s easy. All you have to do is write the correct code to a text file, and let Unity do the compiling. But will that solve all our problems? Let’s review our design goals.

    Fast loading time
    When the strings are part of the source code, they’ll get compiled together with the code, and bundled in one of the dll-files. Unity handles the loading of dll’s, and makes sure all code is ready when our game code starts. Convenient!

    Switch language at runtime
    When strings are grouped in separate classes, one for each language, loading a new language is a matter of instantiating the correct class. Easy and fast!

    Produce as little garbage as possible
    The language instance doesn’t need any parsing on load, so all it does is allocate a bit of RAM to make room for the class instance. Each string returned allocates a bit of RAM. Switching language means throwing away the previous language instance, and replace it with a new one. The garbage collector will do a bit of work when cleaning up the old instance. So, compared to loading an asset of serialized strings, this will most likely produce less garbage. Sounds like win!

    Localized strings must support variables
    Check!

    Update content with little effort
    Generating the code can be automated. Deciding what to generate code for is done via parsing the Google Sheet. Getting the Google Sheet can be solved by using third party software. Someone must have made a Unity integration for Google Docs. So, all of this can be automated via editor scripting. It’s just a matter of writing the code and getting it right. Easy!

    Code generation does indeed solve all our problems. Let’s get started!

    Code generation
    So, to make this work, I had to create one cs-file per language, with all the localized strings wrapped in functions with a name that reflects the contents of the string. Here’s a method from the english file:

    Code (CSharp):
    1. public string GetVO_Awesome() {
    2.   return $"Awesome";
    3. }
    The method needs to be generated using only the information returned from the Google Sheet. For this method, I know that it comes from a column called “English”, and a row called “VO_Awesome”. The cell contains only the word “Awesome”. With this information, I created a few simple rules:

    • Use the name of the column to dictate the name of the file. In this case it’s named “EntriesEnglish.cs”.
    • Use the name of the row to dictate the name of the method. Always prefix the name with “Get”, so that the method is named “GetVO_Awesome”.
    • Methods has to be marked as `public string` in order to be publicly available and return a string.
    • Use string interpolation ($) for all strings.

    This is easy for strings without variables. When they contain variables, I have to dig a little deeper.

    Variables in strings
    I needed a way to identify variables in an unambiguous way. In TERROR SQUID we don’t use many special characters. We might use an exclamation mark, or a question mark, so it was safe to decide on using curly brackets for variables, like this:

    You scored {score} points!


    In order to pass a variable via method parameters, it has to be of the correct type. So, I needed a way to describe the variables. The type of variables we needed support for was integers, floats and strings.

    Variables ended up being defined like this:
    You scored {score:i} points!
    for integers.
    Your name is {name:s}
    for strings.
    You are {height:f} meters tall
    for floating points.

    By postfixing the variable with an i, f or s, I have everything I need to generate the code.

    Generating the code
    For each string parsed, I have to figure out if it contains variables or not. For that I use a simple regex to look for matches of the variable pattern, which looks like this:
    private const string patternVariable = @"{(.*?)}";


    The generator is split into three phases. First phase looks for variables. Second phase uses the information about the variables to generate the method signature. The third and last phase generates the method body. Used together, I could easily generate all the necessary classes and methods, both with and without parameters.

    Runtime code
    To make sure any calling code can access all strings, regardless of language, I created an interface for the entries classes. I had all the information I needed to generate the interface. It looks like this:

    Code (CSharp):
    1. namespace TerrorSquid.Localization {
    2.     public interface ILangEntries {
    3.         string GetMainMenu_Play();
    4.         string GetStats_SurvivalFormat(int day, int hour, int minute, int second);
    5.     }
    6. }
    Implementations of the interface gets generated like this:

    Code (CSharp):
    1. namespace TerrorSquid.Localization {
    2.     public class EntriesEnglish : ILangEntries {
    3.         public string GetMainMenu_Play() {
    4.             return $"Play";
    5.         }
    6.         public string GetStats_SurvivalFormat(int day, int hour, int minute, int second) {
    7.             return $"{day}d {hour}h {minute}m {second}s";
    8.         }
    9.     }
    10. }
    To be able to switch between languages in a type safe way, I also generate an enum with all the languages the game supports.

    Code (CSharp):
    1. namespace TerrorSquid.Localization {
    2.     public enum Lang {
    3.         English = 0,
    4.         Norwegian = 1,
    5.         French = 2,
    6.         German = 3,
    7.     }
    8. }
    For displaying the possible languages to toggle between in the UI, I wanted to avoid parsing the enum to string at runtime. That’s why I generated a class with the pre-parsed strings.

    Code (CSharp):
    1. namespace TerrorSquid.Localization {
    2.     public static class LangHelper {
    3.         public const int NUM_LANGUAGES = 26;
    4.         public static readonly string[] LANG_LABELS = {
    5.             "English",
    6.             "Norwegian",
    7.             "French",
    8.             "German",
    9.         };
    10.     }
    11. }
    When using an enum to switch between languages, I also needed a way to know which class to instantiate based on the value of the enum. That could be generated too, like this:

    Code (CSharp):
    1. namespace TerrorSquid.Localization {
    2.     public static class LangEntries {
    3.         public static ILangEntries Create(Lang lang) {
    4.             switch (lang) {
    5.                 case Lang.English: return new EntriesEnglish();
    6.                 case Lang.Norwegian: return new EntriesNorwegian();
    7.                 case Lang.French: return new EntriesFrench();
    8.                 case Lang.German: return new EntriesGerman();
    9.             }
    10.         }
    11.     }
    12. }
    The only thing I didn’t generate was the entry point that binds all of the generated classes together.

    Code (CSharp):
    1. namespace TerrorSquid.Localization {
    2.  
    3.     public delegate void LanguageLoaded(Lang fromLang, Lang toLang);
    4.  
    5.     public static class Loc {
    6.  
    7.         public static ILangEntries entries;
    8.  
    9.         public static Lang                 CurrentLanguage  { get; private set; }
    10.         public static int                  NumLanguages     => LangHelper.NUM_LANGUAGES;
    11.         public static event LanguageLoaded OnLanguageLoaded = (f, t) => {};
    12.  
    13.         public static void SetLanguage(Lang lang) {
    14.             Lang fromLang = CurrentLanguage;
    15.             CurrentLanguage = lang;
    16.             entries = LangEntries.Create(CurrentLanguage);
    17.             OnLanguageLoaded.Invoke(fromLang, lang);
    18.         }
    19.  
    20.         public static string GetLangLabel(Lang lang) => LangHelper.LANG_LABELS[(int)lang];
    21.     }
    22. }
    Of all the runtime code written for the localization system, only 23 are written and maintained by hand. The other 20,173 lines are auto generated from a Google Sheet.

    So far, this is the best localization system I’ve ever written. At least it solves our challenges in possibly the best way it could.

    Hope this is of any inspiration.



    Discord: https://discord.gg/9wsFA4Z
    Website: https://terrorsquid.ink/
    Reddit: https://www.reddit.com/r/TerrorSquid/
     
    hippocoder likes this.
  4. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    Nice breakdown, thanks for sharing! I hope your game is a success.
     
    mandarin likes this.
  5. mandarin

    mandarin

    Joined:
    Dec 8, 2009
    Posts:
    21
    Thanks! So do I. :)

    A guy at Reddit mentioned something called Resource Assemblies, a feature built into .net. Resource assemblies are like code assemblies, except that they can embed both code and serialized assets. Definitely going to try that for the next game!
     
  6. mandarin

    mandarin

    Joined:
    Dec 8, 2009
    Posts:
    21
    Today, we worked on the replay functionality, and finally got it working. Or so the tests shows. The main challenge is that recording a play session will happen at a variable frame rate, and so will the playback of the recording. A recording has to be played in the same speed as the recording was recorded. After a few hours of discussing, scribbling and testing different implementations, we nailed it.

    Now we need to implement the changes and test it thoroughly on PC, Mac and the Switch. We already have a test setup that automates the process. We simply start the test, and the game will play itself with a simple "AI". These tests runs for at least 12 hours to gather enough data to be able to safely conclude that features are in fact correctly implemented.