Lua Explained - creating a simple minigame step-by-step

Qmicozium

Little Mouse
Hello,
Today we're going to make a simple minigame with full explaination of every line of the code.
This tutorial is meant for people that have some basic knowledge about Lua or programming in general.

So, we'll make a minigame similar to survivor but cannonballs will be spawned automaticly. A difficulty level will grow slowly. Each player will have 2 lives and the last mouse standing will be the winner.
Let's start!

First of all we need a map on which players will be playing. In Lua we can load a new map using a @MapCode or using an XML code.
I made a supersimple map and I'm going to load its XML code. A function that loads a new map is called tfm.exec.newGame().

code_language.lua:
tfm.exec.newGame('<C><P F="1" /><Z><S><S L="800" P="0,0,0.3,0.2,0,0,0,0" T="6" Y="400" X="400" H="50" /><S L="10" o="fffffffff" P="0,0,0,0,0,0,0,0" T="12" Y="-367" c="3" X="550" H="1500" /></S><D><DS X="737" Y="361" /></D><O /></Z></C>')
After executing this code, our map will load.
Now, we need to spawn the cannonballs. We want to spawn them on the left side of the screen and they should go right.
A function that spawns a shaman object is called tfm.exec.addShamanObject(), it also has some arguments inside of ( ) - first of them is an ID of the object.
ID of a cannonball is 17 (https://mforum.ist/threads/lua-documentation.16).
Another 2 arguments are X position and Y position, look at the picture down here to understand how does it work:

.axis.png


And we want to spawn this cannonball somewhere over there:

mf2.png

So X should be ~50 and Y should be ~220.
Spawning a cannonball on this position will look like that:
code_language.lua:
tfm.exec.addShamanObject(17, 50, 220)
This code works fine, it spawns a cannonball where we want to but it's facing up.
To make it go right, we'll use 4th argument of this function - angle. We need to rotate a cannonball 90 degrees:
code_language.lua:
tfm.exec.addShamanObject(17, 50, 220, 90)

Okay, it was quite easy.
Now we need to spawn this cannonball every 0.5 second and the only thing we need to do is insert this function inside of an eventLoop function:
code_language.lua:
function eventLoop()
  tfm.exec.addShamanObject(17, 50, 220, 90)
end
eventLoop is a function that executes itself every 0.5 second so this code will spawn the cannonballs every 0.5 second.
It works fine, but the game should start from something easier, at the beggining we should spawn the cannonballs like every 2 seconds.

To do this we need this code:
code_language.lua:
local spawnDelay = 2
local boostDelay = 5
local toSpawn = 0
local toBoost = 0

function eventLoop()
  if (toBoost == boostDelay and spawnDelay > 0.5) then
    spawnDelay = spawnDelay - 0.5
    toBoost = 0
  end

  if (toSpawn == 0) then
    tfm.exec.addShamanObject(17, 50, 220, 90)
    toBoost = toBoost + 1
    toSpawn = spawnDelay
  end

  toSpawn = toSpawn - 0.5
end
And I think this is the most complicated piece of code in this tutorial.
At the beggining of the code we have 4 variables:
  • spawnDelay - delay between cannonballs, set to 2 (seconds) by default
  • boostDelay - number of cannonballs that need to be spawned, to decrease spawnDelay
  • toSpawn - counter nr. 1 used in our code, we use it to check how many seconds left to spawn another cannonball
  • toBoost - counter nr. 2 used in our code, we use it to check how many cannonballs left to decrease spawnDelay

Rest of the code you can analyze yourself, you don't need any programming knowledge to understand it.
The code describes itself, don't worry if you don't undestand it straightaway. Spend some more time on it.
You can print out some values, it can help. I did it for you, so you can use this code for analyzing purposes:

code_language.lua:
local spawnDelay = 2
local boostDelay = 5
local toSpawn = 0
local toBoost = 0

function eventLoop()
  if (toBoost == boostDelay and spawnDelay > 0.5) then
    print('<vp>I\'m decresing spawnDelay!</vp>')
    spawnDelay = spawnDelay - 0.5
    toBoost = 0
  end

  if (toSpawn == 0) then
    tfm.exec.addShamanObject(17, 50, 220, 90)
    toBoost = toBoost + 1
    toSpawn = spawnDelay
    print('<rose>spawnDelay = ' .. spawnDelay .. ' (' .. boostDelay-toBoost .. ' cannonballs left to decrease spawnDelay)</rose>')
  end

  toSpawn = toSpawn - 0.5
  print('toSpawn = ' .. toSpawn)
end
So now, we have a working auto-dificulty system!
That's not the end - as I said at the beggining there should be no mice joining during the game.
The best way to do it, is creating a table with all the mice that were in the room when the game started.
We'll use eventNewGame() here to detect when the game starts and then, put every player to this table.
A table-variable with all the mice should be placed at the beggining of the code, I called it inGame.

code_language.lua:
local inGame = {}

........

function eventNewGame()
  for player in next, tfm.get.room.playerList do
    inGame[player] = true
  end
end

I use a FOR loop to go thru the list of players in the room and add every player to inGame variable we definied before.
But how to prevent players from joining during the game?
The easiest way is just killing them over and over. We need 2 events here - eventNewPlayer() and eventPlayerRespawn()
In eventNewPlayer() we should just kill a player and in eventPlayerRespawn() we should check if the respawned player is inside of inGame table and if he isn't, then we just kill him.

code_language.lua:
function eventNewPlayer(player)
  tfm.exec.killPlayer(player)
end

function eventPlayerRespawn(player)
  if (inGame[player] ~= nil) then
    tfm.exec.killPlayer(player)
  end
end

But what if the player is in the game but he leaves the tribehouse and then re-joins?
He will still be inside of inGame table - it's not good! We can easily fix it by adding another event - eventPlayerLeft() in which we'll remove from inGame table the player that left the room:
code_language.lua:
function eventPlayerLeft(player)
  inGame[player] = nil
end
Fixed!


Now, the entire code looks like this:
code_language.lua:
local spawnDelay = 2
local boostDelay = 5
local toSpawn = 0
local toBoost = 0

local inGame = {}

tfm.exec.newGame('<C><P F="1" /><Z><S><S L="800" P="0,0,0.3,0.2,0,0,0,0" T="6" Y="400" X="400" H="50" /><S L="10" o="fffffffff" P="0,0,0,0,0,0,0,0" T="12" Y="-367" c="3" X="550" H="1500" /></S><D><DS X="737" Y="361" /></D><O /></Z></C>')

function eventLoop()
  if (toBoost == boostDelay and spawnDelay > 0.5) then
    spawnDelay = spawnDelay - 0.5
    toBoost = 0
  end

  if (toSpawn == 0) then
    tfm.exec.addShamanObject(17, 50, 220, 90)
    toBoost = toBoost + 1
    toSpawn = spawnDelay
  end

  toSpawn = toSpawn - 0.5
end

function eventNewGame()
  for player in next, tfm.get.room.playerList do
    inGame[player] = true
  end
end

function eventNewPlayer(player)
  tfm.exec.killPlayer(player)
end

function eventPlayerRespawn(player)
  if (inGame[player] == nil) then
    tfm.exec.killPlayer(player)
  end
end

function eventPlayerLeft(player)
  inGame[player] = nil
end

Looks great! The last thing we need to implement is 2 lives for each player.
Let's kill two birds with one stone. I'm sure you remember inGame table. When the game starts we are doing inGame[player] = true. And when we check if the player is inGame, we are doing if (inGame[player] ~= nil) - we just check if the value exists. So instead of inGame[player] = true we could do inGame[player] = 'BANANA' and it'd work too because we just check if this value exists (nil = doesn't exist).
So what if instead of true (or instead of banana) we would hold a number of lives of every player?
Let's check it! Inside of function eventNewGame() replace true with number 2:
code_language.lua:
function eventNewGame()
  for player in next, tfm.get.room.playerList do
    inGame[player] = 2
  end
