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
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
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
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
- 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
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
Make sure you have set a User Id in Application Settings, as every action now is linked to this user id!
- Click on the Unlock button - the challenge is now unlocked and will show an Activate button
- Click on the Activate button - the challenge is now active and listens for incoming events and shows the goal and the current counter.
- 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 - Change back the user id (should be
1234
) and click on Update to bring back the activate challenges for user1234
- 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
The counter should now show 1/10
. If that is not the case, reload the Playground application.
If it does not work, 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
- Click a couple of times on the Send Event button until you have reached the challenges goal. Clicking 10 times should be enough.
- Once you have reached the goal, the challenges state will change and will show Claim Button
- 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.
- 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.
- 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
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
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
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: