Jump to content

* * * * *
Contents
Welcome to the third (and most fun) series of tutorials.  Now that you know how to use the editor and understand all the important parts of the Lua programming language, we're going to start pulling it all together to implement gameplay.  This lesson demonstrates how to create a simple marble game using the knowledge we've learned in the previous tutorials.

Project Setup

This lesson uses the "Marble Game" project template.  To begin, select the File > Project Manager menu item in the main menu.  Press the New button in the project manager window to open the New Project window.

Attached Image: Image1.jpg

Enter a name for your project and press the OK button to create the project.  Press the OK button in the project manager window to switch to our new project and begin the tutorials.

Select the File > Open menu item in the main menu and choose the "start.map" file to open.  This scene just contains some ground, a directional light, and two models, a ball and a coin.  The ball and coin models don't have any script attached to them, so at this point they don't do anything special.  There is also no camera in the scene, so running the map at this point will just result in a black screen.  This isn't much of a game at this point, so let's see what we can do to make it more fun.

Creating The Player Script

The project already contains scripts for the ball and coin behavior.  However, we're going to create our own scripts from scratch so we can learn step-by-step how everything works.  Use the asset browser to navigate to the "Scripts\Objects\Player" folder.  Right-click anywhere in the bottom panel and select the New > Script popup menu to create a new script file.  Name this file "MyBallPlayer".  This is our custom player script we will work on.

Attached Image: Image4.jpg

Double-click on the new script to open it in the script editor.  The script will already have all the available functions declared but commented out.  Let's get rid of everything and start from scratch.  Select the Edit > Select All menu item in the script editor and press backspace to delete all the text so we can start with a blank page.

Adding a Camera

The first thing we're going to do is add a camera that gets created in our script.  We could also create a camera entity in the editor, but creating it in the script makes things easier.  We'll create the camera in the Start() function so it gets created when the map is loaded.  We will also add an UpdateCamera() function we can call from anywhere to adjust the camera's position so it always points at the ball.  At this code to the "MyBallPlayer.lua" script:

function Script:Start()
--Create a camera
self.camera = Camera:Create()
--Update the camera
self:UpdateCamera()
end

--Adjust the camera orientation relative to the ball
function Script:UpdateCamera()
self.camera:SetRotation(45,0,0)
self.camera:SetPosition(self.entity:GetPosition())
self.camera:Move(0,0,-4)
end

If you're ever not sure what a command does, check the API Reference for a description and examples.

Commands Used

At this point, the script isn't attached to an entity, so if we run the game it won't get used.  We're going to attach this script to our ball model.  Select the ball model by left-clicking on it in the 3D viewport.  Select the Scene tab in the side panel to view the scene panel.  The object's properties will be shown in the lower half of the scene panel:

Attached Image: Image8.jpg

This model already has a couple of properties set up to get it ready for the game.  Under the appearance tab, the diffuse color has been set to blue to make it more interesting.  You can change this color to your own if you wish.  More importantly, under the physics tab, the mass has already been set to 1.0.  By default, objects have zero mass and will not react to physics forces.  By setting the mass to a value greater than zero, we allow the entity to react to physics forces, which we will use to move the ball.

Now we're going to add our new script to the ball model so that our code gets executed.  Select the Script tab in the properties editor.  Next to the Script property, press the button marked with a folder.  This will open a file selection dialog where you can choose the script file "Scripts\Objects\Player\MyBallPlayer.lua:

Attached Image: Image11.jpg

Press the Open button, and the script property will now be set to our custom player script:

Attached Image: Image10.jpg

Press F5 to run the game, and we will now be able to see the scene from the point of view of the camera.

Attached Image: Image6.jpg

Player Movement

The next step is to add controls so the player can move the ball by pressing keys on their keyboard.  We will use the Window:KeyDown() function to check if a key is pressed, and if it is we will apply a force to the ball.  Enter this code into your custom script:

function Script:Start()
--Create a camera
self.camera = Camera:Create()
--Update the camera
self:UpdateCamera()
end

--Adjust the camera orientation relative to the ball
function Script:UpdateCamera()
self.camera:SetRotation(45,0,0)
self.camera:SetPosition(self.entity:GetPosition())
self.camera:Move(0,0,-4)
end

function Script:UpdatePhysics()
--Get the game window
local window = Window:GetCurrent()
--If the 'W' key is pressed add a force to the entity
if window:KeyDown(Key.W) then
self.entity:AddForce(0,0,10,true)
end
end

function Script:UpdateWorld()
--Update the camera each frame
self:UpdateCamera()
end


Run the game, and you will be able to control the ball by pressing the W key.  In fact, you can send it right over the edge into space!

