Sign in

Tanks (Unity)

The Survivor Tank Game is a simple open source game created with Unity. In this document we’ll show you how we implemented SCILL into this game using our C# SDK.

The game is pretty simple. You play a little tank, controlled by your keyboard and pressing the space bar fires a bullet. Enemy tanks shoot at you and you must kill them before they kill you. You only have one life but simple upgrades are available to improve your weapon or increase your health level.

While the game is fun, it gets boring very quickly, as the only motivation to play again is the score which is not persisted.

By adding some challenges to the game, users get motivated to play again and again until they have solved all challenges.

Create an app

Create a game in the Admin Panel:

Adding a game in the Admin Panel

Adding a game in the Admin Panel

After that, select the new app from the app selection in the top bar and navigate to “API Keys” to add an API key for the new game:

Adding an api key for our new game

Adding an api key for our new game

Defining events and challenges

Now, that we have set up the app in SCILL, we need to figure our which events we have in the game and what challenges we can create based on those events. We recommend getting started by defining obvious events and challenges for your game to get started. Once you have done that, you’ll figure our other interesting challenges which might require some other events. It’s a bit a back and forth process, but a lot of fun and as it’s an iterative process you can always start with something simple to get more complicated down the road.

SCILL events are preconfigured for you. You can find the complete list of event types in our Event Types list in this documentation.

By digging through the list, we found these events that we can pretty easily define and implement for this game:

Every event has a couple of parameters that can be attached to the event. Some of them are required, others are optional. The documentation describes all those parameters in detail.

Based on those events, some challenges are obvious:

  • Kill 10 enemies
  • Play 10 rounds
  • Achieve at least a score of 200
  • Collect 50 health

Of course, these are simple challenges, but they already improve the game as there are additional objectives than just killing enemies. You can create harder to achieve challenges, especially if you add time constraints like “Kill 10 enemies in 3 minutes”. However, to keep things simple, we just create very simple challenges.

Setting up Challenges

In the Admin Panel, make sure the app is selected from the list and click on “Challenges” in the menu. First, we need to create category. Challenges are organized into categories.

Adding Level 1 category

Adding Level 1 category

You can enable and disable categories. This way, you can create many challenges in advance and unlock them anytime. You could create “Christmas” challenges and unlock them at christmas or you could organize your challenges in seasons and switching them every month. If you add great rewards to some challenges, users will be forced to manage challenges in time which also drives users retention.

Let’s create our first, simple challenge. For this, choose the Challenges tab in the Personal Challenges section of the Admin Panel and click on Add Challenge.

Adding the Kill 10 enemies challenge

Adding the Kill 10 enemies challenge

Name
This is the description/name of the challenge. This is the action plan the user must follow to achieve this challenge. Keep it short and precise.
Category
This is the category the challenge is attached to. In this case, choose the “Level 1” category created before.
Challenge-Type
Choose the event that this challenge will track. The challenge will wait for events coming in of this type and process them. You can further specify which events are processed in the Meta Data section below. We’ll come to that later. In this case we choose the kill-enemy event type.
Repeatable
Challenges have a duration and need to be unlocked/activated. This setting defines what happens if the Challenge has been lost (timed out) or won (achieved within time constraint). If it’s set to be repeatable, it will be reset and available for another round. If it’s not repeatable this challenge will not be available for the user again. In this case we set it to be repeatable.
Difficulty Level
You can set an integer here. You can use this value to sort challenges on your end for example. We set it to 1 in this case.
Live Date
We leave this field blank, which means, we don’t set any date. You can create challenges and assign a date when they should “go live”. I.e. create a Halloween challenge and set to October 31th and this challenge will automatically pop up.
Challenge Icon
You can set any string here. For example you could reference a Sprite image in the Unity Asset Library to be used. As our UI does not have any images, we just leave that blank.
Challenge Icon HD
Same as Challenge Icon, but allows you to set a high res image, for example for some teasers or for Desktop version of your game, while you use the Challenge Icon for the mobile version with a much denser UI.
Duration
Set the time limit for the challenge in minutes. We set it to 24 hours in this case.
Goal
This is the goal as an integer value of the challenge. As we want to have 10 kills, we set it to 10.
Challenge Goal Condition
This can be set to 0 or 1. If 0, than the challenge is achieved if you increment the counter greater or equal than the goal. I.e. we set the goal to 10, and the challenge requires to kill at least 10 enemies, so we set this to 0. We could also create a challenge: “Don’t get hit more than 5 times”, then we would need to set this to 1, because in this case the challenge will only be achieved if we stay below 5.
Reward, Price and XP
These are integer values that you can set and use as you like. We don’t make use of them right now, so just leave them blank.
Meta-Data
We’ll come to that later.

Save the challenge. Congratulations, we just created our first challenge. Time to play around with the challenge in SCILL Playground Application.

