Tutorials > Tutorial n°1: Tutoworld

In this first tutorial, we're going to learn to make a simple but well-featured Particubes solo-game: "Tutoworld"! We'll see the basics of coding a game in Particubes, and we'll explore quite a few important concepts for using the engine like a pro. Here's a little mouth-wathering demo of the finished thing:

As you'll see very soon, this is actually quite straight-forward to do thanks to the various built-ins in Particubes's API!

So, ready to get started? Let's dive in and create our new world :)

Creating a new world

To create a new world of your own, open the Particubes app and, in the main screen, click on the 🏗 Build button at the bottom of the screen:

This brings up the list of worlds you already created, and you can easily switch to your list of items with the top row of buttons:

For now, let's click on the ✨ New world! ✨ button. This makes a new Particubes world, gives it some default map and code and publishes it to the rest of the world. Since you are the author, you can now access its edit page and fill some info like the title and the description, or jump into it to actually edit its data! But because every new world is generated with some sample code that already allows you to move around, jump and explore a little voxel land, you'll see that if you click the Edit button you'll be directly teleported in the new world with some basic controls:

You can read the code that makes all of this possible by pausing the game and clicking the "Edit the code" button. For now, your game runs the following Lua code:

Config = {
    Map = "aduermael.hills"
}

Client.OnStart = function()

    -- Defines a function to drop
    -- the player above the map.
    dropPlayer = function()
        Player.Position = Number3(Map.Width * 0.5, Map.Height + 10, Map.Depth * 0.5) * Map.Scale
        Player.Rotation = { 0, 0, 0 }
        Player.Velocity = { 0, 0, 0 }
    end

    World:AddChild(Player)

    -- Call dropPlayer function:
    dropPlayer()
end

Client.Tick = function(dt)
    -- Game loop, executed ~30 times per second on each client.

    -- Detect if player is falling,
    -- drop it above the map when it happens.
    if Player.Position.Y < -500 then
        dropPlayer()
        Player:TextBubble("💀 Oops!")
    end
end

-- jump function, triggered with Action1
Client.Action1 = function()
    if Player.IsOnGround then
        Player.Velocity.Y = 100
    end
end

--
-- Server code
--

Server.Tick = function(dt) 
    -- Server game loop, executed ~30 times per second on the server.
end

The code is quite self-explanatory for the most part, and you can also check out the Beginner's Guide for a quick peek at the overall structure... but don't worry: throughout the rest of this tutorial, we'll modify or expand on this snippet and gradually dive into its different parts.

Setting the world's map

First of all, let's look at the very first lines of the script:

Config = {
    Map = "aduermael.hills"
}

This Config object is where we set the global parameters of our world, and most importantly the map to use and the items to import. By default, the auto-generated code loads one of the maps made by the team, which is referenced by the code: aduermael.hills.

This is a good opportunity to say two important things about map and items in Particubes:

1. they are all shared to every user in Particubes - this means that, as soon as you know the reference of an asset, you can import and use it in your own game directly
2. all map or item references in Particubes are built with the same format: creator_name.item_name. So, here, aduermael is the creator, and hills is the name of the map to import.

If you want to get started quickly, you can use some of the maps already prepared by our team:

- aduermael.hills
- aduermael.mountains_cabin
- aduermael.lovely_castle
- aduermael.red_planet
- aduermael.rockies
- claire.desert
- claire.market

But, of course, you can also create your own! To do so, just go to the Item editor and assemble voxels to your liking as explained in the Beginner's Guide :)

Note: at this time, there isn't a dedicated map editor yet. So you simply make a map "item", and then import it as the map of your world using the Config object.

For example, in my case, I prepared a small map called minadune.tutoworld_map:

So I can simply update the Config in my "Tutoworld" game to use it instead of the default one:

Config = {
    Map = "minadune.tutoworld_map"
}

And again, since all maps and items are shared, you can use this reference in your game too to use my map!

After updating the code, we just have to publish the changes to push the new game script online and share the new version with everyone (this will instantly relaunch the server for any player currently in the game, including us):

Instantiating items

Now, it's time to populate our scene by adding some objects in this map. This is done in two steps:

1. we import the item by defining its reference in the Config object of our game
2. we use this reference to create an actual copy of the item in the scene: we call this instantiating the item