Attached Image: Image2.jpg

Of course, we want to be able to move our ball in all directions, so let's add some more code to handle that:  The code below will allow the ball to roll in all directions using the WASD keys for up, left, back, and right movement:

function Script:Start()
--Create a camera
self.camera = Camera:Create()
--Update the camera
self:UpdateCamera()
end

--Adjust the camera orientation relative to the ball
function Script:UpdateCamera()
self.camera:SetRotation(45,0,0)
self.camera:SetPosition(self.entity:GetPosition())
self.camera:Move(0,0,-4)
end

function Script:UpdatePhysics()
--Get the game window
local window = Window:GetCurrent()
--Add force to the player when a key is pressed
if window:KeyDown(Key.W) then self.entity:AddForce(0,0,10,true) end
if window:KeyDown(Key.A) then self.entity:AddForce(-10,0,0,true) end
if window:KeyDown(Key.D) then self.entity:AddForce(10,0,0,true) end
if window:KeyDown(Key.S) then self.entity:AddForce(0,0,-10,true) end
end

function Script:UpdateWorld()
--Update the camera each frame
self:UpdateCamera()
end

Adding a Powerup

Powerups are a great way to motivate the player to go where you want and reward them for accomplishing a difficult task.  Think about some of the games you have played and the different types of powerups they may have used.  This includes weapons, ammo, special items in your inventory, as well as health packs and even money.  In our game we're going to use the coin to reward the player, and give them a goal of collecting all the coins they can find.

You might even have tried to pick up the coin when we added player controls.  If you did, you would see that the player simply goes right through the coin:

Attached Image: Image15.jpg

Let's take a closer look at the coin model's properties.  Select this object and take a look at the physics tab in the properties editor.  Notice that the collision type if set to "Trigger".  This is important because we want to be able to detect collision between the player and the coin, but we don't want to player to bounce off the coin when they hit.  So right now although the collision between to two is being detected, there's no script that takes any action on that event, so it looks like nothing is happening.

Attached Image: Image17.jpg

We're going to create a custom script for the coin, in the same way we did for the player.  In the asset browser, navigate to the "Scripts\Objects\Coin" folder.  Create a new script here and name it "MyCoin.lua".

Attached Image: Image19.jpg

Switch back to the scene panel and select the Scripts tab in the properties editor.  Press the button marked with a folder icon and choose the script we just created, "Scripts\Objects\Coin\MyCoin.lua".

Attached Image: Image20.jpg

Press the Open button in the file open dialog and our new script will now be attached to the coin model.

Attached Image: Image22.jpg

At this point, our coin script still doesn't do anything, so let's open it in the script editor.  Select the Edit > Select All menu item in the script editor menu to select all text, then press backspace to delete it all.  Now let's add some simple code that makes the coin spin around slowly in place.  This will make it more apparent that the coin is an object we can interact with.  Paste this code into the script editor, run the game, and you will see the coin slowly spinning in place:

function Script:UpdateWorld()
self.entity:Turn(0,Time:GetSpeed(),0,true)
end

Now we will add some code to make our coin react when the player touches it.When the player picks up the coin, we want a few different things to happen.  First, we want a sound to play.  This helps communicate to the player that something good happened, and rewards them for their actions.  We also want to coin to disappear, because it has been "picked up".  Thanks to the simple API Leadwerks uses, this is extremely easy to do with the following code.  Enter this in the MyCoin.lua script, run the game, and try to roll the ball over the coin to pick it up:

function Script:Start()
--Load a sound
self.sound = Sound:Load("Sound/coin.wav")
end

function Script:Collision(entity, position, normal, speed)
--Make sure the sound exists and play it
if self.sound then
self.sound:Play()
end
--Hide the coin because it has been "picked up"
self.entity:Hide()
end

function Script:UpdateWorld()
self.entity:Turn(0,Time:GetSpeed(),0,true)
end

function Script:Release()
--If the sound was loaded, release it now and set the variable to nil
if self.sound then
self.sound:Release()
self.sound = nil
end
end


You can copy and paste the coin model to add more coins to collect. Now we're getting somewhere!:

Attached Image: Image24.jpg

Keeping Score

Although our project is more fun to play now, it still isn't a real game.  There's not really any point to the gameplay, and the player has to count coins themselves to keep track of their score.  So let's build a simple scoring system into our game so the player can see how they are doing.

There are two pieces of information we want to display to the player.  We want to show how many coins the player has collected.  We also want to show how many coins are available in the level.  To do this we will use two global variables which we will call CoinsCollected and TotalCoins.  A global variable can be accessed anywhere in our game, by any script.  Global variables should only be used when we need multiple scripts to access them, and it doesn't make sense to make them part of an object.  Add the following code inside the App:Start() function in the MyCoin.lua script:

