Save Game Synchronization with iCloud In Godot

*Disclaimer*

This guide is by no means authoritative on this subject. I was satisfied with how the feature was working in my own game and thought I would share the steps I took did to get it working. Hopefully somebody will find this useful, and maybe improve it.

With the release of Godot 3.3 and, plugins finally being available for iOS, I figured this would be a great time to try to get this feature working. The Godot developers have official iOS Plugins for GameCenter, iCloud and StoreKit and they have written official documentation for them.

Wait a minute, there isn’t any documentation for iCloud on that page, what gives? Your observation is correct, and that is what made this task a challenge to complete. Hopefully this guide will be useful as a stepping stone in helping other developers get this feature working in their games.

Apple offers 3 different types of online storage that you can use with your game. Key-Value Storage, Document Storage and Cloudkit. This guide is going to focus on Key-Value Storage to achieve Cloud Saves and Save Game Synchronization. Key-Value Storage does have some limitations to be aware of. Your game will only have 1 megabyte of Key-Value iCloud Storage per user and strings are limited to being 64 bytes.

If you have not already registered to be an Apple developer, you will need to head on over and register, and pay your $100 annual fee. Once you are a registered Apple developer, make an app identifier and select iCloud as one of the app’s capabilities. Click on the edit button to create an iCloud Container URL.

This will not work if you do not have the service set up properly with apple.

Once the App Configuration with Apple is finished we are ready to start working in Godot. Lets create a new project and start by installing the iCloud plugin from its Git Repository. After downloading and extracting the plugin, we need to add it to our project. Inside of our project directory make a “ios/plugins” folder, then drag the iCloud folder that you extracted into the plugins folder.

This is what your project directory will look like with the plugin successfully installed

With the plugin installed we are ready to start writing some code. Make a new scene with the root node just being a standard node and attach a script to it. The very first variable that we are going to declare is an empty reference to store the iCloud Singleton.

var iCloud;

This is now where we need to start planning what kind of data we need to store and synchronize with iCloud. The Key-Value Storage solution has methods that we can use to upload and download individual Key / Value pairs or groups of Key / Value pairs. If hearing that made you start thinking that this sounds almost exactly like a Godot Dictionary, that’s because you are correct.

Instead of managing individual variables we can make a dictionary with all of our data. Things that you typically would want to save would be player progress related. As an example I am going to make a dictionary with HP, XP and Coins. We are also going to need a key in the dictionary to help keep your data synchronized, so I am going to make a Timestamp key too.

var save_data = {
HP = 3,
XP = 100,
Coins = 10,
Timestamp = 0
}

Inside of the node’s ready event we are going to check if iCloud is available and assign the singleton to the blank reference that we created earlier. Because we are dealing with a singleton that may not always be available, I like to keep any function calls that are platform specific inside of an if statement that checks for the current platform.

func _ready():
     if (OS.get_name() == "iOS" && Engine.has_singleton("ICloud")):
          iCloud = Engine.get_singleton("ICloud");

With the iCloud singleton now assigned, we can write save and load functions to send and receive data to and from iCloud. Inside of our _save() function we will call set_key_values and pass it our dictionary to upload it, and after we will synchronize iCloud. _load() will be where we call get_all_key_values to to download data from iCloud and assign it to a temporary variable.

func _save(data):
     if (OS.get_name() == "iOS" && iCloud):
          iCloud.set_key_values(data);
          iCloud.synchronize_key_values();

func _load():
     if (OS.get_name() == "iOS" && iCloud):
          var temp_data = iCloud.get_all_key_values();

Now at this point we have data being uploaded to iCloud, and we are downloading data from iCloud. But we are not doing anything with that data yet, and we have no way of knowing if the data that we just downloaded from iCloud is correct. So at this point we are going to do a little bit of validation.

The timestamp key that we created in our local dictionary is going to be what we use to determine which save file is is the most to date. That key will need to store the timestamp from when it was uploaded. Then when we download the data we can check the time that it was uploaded and compare it with our local data to see which is more recent.

Godot has multiple methods of getting a date that you can compare, but I think that the easiest one is to get a Unix time stamp. Unix and Linux calculate time by counting the number of seconds since the Unix Epoch. This is great for us, that means that the function OS.get_unix_time() returns in integer value, and we can just compare which int is larger. We can also add a print line just so that we can see it worked when we run this on an iPhone.