In my case, I want to import a basic crate I prepared beforehand and that has the minadune.crate reference; so I'll just add a new Items key in my Config and add this reference to the list:

Config = {
    Map = "minadune.tutoworld_map",
    Items = {
        "minadune.crate",
    }
}

Note: although you could in theory list your items directly at the root of the Config object, it's best practice to group them in an inner Items object to make the code more readable ;)

Then, I can create an instance of my item by using the Shape API object and passing it my item's path. I'll put this crate spawning process in a new function to make it easier to re-use:

function spawnCrate()
    local crate = Shape(Items.minadune.crate)
end

We can also set some properties of the instance like its position in the world, its scale, etc. Of course, we're not required to set everything. Here, I'll pass some (X, Y, Z) position to my function to set my crate's position, and I'll scale it slightly:

function spawnCrate(x, y, z)
    local crate = Shape(Items.minadune.crate)
    crate.Position = Number3(x, y, z) * Map.Scale
    crate.Scale = 1.5
end

Finally, we need to make sure that our instance is actually a part of our world! Because, for now, it is just floating about, completely unanchored...

Like most game engines and 3D software, Particubes relies on a system of hierarchy: you have a root node, the world, and then inside you have children nodes like the map, and then inside those children nodes you can have more nodes, and so on.

Right now, our new crate instance isn't parented to any node in this hierarchy, so it's effectively "disconnected" from the game's world. We can solve this by adding it as a child of the World object, or the Map:

function spawnCrate(x, y, z)
    local crate = Shape(Items.minadune.crate)
    crate.Position = Number3(x, y, z) * Map.Scale
    crate.Scale = 1.5
    World:AddChild(crate)
end

Now, we can call this spawnCrate() function from our Client.OnStart() entry point to have it run when the game first starts (I'll place this crate in one of the corners of my map):

Client.OnStart = function()

    -- Defines a function to drop
    -- the player above the map.
    dropPlayer = function()
        Player.Position = Number3(Map.Width * 0.5, Map.Height + 10, Map.Depth * 0.5) * Map.Scale
        Player.Rotation = { 0, 0, 0 }
        Player.Velocity = { 0, 0, 0 }
    end

    World:AddChild(Player)

    -- Call dropPlayer function:
    dropPlayer()

    spawnCrate(2, Map.Height, 2)
end

And if you re-publish, you'll see that there is now a crate on the terrain!

Then, let's simply wrap the instantiation process for the four crates (one in each corner) in a function to better encapsulate this initialisation step:

Client.OnStart = function()

    -- Defines a function to drop
    -- the player above the map.
    dropPlayer = function()
        Player.Position = Number3(Map.Width * 0.5, Map.Height + 10, Map.Depth * 0.5) * Map.Scale
        Player.Rotation = { 0, 0, 0 }
        Player.Velocity = { 0, 0, 0 }
    end

    World:AddChild(Player)

    -- Call dropPlayer function:
    dropPlayer()

    spawnCrates()
end

function spawnCrates()
    spawnCrate(2, Map.Height, 2)
    spawnCrate(Map.Width - 2, Map.Height, 2)
    spawnCrate(Map.Width - 2, Map.Height, Map.Depth - 2)
    spawnCrate(2, Map.Height, Map.Depth - 2)
end

Colliding with the objects

In the last part, we've successully added several instance of our minadune.crate item on the map. However, if you play the game now, you'll notice something pretty annoying: the player can walk through these objects!

This is not very natural, and we should rather make sure the player collides with these crates. In Particubes, this collision system works with CollisionGroups: you can assign objects to one or more groups, and then set which groups the player should collider with. This way, you can have some item on your map block the player, as if she/he was witting an invisible wall when trying to get close to this object.

Note: at the moment, items can only have a box-shaped collision box.

There are 8 possible CollisionGroups, numbered from 1 to 8. So, for example, we can add each new crate to the group n°2 in our spawnCrate() function:

function spawnCrate(x, y, z)
    local crate = Shape(Items.minadune.crate)
    crate.Position = Number3(x, y, z) * Map.Scale
    crate.Scale = 1.5
    crate.CollisionGroups = 2
    World:AddChild(crate)
end

Then, we need to set up the player to compute collisions with this group. By default, it only collides with the CollisionGroup n°1 that contains the map, so that you don't instantly fall to your death... but we can easily add more, like this:

Client.OnStart = function()
    -- Player collides with:
    --  - the Map (1)
    --  - the crates (2)
    Player.CollidesWithGroups = { 1, 2 }

    ...
end

If we re-publish and retry walking towards a crate, our avatar is now stopped as soon as it hits the object :)