end

Also, we need to create another event - eventPlayerDied() and inside of it we'll check 2 things - if the player is inside of inGame and if the player has more than 1 life left.
If both conditions are true, we'll respawn a player and decrease his lives with 1. Else, we will remove him from inGame so he won't be able to play until the game ends.
code_language.lua:
function eventPlayerDied(player)
  if (inGame[player] ~= nil and inGame[player] > 1) then
    tfm.exec.respawnPlayer(player)
    inGame[player] = inGame[player] - 1
  else
    inGame[player] = nil
  end
end

Done!

But what with the winner?! Every time when any player dies or leaves, we need to check if there are more than 1 players alive. If so, game continues but if there are only 1 player left, it means that we have a winner! But checking if there's only 1 player in 2 places will require repeating our code, which is not good, I think it'd be better if we check it inside of eventLoop.
Unfortunately, Lua doesn't have any built-in function to count elements of a table with non-numeric keys, we have to make this function ourselves.
code_language.lua:
function checkWinner(t)
  local i = 0
  for _ in pairs(t) do i = i + 1 end

  if (i == 1) then
    for player in next, t do
      return player
    end
  elseif (i == 0) then
    return nil
  end

  return false

end
This function counts given table, then checks how many elements does it have - if only 1 - it returns the last player standing nickname, if 0 - returns nil (to avoid bugs) and if more than 1, it returns false.
So: nil = error, false = no winner.