--Initialize the 'CoinsCollected' value to 0
CoinsCollected=0
--Initialize the 'TotalCoins' count if it hasn't been already
if TotalCoins==nil then
TotalCoins=0
end
--Increment toe 'TotalCoins" value by 1
TotalCoins=TotalCoins+1

This will set the initial CoinsCollected value to 0.  This function will get called once for each coin in our scene, so we need to initially check to see of the TotalCoins variable has already been initialized, and set it to 0 if it has not.  Then we will add one to the TotalCoins value.  Because this function gets called once for each coin in the scene, the TotalCoins value will end up being equal to the number of coins we can collect.  Neat, huh?

Now we need to make it so touching the coin causes the CoinsCollected value to increase by one.  This can be done easily by adding this line of code at the end of the Collision() function in the "MyCoin.lua" script:

CoinsCollected=CoinsCollected+1

Your finished "MyCoin.lua" script now looks like this:

function Script:Start()
--Load a sound
self.sound = Sound:Load("Sound/coin.wav")
--Initialize the 'CoinsCollected' value to 0
CoinsCollected=0
--Initialize the 'TotalCoins' count if it hasn't been already
if TotalCoins==nil then
TotalCoins=0
end
--Increment toe 'TotalCoins" value by 1
TotalCoins=TotalCoins+1
end

function Script:Collision(entity, position, normal, speed)
--Make sure the sound exists and play it
if self.sound then
self.sound:Play()
end
--Hide the coin because it has been "picked up"
self.entity:Hide()
CoinsCollected=CoinsCollected+1
end

function Script:UpdateWorld()
self.entity:Turn(0,Time:GetSpeed(),0,true)
end

function Script:Release()
--If the sound was loaded, release it now and set the variable to nil
if self.sound then
self.sound:Release()
self.sound = nil
end
end

Finally, we need a way to display the score so the player can see what's happening.  We're going to do this by adding a new function in the "MyBallPlayer.lua" script.  Open that file and add this code to the end of the script, leaving the existing code in place.  The PostRender() function is called after the world is rendered, and allows us to draw 2D text and images on the screen to make a simple HUD (heads-up display).  In this case we will just display the number of coins collected and the total number of coins in the map.  Make sure you add this code to "MyBallPlayer.lua", not to the coin script:

function Script:PostRender(context)
if TotalCoins~=nil and CoinsCollected~=nil then
context:SetBlendMode(Blend.Alpha)
context:DrawText(CoinsCollected.." / "..TotalCoins,context:GetWidth()-30,4)
end
end


Your "MyBallPlayer.lua" script should now look like this:

function Script:Start()
--Create a camera
self.camera = Camera:Create()
--Update the camera
self:UpdateCamera()
end

--Adjust the camera orientation relative to the ball
function Script:UpdateCamera()
self.camera:SetRotation(45,0,0)
self.camera:SetPosition(self.entity:GetPosition())
self.camera:Move(0,0,-4)
end

function Script:UpdatePhysics()
--Get the game window
local window = Window:GetCurrent()
--Add force to the player when a key is pressed
if window:KeyDown(Key.W) then self.entity:AddForce(0,0,10,true) end
if window:KeyDown(Key.A) then self.entity:AddForce(-10,0,0,true) end
if window:KeyDown(Key.D) then self.entity:AddForce(10,0,0,true) end
if window:KeyDown(Key.S) then self.entity:AddForce(0,0,-10,true) end
end

function Script:UpdateWorld()
--Update the camera each frame
self:UpdateCamera()
end

function Script:PostRender(context)
if TotalCoins~=nil and CoinsCollected~=nil then
context:SetBlendMode(Blend.Alpha)
context:DrawText(CoinsCollected.." / "..TotalCoins,context:GetWidth()-30,4)
end
end

Run the game now and you will see the score update as you collect coins.

Attached Image: Image25.jpg

Note that we chose to add the HUD code in the player script and not the coins script.  This is because there is only one player script, but there may be many coins in the map.  It makes more sense to draw the HUD just once rather than drawing it over and over again for each coin in the map.  Also note that we used an "if" statement to make sure that CoinsCollected and TotalCoins are not nil values.  This will allow the game to run even if there are no coins present in a map.

Finally, let's make our HUD a little more "game-like" by adding a custom font.  In the Start function of the "MyBallPlayer.lua" script add this line of code:

self.font = Font:Load("Fonts/Ranchers-Regular.ttf",18)