Equipping a sword and destroying the crates!

Now that the map is initialised properly, time to actually add some cool action in this game! As shown in the demo above, what I want to do is have a sword in my right hand and use one of the available user actions to "attack" and break the crates.

Luckily, Particubes has lots of nice quick-wins to help us to do all of that :)

First, let's equip a word: we just have to import a matching item by its reference (for example, the aduermael.wooden_sword) and then use the readily available EquipRightHand() method on our Player object:

Config = {
    Map = "minadune.tutoworld_map",
    Items = {
        "aduermael.wooden_sword",
        "minadune.crate",
    }
}

Client.OnStart = function()
    -- Player collides with:
    --  - the Map (1)
    --  - the crates (2)
    Player.CollidesWithGroups = { 1, 2 }

    -- Add weapon in the player's hand
    Player:EquipRightHand(Items.aduermael.wooden_sword)

    ...
end

Tadaa! The avatar now start the game with a sword in its right hand:

The next step is to call another handy function of the Player, SwingRight(), to display a short attack animation when the user triggers a given action.

Particubes has 5 types of action that are totally cross-platform:

- Action1
- Action2
- Action3
- DirectionalPad
- AnalogPad

The DirectionalPad is for the keyboard directional keys, the AnalogPad is for the mouse movements and the Action1, Action2 and Action3 are configured differently on computers and on mobiles:

Computers

- Action1: Space bar
- Action2: Left click
- Action3: Right click

Mobiles

Here, the most intuitive action to use is the Action2; so let's define it and, inside, call Player:SwingRight():

-- attack function, triggered with the Left click
Client.Action2 = function()
    Player:SwingRight()
end

Of course, for now, this "attack" isn't actually doing anything... we need to check if the player is currently facing a crate and close enough when she/he hits the Action1 button. To do this, we have to use the Ray object and do a "raycast": in short, we're going to project a virtual invisible ray from the point of the player's avatar to infinity in its forward direction and look for any "impacts" on objects in the world:

This is easy to do thanks to the Player:CastRay() method, but we need to be careful about a couple of things:

- this raycast will report the impacts on both the crates and the map, but we're only interested in the ones on the crates: this means we'll have to examine the object the ray hit when we get an impact and ignore it if this object is the map
- we also want to limit the "attack range" of the player: we should only be able to destroy the crate if we're somewhat near it

So let's expand our Client.Action2() function with this raycast logic and define a global crateDestroyDistance to set this attack range. Then, to "destroy" the crates, we'll remove them from their parent to take them out of the game:

Client.OnStart = function()
    ...
    -- Global variables
    crateDestroyDistance = 15
end

-- attack function, triggered with the Left click
Client.Action2 = function()
    Player:SwingRight()
    local impact = Player:CastRay()
    if impact ~= nil and impact.Object ~= Map then
        if impact.Distance <= crateDestroyDistance then
            destroyCrate(impact.Object)
        end
    end
end

function destroyCrate(crate)
    crate:RemoveFromParent()
end

Spawning, animating & collecting the gems

Our "Tutoworld" game is starting to look a bit better - but we still can't loot any gem! So let's insure that whenever a crate is destroyed, a gem pops in its place and hovers around to catch the player's attention:

The instantiation process is the same as before: we import the minadune.gem item, instantiate it with the Shape API and set some properties:

Config = {
    Map = "minadune.tutoworld_map",
    Items = {
        "aduermael.wooden_sword",
        "minadune.crate",
        "minadune.gem",
    }
}

function destroyCrate(crate)
    local spawnGem = function(pos)
        local gem = Shape(Items.minadune.gem)
        gem.Position = pos
        gem.Scale = 0.5
        World:AddChild(gem)
    end
    spawnGem(crate.Position)
    crate:RemoveFromParent()
end

But then, how to animate this item? Particubes doesn't yet have too much in terms of animation, so we're going to do it by hand by using the Client.Tick function to gradually update the rotations and positions of the gems currently present in the world. This means that:

- we need to keep track of the gems we create in a table and then iterate through it
- we should also keep track of the current in-game time to create a periodic movement using a trigonometric function