Okay, now we need to put this function into eventLoop()
code_language.lua:
function eventLoop()
  winner = checkWinner(inGame)
  if (winner == nil) then
    -- 0 players, weird, lets just start another game
    tfm.exec.newGame('<C><P F="1" /><Z><S><S L="800" P="0,0,0.3,0.2,0,0,0,0" T="6" Y="400" X="400" H="50" /><S L="10" o="fffffffff" P="0,0,0,0,0,0,0,0" T="12" Y="-367" c="3" X="550" H="1500" /></S><D><DS X="737" Y="361" /></D><O /></Z></C>')
  elseif (winner ~= false) then
    -- if it isn't false, then we have a winner!
    print('WINNER: ' .. winner)
    tfm.exec.newGame('<C><P F="1" /><Z><S><S L="800" P="0,0,0.3,0.2,0,0,0,0" T="6" Y="400" X="400" H="50" /><S L="10" o="fffffffff" P="0,0,0,0,0,0,0,0" T="12" Y="-367" c="3" X="550" H="1500" /></S><D><DS X="737" Y="361" /></D><O /></Z></C>')
  end

  ...........

end

The code is almost done. After each game we should reset changed variables to default.
To reset all of them easily, we can put them all to the function (they can't be local!).
code_language.lua:
function set()
  spawnDelay = 2
  toSpawn = 0
  toBoost = 0
end
Notice, I haven't put boostDelay variable here because it wasn't changed anywhere in the script. It's a "constant variable".
From now to reset these variables, you just call set() function.
These variables should be reseted when the the game starts, so inside of eventNewGame() function.
code_language.lua:
functon eventNewGame()
  set()

  ......
end

2 small changes and our minigame will be ready!
I think that it would be much more interesting if we add some randomness to it. Random spawn height and random cannonball angle would be great!
It's easy, we need math.random() function.
Let's go back to our cannonball spawn function and change it a bit.

tfm.exec.addShamanObject(17, 50, math.random(190, 250), math.random(80, 100))



And the last thing for this tutorial is beauty of our code. Notice, that we repeat our XML code 3 times. If you wanted to change it, you would need to change it in 3 places. It doesn't sound scary enough, I know but sometimes you will use one value like 10, 20 or even 100 times so it's good to get used to this.
Let's just put the XML code into some variable and then use this variable instead of XML itself, it should be easy for you! :)

That's all in this tutorial, I hope I helped you with Lua learning! A minigame is ready to play.
Full code:
code_language.lua:
local boostDelay = 5
local inGame = {}
local xml = '<C><P F="1" /><Z><S><S L="800" P="0,0,0.3,0.2,0,0,0,0" T="6" Y="400" X="400" H="50" /><S L="10" o="fffffffff" P="0,0,0,0,0,0,0,0" T="12" Y="-367" c="3" X="550" H="1500" /></S><D><DS X="737" Y="361" /></D><O /></Z></C>'

function set()
  spawnDelay = 2
  toSpawn = 0
  toBoost = 0
end

function checkWinner(t)
  local i = 0
  for _ in pairs(t) do i = i + 1 end

  if (i == 1) then
    for player in next, t do
      return player
    end
  elseif (i == 0) then
    return nil
  end

  return false

end

tfm.exec.newGame(xml)

function eventLoop()

  winner = checkWinner(inGame)
  if (winner == nil) then
    -- 0 players, weird, lets just start another game
    tfm.exec.newGame(xml)
  elseif (winner ~= false) then
    -- if it isn't false, then we have a winner!
    print('WINNER: ' .. winner)
    tfm.exec.newGame(xml)
  end

  if (toBoost == boostDelay and spawnDelay > 0.5) then
    spawnDelay = spawnDelay - 0.5
    toBoost = 0
  end

  if (toSpawn == 0) then
    tfm.exec.addShamanObject(17, 50, math.random(190, 250), math.random(80, 100))
    toBoost = toBoost + 1
    toSpawn = spawnDelay
  end

  toSpawn = toSpawn - 0.5
end

function eventNewGame()
  set()

  for player in next, tfm.get.room.playerList do
    inGame[player] = 2
  end
end

function eventNewPlayer(player)
  tfm.exec.killPlayer(player)
end

function eventPlayerRespawn(player)
  if (inGame[player] == nil) then
    tfm.exec.killPlayer(player)
  end
end

function eventPlayerLeft(player)
  inGame[player] = nil
end

function eventPlayerDied(player)
  if (inGame[player] ~= nil and inGame[player] > 1) then
    tfm.exec.respawnPlayer(player)
    inGame[player] = inGame[player] - 1
  else
    inGame[player] = nil
  end
end
There is still some stuff in this script that could be upgraded but currently this tutorial is much bigger than I expected so I leave it as it is.
Remember! You need at least 2 players, otherwise minigame won't work correctly!

Qmicozium
 
Last edited:

Nightsea

MAH CHEESE!
Amazing!
 
Top
"Dev-TR" theme by Soulzone