Now in the same file, replace the Script:PostRender() function with the code below.  This will use a larger cartoonish custom font and display it in the upper right corner, using the width of the rendered text to position it precisely:

function Script:PostRender(context)
--Set the font to a cartoonish style
if self.font~=nil then
context:SetFont(self.font)
end
if TotalCoins~=nil and CoinsCollected~=nil then
--Enable alpha blending
context:SetBlendMode(Blend.Alpha)
--Set drawing color to red
context:SetColor(1,0,0,1)
--Combine our variables into some text
local text = "Coins: "..CoinsCollected.." / "..TotalCoins
--Draw the text in the upper right corner of the screen
context:DrawText(text,context:GetWidth()-context:GetFont():GetTextWidth(text)-4,4)
end
--Restore the default font for drawing
context:SetFont(nil)
end


This will give a better appearance, as shown below:

Attached Image: Image2.jpg

There are many different ways we could have designed this code.  We could have declared the CoinsCollected variable in the player script, or made it part of the player object.  Game programming is all about coming up with creative ways to get the behavior you want.

Changing the Level

We now have a more playable game, but it only consists of one level.  A game should have a definite end point where you "win" but our game is still lacking that.  We will add an end point for the level by using a special trigger script that changes the map.  When the player hits the trigger, the level is complete and a new level will be loaded.

First, let's pick a new material so we can easily distinguish our trigger object from the rest of the scene.  In the asset browser, select the file "Materials\Developer\bluegrid.mat".  The draw a box brush at the top of the map, even with the floor.  This will be the trigger we use to change the map:

Attached Image: Image29.jpg

With the new box still selected, select the Scene tab in the side panel to show the scene panel.  Select the General tab in the properties editor and set the Name value to "Change Map Trigger".  This will make it easy to identify this object in the scene editor.  Select the Script tab in the properties editor and press the folder button to open a script file.  Choose the file "Scripts\Triggers\TriggerChangeMap.lua" and press the OK button tp attach the script.  In the Map Name field, type in "start".  This will cause the "start.map" file to load when the trigger is activated.

Attached Image: Image32.jpg

Now when the player reaches the finishing point of the level, a new level will be loaded automatically.  Go ahead and try it!

Respawning

Our game is almost complete, but there is one situation we have not accounted for.  If the player runs off the edge of the world, there is no way for them to get back to the start without restarting the game.  We want them to be able to make mistakes and keep going, so let's add a mechanism to deal with this when the player "dies".  We will accomplish this by using another trigger script that causes damage to the player.

Create a large box brush underneath your level.  Make sure it is big enough the player will always hit it if they fall into space:

Attached Image: Image38.jpg

With this box still selected, select the Scene tab in the side panel.  Select the General tab in the properties editor and set the name of this object to "Kill Trigger".  Select the Script tab in the properties editor and press button with a folder icon to open a file open dialog.  Select the file "Scripts\Objects\Triggers\TriggerPain.lua".  We want this trigger to kill the player immediately, so set the damage value to 100.

Attached Image: Image36.jpg

If we open the TriggerPain script, we can see how it works.  In the Script:Collision() function, this script will check to see if the colliding entity has a script with a TakeDamage() function declared.  If the function is present, this script will call it, passing its own damage value in as a function argument.  However, our player script does not have a TakeDamage() function, so without further modification this trigger will have no effect.

function Script:Collision(entity, position, normal, speed)
if self.enabled then
if entity.script then
if type(entity.script.TakeDamage)=="function" then
entity.script:TakeDamage(self.damage)
end
end
end
end

Let's open our "MyBallPlayer.lua" script again and make some modifications.  First, we want the player to start with 100 health.  We also want to get the player's starting position so that we can use it later to reposition them if they die.  Add these two lines of code inside the Script:Start() function:

self.health = 100
self.startposition = self.entity:GetPosition()

Now we will add a TakeDamage() function so the pain trigger has something to call. Add this function at the end of the "MyBallPlayer.lua" script:

function Script:TakeDamage(damage)
self.health = self.health - 100
if self.health<=0 then
self.health=100
self.entity:SetPosition(self.startposition)
self.entity:SetRotation(0,0,0)
self.entity:SetVelocity(Vec3(0,0,0))
self.entity:SetOmega(Vec3(0,0,0))
end
end


Our new Script:TakeDamage() function will subtract health from the player when it is called.  If the player's health is less than or equal to zero, then the player will respawn at their original starting position with 100 health.  We also set their rotation, velocity, and omega (rotational velocity) back to zero so they can start the level over and try again.  Go ahead and run the game now.  You should be able to roll the ball off into space and respawn at the start of the map.