All of this gives us the following code:

Client.OnStart = function()
    ...
    -- Global variables
    crateDestroyDistance = 15

    time = 0
    gems = {}
end

Client.Tick = function(dt)
    -- Game loop, executed ~30 times per second on each client.
    time = time + dt
    for i, gem in ipairs(gems) do
        gem.Position.Y = (Map.Height + 0.5 * math.sin(time)) * Map.Scale.Y
        gem.LocalRotation.Y = gem.LocalRotation.Y + dt
    end
end

function destroyCrate(crate)
    local spawnGem = function(pos)
        local gem = Shape(Items.minadune.gem)
        gem.Position = pos
        gem.Scale = 0.5
        World:AddChild(gem)
        -- add to "gems" table
        table.insert(gems, gem)
    end
    spawnGem(crate.Position)
    crate:RemoveFromParent()
end

Last but not least, let's add some logic to actually collect the gems! Basically, the idea is to look at the position of the player and each gem and, in case they overlap, loot the gem. The engine doesn't have area triggers for now, so we'll use our own collides() function:

Client.OnStart = function()
    ...
    -- Global variables
    kPlayerHeight = 10.5 -- 2.1 map cubes
    kPlayerHalfWidth = 2.0
    kPlayerHalfDepth = 2.0
    
    crateDestroyDistance = 15

    time = 0
    gems = {}
end

-- the engine can't report collisions yet (but it will)
-- implementing simple collision test for this game in
-- the meantime...
function collides(shape)
    local playerMin = Number3(
        Player.Position.X - kPlayerHalfWidth,
        Player.Position.Y,
        Player.Position.Z - kPlayerHalfDepth)
    local playerMax = Number3(
        Player.Position.X + kPlayerHalfWidth,
        Player.Position.Y + kPlayerHeight,
        Player.Position.Z + kPlayerHalfDepth)
    local halfSize = (
        shape.Width > shape.Depth
        and shape.Width * 0.5
        or shape.Depth * 0.5) * shape.LocalScale.X
    local shapeMin = Number3(
        shape.Position.X - halfSize,
        shape.Position.Y,
        shape.Position.Z - halfSize)
    local shapeMax = Number3(
        shape.Position.X + halfSize,
        shape.Position.Y + shape.Height * shape.LocalScale.X,
        shape.Position.Z + halfSize)
    if playerMax.X > shapeMin.X and
        playerMin.X < shapeMax.X and
        playerMax.Y > shapeMin.Y and
        playerMin.Y < shapeMax.Y and
        playerMax.Z > shapeMin.Z and
        playerMin.Z < shapeMax.Z then
        return true
    end
    return false
end

This way, we can very easily check if the player is close of a gem and should loot it. In that case, we'll remove the gem from the game and from the gems table and we'll increase the current score:

Client.OnStart = function()
    ...
    -- Global variables
    crateDestroyDistance = 15

    time = 0
    score = 0
    gems = {}
end

Client.Tick = function(dt)
    -- Game loop, executed ~30 times per second on each client.
    time = time + dt
    for i, gem in ipairs(gems) do
        gem.Position.Y = (Map.Height + 0.5 * math.sin(time)) * Map.Scale.Y
        gem.LocalRotation.Y = gem.LocalRotation.Y + dt

        if collides(gem) then
            pickupGem(i, gem)
        end
    end
end

function pickupGem(i, gem)
    score = score + 1
    table.remove(gems, i)
    gem:RemoveFromParent()
end

And by the way, since we already know that there are exactly 4 gems to collect, we can directly call an endGame() function as soon as our score reaches this value:

function pickupGem(i, gem)
    score = score + 1
    table.remove(gems, i)
    gem:RemoveFromParent()

    if score == 4 then endGame() end
end

function endGame()
end

Adding a basic UI & handling the end of the game

We're almost there! The last things we need to do is add some UI label on our screen to show the score we just computed and design the end screen to let players easily replay our game.

Particubes allows us to create simple interfaces using its UI API. For example, we can create a label and anchor it at the top center of the screen to display our score:

Client.OnStart = function()
    ...
    -- UI variables
    ui = {}
    ui.gemsLabel = UI.Label("", Anchor.HCenter, Anchor.Top)
    ui.gemsLabel.TextColor = Color(0, 0, 0)
    ui.gemsLabel.Text = "Gems:" .. score