SCILL Playground Application

In our Playground application you can play around with challenges and events and see them from your users perspective. It’s a good place to simulate if challenges and events are working as you intended before adding User Interface or code to your application.

Setting up the Playground

On the left side, you need to enter your AppId and API key for the application you just created. And you need to enter a user id. In SCILL we link all events and challenges to these user ids. SCILL does not store any personal data of users nor does SCILL have any user accounts. It’s up to you to set user ids. For testing purposes use 1234. Leave the environment setting to Production, as you only have a production account.

You should see something like this in your browser:

SCILL Playground in Action

SCILL Playground in Action

Setting up a Webhook

First thing we need to do to play around with the Playground application is setting up a Webhook. A Webhook can be set in the Admin Panel for your application and is a URL that you can define that will be called from the SCILL backend whenever a challenge changes. The Playground Application also offers a simple Websocket server, routing incoming Webhocks to listening clients. You can use that Websocket server for development purposes, but you are required to implement/run that yourself if going in production, as our Playground Websocket Server is set up for high traffic or usage and can be changed anytime.

Click on Challenges in the Playground application. Here the Webhook URL is printed for you. You just need to Copy this URL and add set it as the Webhook URL in Admin Panel:

Get back into the Personal Challenges section of Admin Panel, click on Set Webhook (above the challenge list) and click on Add Webhook. This will bring up a dialog asking you for the URL (just copy & paste from the Playground). After that, your Admin Panel should look like this:

Playground Webhook set in the Admin Panel

Playground Webhook set in the Admin Panel

Sending events in Playground

Finally, we can play around in Playground. Click on the Challenges tab in the Playground application. You’ll see the challenges you have created in Admin Panel. You should see a Kill 10 enemies entry there and an unlock button.

Playground Webhook set in the Admin Panel

Playground Webhook set in the Admin Panel

Make sure you have set a User Id in Application Settings, as every action now is linked to this user id!

  1. Click on the Unlock button - the challenge is now unlocked and will show an Activate button
  2. Click on the Activate button - the challenge is now active and listens for incoming events and shows the goal and the current counter.
  3. As a test, change the User Id to 12345 and click on Update. As you can see, the challenges are now reflecting that users state. As this user did not do anything yet, challenges are locked and need to be unlocked
  4. Change back the user id (should be 1234) and click on Update to bring back the activate challenges for user 1234
  5. Now, reload your browser window. The Playground will end up in the same state. That is, because app info is persisted in your browsers localStorage and user data is persistent in the SCILL cloud.

Ok, time for sending some events and checking out if our challenge works.

On the right side, you can see the Event editor. Here, you can define an event and send it to the SCILL backend. From the list, choose kill-enemy event.

Sending a kill-enemy event

Sending a kill-enemy event

If you have setup the Webhook correctly, you should now immediately see progress of the challenge in real time. The counter should now show 1/10. If that is not the case, reload the Playground application. If now shows 1/10 the Websocket connection (pushing changes to the playground application in real time) does not seem to work. Make sure you have set up the correct Webhook URL shown in the Playground application. If the counter did not change, try to send the event again. If that did not help, check out the challenge in Admin Panel and make sure it’s event name is set to kill-enemy and that the challenge is still active (i.e. shows a progress bar).

If nothing works, please contact support.

After sending an event, the Playground application shows some more details about the event. First, it displays the JSON payload used to send an event. You can also see Code generated for you in various programming languages that you can just copy and paste into your code base to send that event.

Second, it shows the response from the server.

Code generated for you to send the event

Code generated for you to send the event

  1. Click a couple of times on the Send Event button until you have reached the challenges goal. Clicking 10 times should be enough.
  2. Once you have reached the goal, the challenges state will change and will show Claim Button
  3. Click on the Claim Button. Now, up to you, but often you now give the user some sort of an reward, being it an award or some experience points. Use the various fields provided in the challenge editor in Admin Panel to set a value that you can use in your application to distinguish rewards.
  4. Once a challenge is claimed, the Webhook is triggered and allows you to either activate the reward in the backend or send it to the client via Websockets to activate the reward in the client itself.
  5. As the challenge is repeatable it will come now back in Unlock state. If the challenge is not repeatable, it will now be gone (for this user id).

Congratulations: You have gone through a full SCILL challenges lifecyle! That was easy, right!

Adding events in Unity

By just clicking around a bit, you were able to set up complex events, created real time challenges and played around with the lifecycle of a challenge.

Now, we want to add events to Unity. Best way to do that is setting up events in the Playground and examine the code generated for us by the Playground application. This way, we don’t miss anything. Of course you can also just check out our events documentation.

Setup SCILL SDK in Unity

First, we need to setup the SCILL SDK. Download the SCILL SDK and add the contents of the ZIP file into your Unity Asset folder. We chose to add them to a SCILL/SDK folder to keep things clean.