func _save(data):
     if (data.has("Timestamp"):
          data.Timestamp = OS.get_unix_time();
     if (OS.get_name() == "iOS" && iCloud):
          iCloud.set_key_values(data);
          iCloud.synchronize_key_values();

func _load():
     if (OS.get_name() == "iOS" && iCloud):
          var temp_data = iCloud.get_all_key_values();
          if (temp_data.has("Timestamp"):
               if (temp_data.Timestamp >= save_data.Timestamp):
                    save_data = temp_data;
                    print(save_data);

Now that our save and load functions are finished and we validate which is the most recent save data we just need to call these functions. we can call _load() right after we get the iCloud singleton in the ready function. The easiest spot to put the save function would be in the exit tree function when the app is closed.

func _ready():
     if (OS.get_name() == "iOS" && Engine.has_singleton("iCloud")):
          iCloud = Engine.get_singleton("iCloud");
          _load():

func _exit_tree():
     _save(save_data);

That is all of the code that we need to make this project work. We are now ready to export the project and continue working in Xcode. To export click on Project -> Export -> iOS (Runnable). Make sure that you have installed the build template for iOS, clicked the checkbox to export with the iCloud plugin. Your App Store Team ID and App Bundle Identifier are also required for the feature to work.

These need to be filled out properly or this will not work

The Godot iCloud Plugin doesn’t automatically add the capability in Xcode. We will need to add the capability after opening the project in Xcode. Adding a capability is done by clicking your project name in the view on the left hand side, then click “Signing & Capabilities” near the top. The button to add a capability is now immediately under the “Signing & Capabilities” link. After the capability has been added, you need to check the Key-Value Storage checkbox, and the Cloudkit checkbox which seems to be what activates the link to your container.

I have outlined all of the steps in red.

And that is pretty much it. This is all I have needed to do to get this working, plug in an iPhone or iPad press the play button at the top and keep an eye on the console output.

Keep in mind that the very first time your app is run by a new user get_all_key_values() returns an empty dictionary, and the if statements will validate as false the first time your game is executed (and could cause your game to crash if you don’t check if keys exist before you try to access them). Once you quit your game and data is uploaded to iCloud, subsequent boots should trigger the print command that we put in the example.

The console output the first time I got this working in Captain Skyrunner

The new plugin system for iOS is real cool and pretty easy to use, the developers and contributors to Godot should be proud. Unfortunately there isn’t an official Godot Plugin to do save game sync with Android / Google Play, like there is with iOS. Thankfully there is this plugin that is available, and the documentation is pretty good too, so I don’t think that I need to write a guide for Android.

Until Next Time
– Steven

P.S.
I have done some more work on this and the feature works without needing CloudKit or your CloudKit URL checked when you compile.

Randomly Generated 3d Dungeons in Gamemaker Studio 1.4

While working on new projects, or little experiments / prototypes, I will post footage on my Youtube Channel. Somebody commented on one of these experiments (that I have since abandoned) that they wanted me explain how I created it.

This prototype was a 3d dungeon crawler with randomly generated levels. I had success with getting all of the features working, I ended up giving up on the project because I couldn’t make the game any fun to play. Perhaps somebody else can do something with this concept.

This tutorial is built upon the 3d Dungeon Tutorial that Heartbeast did on his YouTube channel. So you will need to have finished that tutorial as a prerequisite to starting this one. Also, this tutorial will only work with Gamemaker Studio 1.4 as the required functions (the d3d functions) were removed from Gamemaker Studio 2.

This is going to be our starting point

First, I want to get all of these events consolidated. Ben’s tutorial has an event for each key stroke, we can put all of this into a single script named move_player(). The script will have 4 variables to listen for the keystrokes and we will copy the existing code from the key-press events into matching if statements inside of this script.

/// Player Input

// Get Keyboard Input
var up = keyboard_check(vk_up);
var down = keyboard_check(vk_down);
var left = keyboard_check(vk_left);
var right = keyboard_check(vk_right);

if (up)
{
    // Move Forward
    
    var vect_x = lengthdir_x(32, target_dir);
    var vect_y = lengthdir_y(32, target_dir);
    
    // Move Forward
    if (!place_meeting(target_x+vect_x, target_y+vect_y, obj_wall))
    {
        target_x += vect_x;
        target_y += vect_y;
    }
}
if (down)
{
    // Move Backwards
    
    var vect_x = lengthdir_x(32, target_dir);
    var vect_y = lengthdir_y(32, target_dir);
    
    // Move Forward
    if (!place_meeting(target_x-vect_x, target_y-vect_y, obj_wall))
    {
        target_x -= vect_x;
        target_y -= vect_y;
    }
}
if (left)
{
    // Turn Left
    target_dir += 90;
}
if (right)
{
    // Turn Right
    target_dir -= 90;
}

To execute this script, you will need to create a step event for the player object and call the move_player(); script. If you have done this your game should run ALMOST exactly the same as it did before; but, because we are using the keyboard_check functions, these keystrokes will validate constantly causing your turning and moving to be VERY fast. To resolve this we will create turn and movement speed variables inside of the player’s create event to slow this down. With these variables set, we can apply them to our move_player script.

// Set Movement and Turn Speeds
mspeed = 2;
tspeed = 2;

Inside of the move forward and move backwards if blocks, we are moving 32 pixels (1 Tile) at a time, so we can update this to our mspeed variable. Inside of the turning if blocks, we are updating our direction by 90 degrees, change the value with our tspeed. With these simple changes we should have smooth movement.

Smooth as butter

Next we can start to build our enemy. I find it is easier to test enemy objects and their AI in the room that is hand crafted instead of randomly generated. To start we need some sort of enemy sprite that is 32 by 32 pixels. This is because our rooms are 32 pixels tall (and because we need all of our sprites to be a power of 2). You can go to Open Game Art and find any number of sprites that are free to use. When you add the enemy sprite to Gamemaker make sure you change its origin to be the center of the sprite so that it stands properly in the room.

With our sprite selected we can make a new object for our enemy. The enemy is going to need a bunch of the same variables that the player has, so we can make a create event to initialize variables like the target_x, target_y, dir, target_dir, mspeed.

/// Enemy Create Event
enemy_sprite = spr_enemy;
target_x = x;
target_y = y;
dir = 0;
target_dir = 0;
agro = 75;
mspeed = 0.5;

Currently the enemy doesn’t draw itself in the 3d space. If you were to put one of these enemies into the room and run the game you will not see anything yet. To get the enemy drawing itself we need to use a bunch of the d3d functions inside of the enemy’s draw event to get it drawing itself in the 3d perspective.

/// Enemy Draw Event
d3d_transform_set_identity();
d3d_transform_add_rotation_x(90);
d3d_transform_add_translation(x, y, 16)
draw_set_alpha_test(1)
draw_sprite_ext(spr_enemy, 0, 0, 0, 1, 1, 0, c_white, 1);
draw_set_alpha_test(false);
d3d_transform_set_identity();

Here is what we are doing with all of these lines of code.

  • d3d_transform_set_identity needs to be run at the start of the event to render it and render any changes to the object.
  • d3d_transform_add_rotation_x rotates the sprite so that is is standing up right, otherwise it will be drawn horizontally and look like a rug.
  • d3d_transform_add_translation will move the sprite up by 16 pixels so that it is not clipping through the floor.
  • draw_set_alpha_test(1) will have the sprite not draw any alpha pixels, otherwise the alpha will be drawn as white instead of being transparent.
  • After the sprite has been drawn we need to reset the alpha_test back to its default value, and run transform_set_identity again.
A spooky ghost

As you can see there is a little bit of a problem with this. Why is the enemy so flat and the walls are not? The walls are being drawn using d3d_draw_block, which essentially draws a cube with the same sprite on all 6 sides. We can’t do that with our enemy, it would just look silly. So instead we are drawing the 2 dimensional sprite in a 3d space.

Drawing a 2d sprite in a 3d world is sort of like putting a picture frame in a room, it will be incredibly thin when you look at it from the side, and you will only see the actual picture when you are looking at it straight on. A solution is to rotate the sprite on its z axis so that it is always facing the direction of the player. This can be achieved by adding the following line of code right after we rotate the sprite on its X axis.

d3d_transform_add_rotation_z(point_direction(x, y, o_player.x, obj_player.y)+90);

Now the enemy object is always going to be facing the player. This is a similar method to how games like Wolf3d, Spear of Destiny and Doom would draw their objects in 3d space.

Its creepy when its always looking at you

The last thing we need to do with the enemy is add a bit of AI so that it will move towards the player when the player enters its agro range. Don’t feel intimidated because this is a 3d game, the logic for this is still 2 dimensional logic.

To get this to work, we need to check the distance between the enemy and the player, if it is less than the agro range we update the position of the enemy by moving the enemy in the player’s direction by the movement speed.

// Set VectX and VectY
var VectX = 0;
var VectY = 0;

// Get Player Position
var px = o_player.x;
var py = o_player.y;

// Check Distance to Player
var distance_to_player = point_distance(x, y, px, py);

// Check if player is in agro range
if (distance_to_player <= agro)
{
    target_dir = point_direction(x, y, o_player.x, o_player.y);
    VectX = lengthdir_x(mspeed, target_dir);
    VectY = lengthdir_y(mspeed, target_dir);
}

// Update Position
target_x += VectX;
target_y += VectY;

// Move Object
dir = lerp(dir, target_dir, .1);
x = lerp(x, target_x, .1);
y = lerp(y, target_y, .1);
Moving straight into my nightmares

Now that our enemy has a very basic AI, we can build the randomly generated levels. The levels generation will be handled by a new object. The logic and code is going to be nearly identical to the random walk from the Isometric Tutorial.

Start by setting the width and height of the room, then get the number of tiles in the room by dividing that by the Cell width and cell height (32 in this case). Use those values to create the DS_GRID, fill the DS_Grid with void.

// Define constants
cell_height = 32;
cell_width = 32;
FLOOR = -5
WALL = -6
VOID = -7

// Set room size
room_width = 1024;
room_height = 768;

// Set the grid width and height
var width = room_width div cell_width;
var height = room_height div cell_height;

// Create the grid
grid = ds_grid_create(width, height);

// Fill grid with void
ds_grid_set_region(grid, 0, 0, width-1, height-1, VOID);

// Randomize
randomize();

Create the controller vector, set it in the middle of the ds_grid, give it the odds to change direction and run a loop to carve out the floor.

// Create the controller in the centre of the grid
var cx = width div 2;
var cy = height div 2;

// Give the controller a random direction
var cdir = irandom(3);
first_move = cdir;

// Place floor at controller position
grid[# cx, cy] = FLOOR;

// The odds of moving direction
var odds = 1;

// Create the level with 400 steps
repeat(400)
{
    // Move the controller
    var xdir = lengthdir_x(1, cdir*90);
    var ydir = lengthdir_y(1, cdir*90);
    cx += xdir;
    cy += ydir;

    // Clamp the controller to the grid
    cx = clamp(cx, 1, width -2);
    cy = clamp(cy, 1, height -2);
    
    // Place floor at controller position
    grid[# cx, cy] = FLOOR;
    
    // Randomize the direction of the controller
    if (irandom(odds) == odds)
        cdir = irandom(3);
}

After the floor tiles have been assigned you need to run another loop to assign walls, and another after that to spawn the walls

// Create the Walls
for (var yy = 1; yy < height -1; yy++)
{
    for (var xx = 1; xx < width -1; xx++)
    {
        if (grid[# xx, yy] == FLOOR)
        {
            // Assign walls
            if (grid[# xx+1, yy] != FLOOR) grid[# xx+1, yy] = WALL;
            if (grid[# xx-1, yy] != FLOOR) grid[# xx-1, yy] = WALL;
            if (grid[# xx, yy+1] != FLOOR) grid[# xx, yy+1] = WALL;
            if (grid[# xx, yy-1] != FLOOR) grid[# xx, yy-1] = WALL;
        }
    }
}

// Draw the level (Spawn Game Objects)
for (var yy = 0; yy < height; yy++)
{
    for (var xx = 0; xx < width; xx++)
    {
        // Spawn Walls
        if (grid[# xx, yy] == WALL)
            instance_create(xx * cell_width, yy * cell_height, obj_wall);
    }
}

Finally you need to spawn the player object and the ceiling and floor object.

instance_create(room_width div 2, room_height div 2, obj_ceiling_and_floor);
instance_create((room_width div 2) + 16, (room_height div 2) + 16, obj_player);

If you create a new room, add this object to it and then run the new room, you should get a randomly generated 3d level. The code doesn’t spawn enemies yet so it will be completely empty of enemies.

Different every time

To spawn the enemies into the level randomly we are going to create a shuffle bag. The shuffle bag is going to use a DS_LIST. We can initialize the DS_LIST right after we initialize the DS_GRID. We also need a counter for our floors and the number of enemies that we want to spawn.

// Create the enemy bag
enemy_bag = ds_list_create();
var floortiles = 0;
var enemycount = 20;

We need to have the exact amount of floor tiles to know how many items to add into our bag. Thankfully we are already keeping track of where our floor tiles are. When we assign the walls, we have an if statement that checks if the current tile is a floor. Inside of that if statement we just need to run floortiles++.

Now that we have the number of floor tiles in the room we add enemy flags and false flags into the bag. The total number of flags in the bag needs to equal the number of floor tiles in the map. We add these flags with a for loop. After everything has been added to the bag, we shuffle it.

// Add enemies into the enemy bag
for (var i = 0; i < floortiles; i++)
{
    if (i<= enemycount)
        ds_list_add(enemy_bag, true);
    else
        ds_list_add(enemy_bag, false);
}

// Shuffile Enemy Locations
ds_list_shuffle(enemy_bag);

Now while we are spawning the wall objects into the room we can check if our for loop is on a floor and check if our bag has an enemy inside of it or not. After you check the bag you need to delete the top item from the bag.

// Spawn enemies
else if (grid[# xx, yy] == FLOOR)
{
    if (enemy_bag[| 0])
        instance_create (xx * cell_width, yy * cell_height, obj_enemy);
    ds_list_delete(enemy_bag, 0);
}
Multiplying like rabbits

With that we have a randomly generated 3d dungeon with enemies spawning that will chase the player when he gets too close. Currently the enemies ignore collisions with the walls. You can add collisions on the enemies by using the place_meeting if statement from the player’s move code.

My minimap tutorial works with this system because we are using a ds_grid to generate the levels, you can try to add that system here as an extra challenge to yourself.

Until Next Time
– Steven