end

Note: you are free to organise your UI like you want in the code and you're not required to put everything in a ui table; but it's usually a good idea to try and group related Lua code in a structure like this to have a more readable code :)

Then, to update it when we pickup a new gem, we can simply update the text with the new value of the score at the right time:

function pickupGem(i, gem)
    score = score + 1
    table.remove(gems, i)
    gem:RemoveFromParent()

    ui.gemsLabel.Text = "Gems:" .. score

    if score == 4 then endGame() end
end

Finally, let's implement an end screen with a congratulations message and a "Restart!" button:

Overall, it's just about adding more elements to our UI and enabling or disabling the right ones when need be. But we also have to make sure our pointer is in the proper mode (we can toggle whether it is in "game pointer mode" or a default mouse pointer with the Pointer API, and whether the pointer is a crosshair or not with the UI.Crosshair):

Client.OnStart = function()
    ...
    -- UI variables
    ui = {}
    ui.gemsLabel = UI.Label("", Anchor.HCenter, Anchor.Top)
    ui.gemsLabel.TextColor = Color(0, 0, 0)
    ui.gemsLabel.Text = "Gems:" .. score

    ui.win = {}
    ui.win.message = Label("Victory, you got all the gems!")
    ui.win.restartButton = Button("Restart!")
    ui.win.restartButton.Color = Color(37, 232, 156)
    
    ui.win.message:Remove()
    ui.win.restartButton:Remove()

    -- Switch to in-game pointer
    Pointer:Hide()
    UI.Crosshair = true
end

function endGame()
    -- Switch to default pointer
    Pointer:Show()
    UI.Crosshair = false

    -- Hide score
    ui.gemsLabel:Remove()
    -- Show end screen UI
    ui.win.message:Add(Anchor.HCenter, Anchor.VCenter)
    ui.win.restartButton:Add(Anchor.HCenter, Anchor.VCenter)
end

The last step is to move some of our code to an initGame() function so we can assign it as the callback of the "Restart!" button (on its OnRelease property) and call it when the game first starts - this will give us a consistent behaviour at each retry:

Client.OnStart = function()
    ...
    -- UI variables
    ui = {}
    ui.gemsLabel = UI.Label("", Anchor.HCenter, Anchor.Top)
    ui.gemsLabel.TextColor = Color(0, 0, 0)

    ui.win = {}
    ui.win.message = Label("Victory, you got all the gems!")
    ui.win.restartButton = Button("Restart!")
    ui.win.restartButton.Color = Color(37, 232, 156)
    ui.win.restartButton.OnRelease = initGame

    initGame()
end

function initPlayer()
    Player.Position = Number3(Map.Width * 0.5, Map.Height, Map.Depth * 0.5) * Map.Scale
    Player.Rotation = { 0, 0, 0 }
    Player.Velocity = { 0, 0, 0 }
end

function initGame()
    -- Initialize map with some crates to loot
    spawnCrates()

    -- Place player
    initPlayer()
    World:AddChild(Player, true)

    -- Reset variables
    time = 0
    score = 0
    gems = {}

    -- Setup UI elements
    ui.gemsLabel.Text = "Gems: " .. score
    ui.gemsLabel:Add(Anchor.HCenter, Anchor.Top)
    ui.win.message:Remove()
    ui.win.restartButton:Remove()

    -- Switch to in-game pointer
    Pointer:Hide()
    UI.Crosshair = true
end

Some bonus features

We now have a simple but nice game to play around with: "Tutoworld"! There are little improvements we can add to get an even better user experience, like:

- fixing the time to noon to always have enough light using the TimeCycle and Time objects:

Client.OnStart = function()
    -- Disables night/day cycle
    -- (and initializes at noon)
    TimeCycle.On = false
    Time.Current = Time.Noon

    ...
end

- making sure the player is "re-spawned" on the map if she/he ever jumps out of it:

Client.Tick = function(dt)
    if Player.Position.Y < -100 then
        initPlayer()
    end

    -- Game loop, executed ~30 times per second on each client.
    ...
end

- hiding the player's avatar when the end screen is on, and toggling it back again on restart... and even completely stop it from moving when the game has ended by overriding the Client.DirectionalPad default implementation and "pausing" the player:

function initPlayer()
    ...
    Player.Motion = { 0, 0, 0 }
end

function initGame()
    paused = false
    ...
    Player.IsHidden = false
end

function endGame()
    paused = true
    ...
    Player.IsHidden = true
    Player.Motion = { 0, 0, 0 }
    Player.Velocity = { 0, 0, 0 }
end

Client.DirectionalPad = function(x, y)
    -- storing globals here for AnalogPad
    -- to update Player.Motion
    dpadX = x 
    dpadY = y
    if not paused then
        Player.Motion = (Player.Forward * y + Player.Right * x) * 50
    end
end

Client.AnalogPad = function(dx, dy)
    Player.LocalRotation.Y = Player.LocalRotation.Y + dx * 0.01
    Player.LocalRotation.X = Player.LocalRotation.X + -dy * 0.01

    if dpadX ~= nil and dpadY ~= nil then
        if not paused then
            Player.Motion = (Player.Forward * dpadY + Player.Right * dpadX) * 50
        end
    end
end

Conclusion

In this first tutorial, we made a simple solo-game, "Tutoworld", and we discussed quite a lot of Particubes features, among which: maps and items instantiation, collisions and triggers, raycasts and even a bit of UI!

You can find the game on Particubes (by going to the worlds list and searching for "Tutoworld") or re-create your own and then tweak it to your liking: feel free to share all your creations with us, we'd love to hear from you! :)

Resources & links

API objects used in this tutorial:

- Button
- Client
- CollisionGroup
- Color
- Config
- Impact
- Items
- Label
- Map
- Number3
- Object (indirectly)
- Player
- Pointer
- Ray (indirectly)
- Shape
- Time
- TimeCycle
- UI
- World

Final code

As a reference, here is the final code of "Tutoworld" - remember that you can also check it out directly on Particubes by playing the game and clicking the "Read the code" button!

Config = {
    Map = "minadune.tutoworld_map",
    Items = {
        "aduermael.wooden_sword",
        "minadune.crate",
        "minadune.gem",
    }
}

-- =====================
-- Base client functions
-- =====================
Client.OnStart = function()
    -- Disables night/day cycle
    -- (and initializes at noon)
    TimeCycle.On = false
    Time.Current = Time.Noon

    -- Player collides with:
    --  - the Map (1)
    --  - the crates (2)
    Player.CollidesWithGroups = { 1, 2 }

    -- Add weapon in the player's hand
    Player:EquipRightHand(Items.aduermael.wooden_sword)

    -- Add player to game
    World:AddChild(Player, true)

    -- Global variables
    kPlayerHeight = 10.5 -- 2.1 map cubes
    kPlayerHalfWidth = 2.0
    kPlayerHalfDepth = 2.0

    crateDestroyDistance = 15

    -- UI variables
    ui = {}
    ui.gemsLabel = UI.Label("", Anchor.HCenter, Anchor.Top)
    ui.gemsLabel.TextColor = Color(0, 0, 0)

    ui.win = {}
    ui.win.message = Label("Victory, you got all the gems!")
    ui.win.restartButton = Button("Restart!")
    ui.win.restartButton.Color = Color(37, 232, 156)
    ui.win.restartButton.OnRelease = initGame

    initGame()
end

Client.Tick = function(dt)
    -- Respawn player if out of the map
    if Player.Position.Y < -100 then
        initPlayer()
    end

    -- Game loop, executed ~30 times per second on each client.
    time = time + dt
    for i, gem in ipairs(gems) do
        gem.Position.Y = (Map.Height + 0.5 * math.sin(time)) * Map.Scale.Y
        gem.LocalRotation.Y = gem.LocalRotation.Y + dt

        if collides(gem) then
            pickupGem(i, gem)
        end
    end
end

-- jump function, triggered with the Spacebar
Client.Action1 = function()
    if Player.IsOnGround then
        Player.Velocity.Y = 100
    end
end

-- attack function, triggered with the Left click
Client.Action2 = function()
    Player:SwingRight()
    local impact = Player:CastRay()
    if impact ~= nil and impact.Object ~= Map then
        if impact.Distance <= crateDestroyDistance then
            destroyCrate(impact.Object)
        end
    end
end

Client.DirectionalPad = function(x, y)
    -- storing globals here for AnalogPad
    -- to update Player.Motion
    dpadX = x 
    dpadY = y
    if not paused then
        Player.Motion = (Player.Forward * y + Player.Right * x) * 50
    end