As we typically don’t want to expose user IDs and your API key in the client, it’s best to generate access tokens that are required to use SCILLs REST-APIs or the SDK in the backend. But to keep things simple, we use the SCILLBackend class in the client (which you should not do in production!) to create the access token.

We also created a simple SCILLManager behaviour class as a Singleton object that allows us to access SCILL APIs from anywhere in our application.

We created a new empty Game Object in Unity and attached the SCILLManager script to it. Then we used the Inspector to set the API key (which you don’t want to do in production!) and the AppId (that is ok of course).

SCILLManager instance added to the Unity scene

SCILLManager instance added to the Unity scene

The SCILLManager script calls DontDestroyOnLoad so it will be available for the lifetime of the application. You can use it anywhere in your code to send an Event for example:

var metaData = new EventMetaData
{
    damage_amount = enemyShooting.damagePerShot, 
    enemy_character = "tank"
};
SCILLManager.Instance.SendEventAsync("receive-damage", "single", metaData);

Sending Kill Enemy Event

It’s pretty easy to figure out how the game is structured. For example there is a EnemyHealth script that is attached to each enemy in the game. Whenever a players bullet hits an enemy, this script is triggered and reduces the health of the enemy tank. If the enemys health is <= 0 it’s considered to be death, i.e. has been killed.

public void TakeDamage(int amount, Vector3 hitPoint)
    {
        // If the enemy is dead...
        if (isDead)
            // ... no need to take damage so exit the function.
            return;

        // Reduce the current health by the amount of damage sustained.
        currentHealth -= amount;

        // If the current health is less than or equal to zero...
        if (currentHealth <= 0)
        {
            // ... the enemy is dead.
            Death();
        }

        void Death()
        {
            // The enemy is dead.
            isDead = true;

            // Turn the collider into a trigger so shots can pass through it.
            boxCollider.isTrigger = true;

            deathParticles.Play();

            // Play the tank explosion sound effect.
            deathAudio.Play();

            // Increase the score by the enemy's score value.
            ScoreManager.score += scoreValue;

            // After 2 seconds destory the enemy.
            Destroy(gameObject, 3f);

            // Send SCILL kill-enemy event
            var metaData = new EventMetaData {amount = 1, enemy_type = gameObject.name};
            SCILLManager.Instance.SendEventAsync("kill-enemy", "single", metaData);
        }
    }

Wow, that’s been easy to add the event here. If you examine the code a bit more (IDEs Find Usages on TakeDamage) you’ll notice, that this function is not only triggered by Players bullets, but also by TNT lying around or perhaps even by friendly fire. We don’t take care right now, but we should improve that!

There are many ways to solve that. kill-enemy has many properties that you can use to attach additional info to the event. In this case, we just added the enemy_type and amount. kill-enemy also has a player_character property. We could use that field to set it to player, friendly_fire or tnt and specify our challenge to only listen on events that have player as player_character set. We’ll leave that for later.

Sending Achieve Score event

Let’s check out another, very interesting event.

The game already features a score. After each round, we store the current score in SCILL with this simple event adding it to the GameOverManager:

if (ScoreManager.score > 0)
        {
	        metaData = new EventMetaData {score = ScoreManager.score};
	        SCILLManager.Instance.SendEventAsync("achieve-score", "group", metaData);			
        }

Note that in comparison to the event before, we use the group event type and not single. What that basically means is that if you use the single type, any new value triggering a challenge will increment the counter by 1 or the amount set in the challenge meta data. If you use the group setting, the counter of the challenge will be set the value provided by the event. In this case it’s score.

So, the score of the last round can be stored persistently in the SCILL cloud.

Adding SCILL UI

We have created a couple of simple scripts:

The [PersonalChallenges] can be attached to the Content object of a Unity Scroll View like shown here:

Personal Challenges Script added to a Scroll View in Unity

Personal Challenges Script added to a Scroll View in Unity

You need to provide a Prefab of a GameObject that has the SCILLCategoryItem script attached. In the prefab folder of the project we have provided a simple prefab for this. The SCILLCategoryItem script will render it’s name and add childs of a prefab with the SCILLChallengeItem attached. It will use the VerticalLayoutGroup to place one challenge after the other.

The SCILLChallengeItem script will need a couple of UI items to be connected, like shown here:

The challenge prefab

The challenge prefab

So, what happens is this:

Loading challenges