Here is the finished "MyBallPlayer.lua" script.  It looks like a lot of code, but because we went through it all bit by bit, you now have a good understand of how it works and what it does:

function Script:Start()
--Create a camera
self.camera = Camera:Create()
--Update the camera
self:UpdateCamera()
--Load a custom font
self.font = Font:Load("Fonts/Ranchers-Regular.ttf",18)
self.health = 100
self.startposition = self.entity:GetPosition()
end

--This function will be called by the pain trigger
function Script:TakeDamage(damage)
self.health = self.health - 100
if self.health<=0 then
self.health=100
self.entity:SetPosition(self.startposition)
self.entity:SetRotation(0,0,0)
self.entity:SetVelocity(Vec3(0,0,0))
self.entity:SetOmega(Vec3(0,0,0))
end
end

--Adjust the camera orientation relative to the ball
function Script:UpdateCamera()
self.camera:SetRotation(45,0,0)
self.camera:SetPosition(self.entity:GetPosition())
self.camera:Move(0,0,-4)
end

function Script:UpdatePhysics()
--Get the game window
local window = Window:GetCurrent()
--Add force to the player when a key is pressed
if window:KeyDown(Key.W) then self.entity:AddForce(0,0,10,true) end
if window:KeyDown(Key.A) then self.entity:AddForce(-10,0,0,true) end
if window:KeyDown(Key.D) then self.entity:AddForce(10,0,0,true) end
if window:KeyDown(Key.S) then self.entity:AddForce(0,0,-10,true) end
end

function Script:UpdateWorld()
--Update the camera each frame
self:UpdateCamera()
end

function Script:PostRender(context)
--Set the font to a cartoonish style
if self.font~=nil then
context:SetFont(self.font)
end
if TotalCoins~=nil and CoinsCollected~=nil then
--Enable alpha blending
context:SetBlendMode(Blend.Alpha)
--Set drawing color to red
context:SetColor(1,0,0,1)
--Combine our variables into some text
local text = "Coins: "..CoinsCollected.." / "..TotalCoins
--Draw the text in the upper right corner of the screen
context:DrawText(text,context:GetWidth()-context:GetFont():GetTextWidth(text)-4,4)
end
--Restore the default font for drawing
context:SetFont(nil)
end

Finishing Touches

Let's add a couple of final touches to finish off our simple game.  First, go to the asset browser and select the "Materials\Effects\invisible.mat" material file.  With the "Kill Trigger" object selected, select the Tools > Paint menu item.  This will give us an invisible trigger the player can still run into, which is a lot better than having a visible floor below the level.

Now let's add a skybox to the scene.  Select the Scene tab in the side panel to show the scene panel.  Select the Root node in the scene editor.  In the properties editor below, press the button next to the Skybox field, with the folder icon.  Choose the TropicalSunnyDay.tex file and press OK.  This will add a nice background to your level.

Attached Image: Image40.jpg

We can add music to the scene with another scripted entity.  Select the Objects tab in the side panel to show the objects panel.  In the top dropdown box choose the "Miscellanous" category.  Now, in the bottom dropdown box choose the "Pivot" object.  Create a pivot anywhere in the scene.  With this object still selected, select the Scene tab to show the scene panel.  In the properties editor below, select the Script tab.  Set the script to "Scripts\Objects\Sound\Noise.lua".  Set the Sound field to "Sound\Music\Move_Forward.wav" and set the Volume field to 25.

Run the game again and you can see how we built up a fun playable little game, one piece at a time.

Attached Image: Image4.jpg

Can you think of any ideas for additional obstacles and levels? Be creative and show off what you make to the community!

Conclusion

Wow, that was a big step!  You've learned how use to the features of Leadwerks Editor, how the Lua programming language works, and now you understand how to make a simple game.  Awesome!


7 Comments

error in "self.camera:SetRotation(45,0,0)"  attempt to index local 'self' (a nil value)

BadMasterUA, on 22 May 2015 - 10:05 AM, said:

error in "self.camera:SetRotation(45,0,0)"  attempt to index local 'self' (a nil value)
What point in the tutorial are you?
I had the same problem because i was calling "self:UpdateCamera()" with only a dot.
I've created a new project from the template but I can't find "basic.map" in the Maps folder, only "start.map"

dknox, on 22 June 2015 - 11:22 AM, said:

I've created a new project from the template but I can't find "basic.map" in the Maps folder, only "start.map"
Thanks, I changed the description above.
hi i have a problem with TriggerPain.lua !
My ball took any dammage so i never respawn and i continue to fall
What can i do to fix the problem ?
Why coins if from them to drive off vanish? And how to make, that they were always visible?