end

Client.AnalogPad = function(dx, dy)
    Player.LocalRotation.Y = Player.LocalRotation.Y + dx * 0.01
    Player.LocalRotation.X = Player.LocalRotation.X + -dy * 0.01

    if dpadX ~= nil and dpadY ~= nil then
        if not paused then
            Player.Motion = (Player.Forward * dpadY + Player.Right * dpadX) * 50
        end
    end
end

-- =================
-- Utility functions
-- =================
function initPlayer()
    Player.Position = Number3(Map.Width * 0.5, Map.Height, Map.Depth * 0.5) * Map.Scale
    Player.Rotation = { 0, 0, 0 }
    Player.Velocity = { 0, 0, 0 }
    Player.Motion = { 0, 0, 0 }
end

function initGame()
    paused = false
    
    -- Initialize map with some crates to loot
    spawnCrates()

    -- Place player
    initPlayer()
    Player.IsHidden = false

    -- Reset variables
    time = 0
    score = 0
    gems = {}

    -- Setup UI elements
    ui.gemsLabel.Text = "Gems: " .. score
    ui.gemsLabel:Add(Anchor.HCenter, Anchor.Top)
    ui.win.message:Remove()
    ui.win.restartButton:Remove()

    Pointer:Hide()
    UI:Crosshair = true
end

function spawnCrate(x, y, z)
    local crate = Shape(Items.minadune.crate)
    crate.Position = Number3(x, y, z) * Map.Scale
    crate.Scale = 1.5
    crate.CollisionGroups = 2
    World:AddChild(crate)
end

function spawnCrates()
    spawnCrate(2, Map.Height, 2)
    spawnCrate(Map.Width - 2, Map.Height, 2)
    spawnCrate(Map.Width - 2, Map.Height, Map.Depth - 2)
    spawnCrate(2, Map.Height, Map.Depth - 2)
end

function destroyCrate(crate)
    local spawnGem = function(pos)
        local gem = Shape(Items.minadune.gem)
        gem.Position = pos
        gem.Scale = 0.5
        World:AddChild(gem)
        table.insert(gems, gem)
    end
    spawnGem(crate.Position)
    crate:RemoveFromParent()
end

function pickupGem(i, gem)
    score = score + 1
    table.remove(gems, i)
    gem:RemoveFromParent()

    ui.gemsLabel.Text = "Gems:" .. score

    if score == 4 then endGame() end
end

function endGame()
    paused = true

    -- switch to pointer mode
    Pointer:Show()
    UI:Crosshair = false

    -- Hide score
    ui.gemsLabel:Remove()
    -- Show end screen UI
    ui.win.message:Add(Anchor.HCenter, Anchor.VCenter)
    ui.win.restartButton:Add(Anchor.HCenter, Anchor.VCenter)

    -- Hide + Stop the player
    Player.IsHidden = true
    Player.Velocity = { 0, 0, 0 }
    Player.Motion = { 0, 0, 0 }
end

-- the engine can't report collisions yet (but it will)
-- implementing simple collision test for this game in
-- the meantime...
function collides(shape)
    local playerMin = Number3(
        Player.Position.X - kPlayerHalfWidth,
        Player.Position.Y,
        Player.Position.Z - kPlayerHalfDepth)
    local playerMax = Number3(
        Player.Position.X + kPlayerHalfWidth,
        Player.Position.Y + kPlayerHeight,
        Player.Position.Z + kPlayerHalfDepth)
    local halfSize = (
        shape.Width > shape.Depth
        and shape.Width * 0.5
        or shape.Depth * 0.5) * shape.LocalScale.X
    local shapeMin = Number3(
        shape.Position.X - halfSize,
        shape.Position.Y,
        shape.Position.Z - halfSize)
    local shapeMax = Number3(
        shape.Position.X + halfSize,
        shape.Position.Y + shape.Height * shape.LocalScale.X,
        shape.Position.Z + halfSize)
    if playerMax.X > shapeMin.X and
        playerMin.X < shapeMax.X and
        playerMax.Y > shapeMin.Y and
        playerMin.Y < shapeMax.Y and
        playerMax.Z > shapeMin.Z and
        playerMin.Z < shapeMax.Z then
        return true
    end
    return false
end