The PersonalChallenges will load the challenges using the SCILLManager class:

    // Start is called before the first frame update
    async void Start()
    {
        var categories = await SCILLManager.Instance.SCILLClient.GetPersonalChallengesAsync();
        _categories = categories;
        foreach (var category in categories)
        {
            var categoryGO = Instantiate(categoryPrefab);
            var categoryItem = categoryGO.GetComponent<SCILLCategoryItem>();
            if (categoryItem)
            {
                categoryItem.Category = category;
            }
            categoryGO.transform.SetParent(transform);
        }

        // Make sure every challenge is unlocked and activated
        foreach (var category in categories)
        {
            foreach (var challenge in category.challenges)
            {
                if (challenge.type == "unlock")
                {
                    await SCILLManager.Instance.SCILLClient.UnlockPersonalChallengeAsync(challenge.challenge_id);
                    await SCILLManager.Instance.SCILLClient.ActivatePersonalChallengeAsync(challenge.challenge_id);
                } else if (challenge.type == "unlocked")
                {
                    await SCILLManager.Instance.SCILLClient.ActivatePersonalChallengeAsync(challenge.challenge_id);
                }
            }
        }

        // Listen to real time updates
        SCILLManager.Instance.OnChallengeWebhookMessage += OnChallengeWebhookMessage;
    }

This will load personal challenges using the SCILLClient API. In this example, we decided to unlock and activate all challenges automatically and not letting the user do that like in the Playground application.

This this, after loading all challenges, we walk through all categories and challenges and unlock them if the are locked and activate them if they are unlocked. This is not the best way to do things, but it works. We plan on adding a feature that allows you to set challenges to “Always Active” in the Admin Panel and will roll out in November 2020.

Then, for each category the Prefab is instantiated and the category is set and added as a child. A VerticalLayoutGroup is added to the GameObject to make sure that all categories are aligned vertically.

Last but not least, PersonalChallenges script attaches a delegate to the OnChallengeWebhookMessage that is triggered whenever a Websocket fires a change in the challenge.

// Listen to real time updates
SCILLManager.Instance.OnChallengeWebhookMessage += OnChallengeWebhookMessage;

The implementation of that function looks like that:

    void OnChallengeWebhookMessage(ChallengeWebhookPayload payload)
    {
        foreach (var category in _categories)
        {
            foreach (var challenge in category.challenges)
            {
                if (challenge.challenge_id == payload.new_challenge.challenge_id)
                {
                    challenge.type = payload.new_challenge.type;
                    challenge.user_challenge_current_score = payload.new_challenge.user_challenge_current_score;
                    challenge.user_challenge_activated_at = payload.new_challenge.user_challenge_activated_at;
                    challenge.user_challenge_unlocked_at = payload.new_challenge.user_challenge_unlocked_at;
                }
            }
        }
    }

As you can see, we just walk through the challenges and update local values with the new once. Of course, we could improve that by creating a Dictionary with the challenge_id but it gets the job done for this simple example.

Rendering Categories and Challenges

The SCILLCategoryItem script has a prefab attached that should be instantiated for each challenge. It will iterate over all challenges, instantiate the prefab with the SCILLChallengeItem script attached and will set the challenge.

void UpdateChallengeList()
{
    foreach (var challenge in _category.challenges)
    {
        var challengeGO = Instantiate(challengePrefab);
        var challengeItem = challengeGO.GetComponent<SCILLChallengeItem>();
        if (challengeItem)
        {
            challengeItem.challenge = challenge;
        }
        challengeGO.transform.SetParent(transform);
    }
}

The SCILLChallengeItem will set values from the Challenge instance to the user interfaces elements like the name and the value of the progress bar. The Challenge prefab uses a VerticalLayoutGroup to show the name and below that the progress of the challenge. If the challenge is not active the progress element will be disabled. This way it’s easy to switch UI between active and inactive challenges.

Once the challenge is in unclaimed type, the challenge has been achieved. So, typically we would now show some sort of UI to collect a reward. In this example we just ignore that and mark the challenge as completed by striking it through.

Final thoughts

You can download/check out the full working example in our Github Repository.

This commit shows all events added to the game. As you can see this is pretty easy to do, especially if you use the SCILLManager approach that exposes a SCILLClient instance as a singleton that you can use everywhere in your code.

The Power of SCILL

The full power of SCILL comes now: We have added events to our game, that are loaded from the SCILL cloud whenever the game starts (or are changed). We can now create more challenges by just adding them in the Admin Panel, and they will show up in the game.

This brings great user retention, because they fell that you care about them and they have something to do all the time they update their app. And you don’t need to create new assets, build a new version, deploying it to your users. They just get it without any app updates.

Because of this, we suggest to send as many events as possible, as this gives you more freedom later in adding challenges.

Also make use of the optional properties and add as many infos as you have. This allows you to create new challenges by specifying filters in the Mata Data section of the Admin Panel. For example, you could create challenges like this:

  • Kill 10 enemies with health smaller than 10.

For this, you would need to add current health as a property in the kill-enemy event, for example as a low-health kill-type property.

References

Here are a couple of references in our documentation for things that we just scratched here: