
Creating a game on Lua and LÖVE - 6
- Transfer
- Tutorial

Table of contents
14. Console
15. Final
Table of contents
- Article 1
- Part 1. Game cycle
- Part 2. Libraries
- Part 3. Rooms and areas
- Part 4. Exercises
- Section 2
- Part 5. Game Basics
- Part 6. Player class basics
- Section 3
- Part 7. Parameters and attacks of the player
- Part 8. Enemies
- Section 4
- Part 9. Director and game cycle
- Part 10. Code Writing Practices
- Part 11. Passive skills
- Section 5
- Part 12. Other passive skills
- Section 5
- Part 13. Skill tree
14. Console
15. Final
Part 13: Skill Tree
Introduction
In this part of the article, we will focus on creating a skill tree. Here is how it looks now . We will not locate each node manually (I will leave this as an exercise), but consider everything necessary for the implementation and proper operation of the skill tree.
First we look at the way each node is defined, then we learn how to read these definitions, create the necessary objects, and apply the corresponding passive skills to the player. Then we will move on to the main objects (nodes and links), and then consider saving and loading the tree. And in the end, we implement the functionality necessary for the player to spend skill points on tree nodes.
Skill tree
There are many different ways to define a skill tree, each of which has its own advantages and disadvantages. We can choose from approximately three options:
- Create a skill tree editor, placing, linking and defining the parameters of each node visually;
- Create a skill tree editor that places and links nodes visually, but defines the parameters of each node in a text file;
- Define everything in a text file.
I am one of those people who strive to make implementation as simple as possible and who have no problems completing a large amount of manual and boring work, so I usually solve problems this way. That is, of the three options proposed above, I am inclined to the third.
For the first two options, we would need to create a visual skill tree editor. To understand what this entails, we should try to list the high-level functions that the visual editor of the skill tree must have:
- Hosting New Nodes
- Linking Nodes Together
- Removing Nodes
- Move nodes
- Text input for determining the parameters of each node
I only came up with these high-level capabilities, which include other functions:
- The nodes most likely should somehow align with each other, that is, we need some kind of alignment system. Perhaps the nodes can only be placed in accordance with a certain grid system.
- Linking, deleting, and moving nodes implies that we need the ability to select specific nodes to which we want to apply such actions. This means that we will also have to implement the function of selecting nodes.
- If we choose the option in which the parameters are determined visually, then text input is required. There are several ways to organize the correct operation of the TextInput element in LÖVE, with little effort ( github.com/keharriso/love-nuklear ), so we only need to add the logic of the moment the text input element is displayed and the information is read from it after writing.
As you can see, adding a skill tree editor doesn't seem like much work compared to what we have already done. Therefore, if you want to choose this option, then it is quite viable and can, in your case, improve the process of creating a skill tree. But as I said, usually I don’t have any problems with performing large volumes of manual and boring work, that is, I can easily determine everything in a text file. Therefore, in this article we will not implement any editor of the skill tree and will fully define it in a text file.
Tree definition
So, to begin the definition of the tree, we need to think about what elements the node consists of:
- Passive Skill Text:
- Title
- Parameters changed by him (6% Increased HP, +10 Max Ammo, etc.)
- Position
- Connected nodes
- Node Type (Normal, Medium, or Large)
An example of a 4% Increased HP node is shown in the gif below:

It can have for example the following definition:
tree[10] = {
name = 'HP',
stats = {
{'4% Increased HP', 'hp_multiplier' = 0.04}
}
x = 150, y = 150,
links = {4, 6, 8},
type = 'Small',
}
We believe that
(150, 150)
this is a suitable position, and the positions in the table of tree
nodes associated with it are 4, 6 and 8 (the position of the node is 10, since it is defined in tree[10]
). Thus, we can easily identify hundreds of tree nodes, pass this huge table to a function that counts it, create Node objects and connect them accordingly, after which we can apply any logic we need to the tree.Nodes and Camera
Now that we have an idea of what the tree file will look like, we can begin implementation on this basis. The first thing we need is to create a new room
SkillTree
and then use gotoRoom
it to go into it at the beginning of the game (because now we will work in it). The basics of this room will be the same as that of the Stage room, so I will assume that you will manage to create it yourself. We will define
tree.lua
two nodes in the file , but for now we will do this only according to their position. Our goal is to read these nodes from the file and create them in the SkillTree room. We can define them as follows:tree = {}
tree[1] = {x = 0, y = 0}
tree[2] = {x = 32, y = 0}
And we can consider them like this:
function SkillTree:new()
...
self.nodes = {}
for _, node in ipairs(tree) do table.insert(self.nodes, Node(node.x, node.y)) end
end
Here we believe that all objects of our SkillTree will not be inside the Area, that is, we do not need to use a
addGameObject
new game object to add to the environment. This also means that we will need to track existing objects ourselves. In this case, we do this in the table nodes
. The object is Node
as follows:Node = Object:extend()
function Node:new(x, y)
self.x, self.y = x, y
end
function Node:update(dt)
end
function Node:draw()
love.graphics.setColor(default_color)
love.graphics.circle('line', self.x, self.y, 12)
end
This is a simple object that does not extend the capabilities of GameObject. And while we simply draw in his position as a circle. If we go around the list
nodes
and call update / draw for each node that it has, assuming that the camera is fixed in position 0, 0
(as opposed to the Stage room in which it is fixed in gw/2, gh/2
), then it should look like this:
As expected, we see here both nodes that are defined in the tree file.
Camera
For the skill tree to work properly, we need to slightly change the camera. As long as we have the same behavior as in the Stage room, that is, the camera is simply attached to the position and does not do anything interesting. But in SkillTree, we want the camera to be able to move with the mouse, and the player to move it away (and zoom in) to see most of the tree at the same time.
To move the camera, we want to make sure that when the player holds the left mouse button and drags the screen, he moves in the opposite direction. That is, when the player holds the button and moves the mouse up, we want the camera to move down. The easiest way to achieve this is by tracking the mouse position in the previous frame, as well as in the current frame, and then move it in the direction opposite to the vector
current_frame_position - previous_frame_position
. It all looks like this:function SkillTree:update(dt)
...
if input:down('left_click') then
local mx, my = camera:getMousePosition(sx, sy, 0, 0, sx*gw, sy*gh)
local dx, dy = mx - self.previous_mx, my - self.previous_my
camera:move(-dx, -dy)
end
self.previous_mx, self.previous_my = camera:getMousePosition(sx, sy, 0, 0, sx*gw, sy*gh)
end
If you check, then everything will work as intended. Notice that
camera:getMousePosition
it has slightly changed compared to the default functionality , since we work with the canvas differently than the library expects. I changed it a long time ago, so I don’t remember why I did it, so I’ll just leave it as it is. But if you are curious, then you should consider this in more detail and figure out whether to do it this way, or is there a way to use the default camera module without changes. As for the distance / zoom, we simply change the property of the camera
scale
when scrolling the mouse wheel up / down:function SKillTree:update(dt)
...
if input:pressed('zoom_in') then
self.timer:tween('zoom', 0.2, camera, {scale = camera.scale + 0.4}, 'in-out-cubic')
end
if input:pressed('zoom_out') then
self.timer:tween('zoom', 0.2, camera, {scale = camera.scale - 0.4}, 'in-out-cubic')
end
end
Here we use a timer to make the scaling a little smoother and look better. In addition, we give both timers the same identifier
'zoom'
, because we want one tween to stop when we start the other. The only thing left in this code fragment is to add the limitations of the lower and upper limits of the scale, because we do not want it, for example, to go lower.Links and options
Thanks to the previous code, we will be able to add nodes and move around the tree. Now we will consider connecting nodes and displaying their parameters.
To connect the nodes, we will create an object
Line
, and this Line object will receive in its constructors the id
two nodes that it connects. id
denotes the index of the node in the object tree
. That is, a node created from tree[2]
will have id = 2
. We can modify the Node object as follows:function Node:new(id, x, y)
self.id = id
self.x, self.y = x, y
end
And we can create the Line object like this:
Line = Object:extend()
function Line:new(node_1_id, node_2_id)
self.node_1_id, self.node_2_id = node_1_id, node_2_id
self.node_1, self.node_2 = tree[node_1_id], tree[node_2_id]
end
function Line:update(dt)
end
function Line:draw()
love.graphics.setColor(default_color)
love.graphics.line(self.node_1.x, self.node_1.y, self.node_2.x, self.node_2.y)
end
Here we use the passed identifiers to get the corresponding nodes and store in
node_1
and node_2
. Then we simply draw a line between the positions of these nodes. Now in the SkillTree room we need to create Line objects based on the table of
links
each node in the tree. Let's say we have a tree that looks like this:tree = {}
tree[1] = {x = 0, y = 0, links = {2}}
tree[2] = {x = 32, y = 0, links = {1, 3}}
tree[3] = {x = 32, y = 32, links = {2}}
We want node 1 to be connected to node 2, node 2 to be connected to node 1 and 3, and node 3 to connected to node 2. In terms of implementation, we must go through each node and each of its connections, and then go to Based on these relationships, create Line objects.
function SkillTree:new()
...
self.nodes = {}
self.lines = {}
for id, node in ipairs(tree) do table.insert(self.nodes, Node(id, node.x, node.y)) end
for id, node in ipairs(tree) do
for _, linked_node_id in ipairs(node.links) do
table.insert(self.lines, Line(id, linked_node_id))
end
end
end
The last thing we can do here is draw the nodes using the mode
'fill'
, otherwise the lines will overlap the nodes and will be displayed a little:function Node:draw()
love.graphics.setColor(background_color)
love.graphics.circle('fill', self.x, self.y, self.r)
love.graphics.setColor(default_color)
love.graphics.circle('line', self.x, self.y, self.r)
end
And after that, everything should look like this:

Now let's move on to the parameters: suppose we have a tree like this:
tree[1] = {
x = 0, y = 0, stats = {
'4% Increased HP', 'hp_multiplier', 0.04,
'4% Increased Ammo', 'ammo_multiplier', 0.04
}, links = {2}
}
tree[2] = {x = 32, y = 0, stats = {'6% Increased HP', 'hp_multiplier', 0.04}, links = {1, 3}}
tree[3] = {x = 32, y = 32, stats = {'4% Increased HP', 'hp_multiplier', 0.04}, links = {2}}
We want to achieve the following:

Regardless of the distance or proximity, when the user hovers the mouse over the node, he should display his parameters in a small rectangle.
The first thing we can do is find out if the player has moved the mouse cursor over the node or not. The easiest way to do this is to check if the mouse position is inside the rectangle defining each node:
function Node:update(dt)
local mx, my = camera:getMousePosition(sx*camera.scale, sy*camera.scale, 0, 0, sx*gw, sy*gh)
if mx >= self.x - self.w/2 and mx <= self.x + self.w/2 and
my >= self.y - self.h/2 and my <= self.y + self.h/2 then
self.hot = true
else self.hot = false end
end
Width and height are defined for each node, so we will check whether the mouse position is
mx, my
inside the rectangle defined by its width and height. If so, then we set it to hot
true; otherwise, false. That is hot
, it's just a boolean telling us if the cursor is on the node. Now let's move on to drawing a rectangle. We want to draw a rectangle on top of everything on the screen, so this will not work in the Node class, since each node is drawn in sequence and our rectangle can sometimes appear under one or another node. Therefore, I do it directly in the SkillTree room. It is also important that we do this outside the block
camera:attach
andcamera:detach
, because we want the size of this rectangle to remain the same regardless of scale. Its base looks like this:
function SkillTree:draw()
love.graphics.setCanvas(self.main_canvas)
love.graphics.clear()
...
camera:detach()
-- Stats rectangle
local font = fonts.m5x7_16
love.graphics.setFont(font)
for _, node in ipairs(self.nodes) do
if node.hot then
-- Draw rectangle and stats here
end
end
love.graphics.setColor(default_color)
love.graphics.setCanvas()
...
end
Before drawing a rectangle, we need to find out its width and height. The width depends on the size of its longest parameter, because the rectangle must by definition be larger than it. To do this, we will try to do something similar:
function SkillTree:draw()
...
for _, node in ipairs(self.nodes) do
if node.hot then
local stats = tree[node.id].stats
-- Figure out max_text_width to be able to set the proper rectangle width
local max_text_width = 0
for i = 1, #stats, 3 do
if font:getWidth(stats[i]) > max_text_width then
max_text_width = font:getWidth(stats[i])
end
end
end
end
...
end
The variable
stats
will contain a list of parameters for the current node. That is, if we go through the node tree[2]
, it stats
will matter {'4% Increased HP', 'hp_multiplier', 0.04, '4% Increased Ammo', 'ammo_multiplier', 0.04}
. The parameter table is always divided into three elements. The first is a visual description of the parameter, then comes the variable that changes the Player object, and then the magnitude of this effect. We need only a visual description, that is, we must go through the table with an increment of 3, which we do in the for loop shown above. After that we need to find the line width taking into account the font used, and for this we will use it
font:getWidth
. The maximum width of all our parameters will be stored in a variable max_text_width
, after which we can start drawing a rectangle:function SkillTree:draw()
...
for _, node in ipairs(self.nodes) do
if node.hot then
...
-- Draw rectangle
local mx, my = love.mouse.getPosition()
mx, my = mx/sx, my/sy
love.graphics.setColor(0, 0, 0, 222)
love.graphics.rectangle('fill', mx, my, 16 + max_text_width,
font:getHeight() + (#stats/3)*font:getHeight())
end
end
...
end
We want to draw a rectangle at the mouse position, except that we do not need to use it
camera:getMousePosition
, because we do not take into account the transformation of the camera. However, we cannot simply use it directly love.mouse.getPosition
, because the canvas is scaled to sx, sy
, that is, the mouse position returned by the LÖVE function is incorrect if the scale of the game is different from 1. Therefore, in order to get the right value, we need to divide this position by scale. Having received the correct position, we can draw a rectangle with a width
16 + max_text_width
, which gives us a border of 8 pixels on each side, and with a height font:getHeight() + (#stats/3)*font:getHeight()
. The first element of this formula ( font:getHeight()
) is used for the same purpose as 16 in calculating the width, that is, it gives a value for the border. In our case, the upper and lower borders of the rectangle will be equalfont:getHeight()/2
. The second part is simply the height occupied by each row of parameters. Since the parameters are grouped in three, it is logical to consider each parameter as #stats/3
, and then multiply this number by the height of the line. The last thing to do is draw the text. We know that the x position of all texts will be equal
8 + mx
, because we decided that there will be an 8 pixel border on each side. And we also know that the position of the first text in y will be equal my + font:getHeight()/2
, because we decided that the border above and below will be equal font:getHeight()/2
. We only need to figure out how to draw a few lines, but we already know this because we have chosen the height of the rectangle to be equal (#stats/3)*font:getHeight()
. This means that each line is drawn 1*font:getHeight()
, 2*font:getHeight()
and so on. All this looks as follows:function SkillTree:draw()
...
for _, node in ipairs(self.nodes) do
if node.hot then
...
-- Draw text
love.graphics.setColor(default_color)
for i = 1, #stats, 3 do
love.graphics.print(stats[i], math.floor(mx + 8),
math.floor(my + font:getHeight()/2 + math.floor(i/3)*font:getHeight()))
end
end
end
...
end
And so we get the result we need. If you look at the whole code as a whole, it looks like this:
function SkillTree:draw()
love.graphics.setCanvas(self.main_canvas)
love.graphics.clear()
...
-- Stats rectangle
local font = fonts.m5x7_16
love.graphics.setFont(font)
for _, node in ipairs(self.nodes) do
if node.hot then
local stats = tree[node.id].stats
-- Figure out max_text_width to be able to set the proper rectangle width
local max_text_width = 0
for i = 1, #stats, 3 do
if font:getWidth(stats[i]) > max_text_width then
max_text_width = font:getWidth(stats[i])
end
end
-- Draw rectangle
local mx, my = love.mouse.getPosition()
mx, my = mx/sx, my/sy
love.graphics.setColor(0, 0, 0, 222)
love.graphics.rectangle('fill', mx, my,
16 + max_text_width, font:getHeight() + (#stats/3)*font:getHeight())
-- Draw text
love.graphics.setColor(default_color)
for i = 1, #stats, 3 do
love.graphics.print(stats[i], math.floor(mx + 8),
math.floor(my + font:getHeight()/2 + math.floor(i/3)*font:getHeight()))
end
end
end
love.graphics.setColor(default_color)
love.graphics.setCanvas()
...
end
And I know that if I saw a similar code a few years ago, I would really not like it. It looks ugly, disordered, and sometimes confusing, but based on my experience, it looks like a stereotypical rendering code in game development. Everywhere there are many small and seemingly random numbers, many different problems instead of a complete piece of code, and so on. Today I’m already used to this type of code and it doesn’t annoy me anymore, and I advise you to get used to it too, because if you try to make it “cleaner”, then, in my experience, this will only lead to even more confusing and less intuitive decisions.
Gameplay
Now that we can arrange the nodes and connect them together, we need to code the nodes purchase logic. The tree will have one or more "entry points" from which the player can start buying nodes, and from which he can only buy nodes adjacent to those already bought. For example, in my scheme there is a central starting node that does not give any bonuses, to which four additional nodes are connected that make up the beginning of the tree:

Suppose now that we have a tree that initially looks like this:
tree = {}
tree[1] = {x = 0, y = 0, links = {2}}
tree[2] = {x = 48, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}, links = {3}}
tree[3] = {x = 96, y = 0, stats = {'6% Increased HP', 'hp_multiplier', 0.06}, links = {4}}
tree[4] = {x = 144, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}}

The first thing we need is to make sure that this node 1 is already activated, and the others are not. By activated node, I mean that it is already bought by the player and its effects are applied in the gameplay. Since node 1 has no effects, in this way we can create a “source node” from which the tree will grow.
We will do this through a global table
bought_node_indexes
, which will simply contain a bunch of numbers pointing to the nodes of the tree that are already purchased. In our case, we simply add to it 1
, that is, it tree[1]
will be active. We also need to slightly change the nodes and links graphically, so that we can more easily see which of them are active and which are not. For now, we will simply display the locked nodes in gray (with alpha = 32 instead of 255), and not white:function Node:update(dt)
...
if fn.any(bought_node_indexes, self.id) then self.bought = true
else self.bought = false end
end
function Node:draw()
local r, g, b = unpack(default_color)
love.graphics.setColor(background_color)
love.graphics.circle('fill', self.x, self.y, self.w)
if self.bought then love.graphics.setColor(r, g, b, 255)
else love.graphics.setColor(r, g, b, 32) end
love.graphics.circle('line', self.x, self.y, self.w)
love.graphics.setColor(r, g, b, 255)
end
And for the links:
function Line:update(dt)
if fn.any(bought_node_indexes, self.node_1_id) and
fn.any(bought_node_indexes, self.node_2_id) then
self.active = true
else self.active = false end
end
function Line:draw()
local r, g, b = unpack(default_color)
if self.active then love.graphics.setColor(r, g, b, 255)
else love.graphics.setColor(r, g, b, 32) end
love.graphics.line(self.node_1.x, self.node_1.y, self.node_2.x, self.node_2.y)
love.graphics.setColor(r, g, b, 255)
end
We activate the line only when both of its nodes are bought, which looks logical. If we say in the constructor of the SkillTree room
bought_node_indexes = {1}
, we get something like this:
And if we say that
bought_node_indexes = {1, 2}
, we get this:
And everything works as we expected. Now we want to add the logic necessary so that when you click on a node it is bought if it is connected to another node that has already been purchased. Determining whether we have enough skill points to purchase a node and adding a confirmation step before buying a node we will leave for exercises.
Before we make it so that you can buy nodes connected to already purchased, we need to eliminate a small problem with the way we define our tree. Now we have the following definition:
tree = {}
tree[1] = {x = 0, y = 0, links = {2}}
tree[2] = {x = 48, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}, links = {3}}
tree[3] = {x = 96, y = 0, stats = {'6% Increased HP', 'hp_multiplier', 0.06}, links = {4}}
tree[4] = {x = 144, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}}
One of the problems with this definition is its one-pointedness. And this was logical to expect, because if it were not unidirectional, then we would have to determine the connection between several nodes many times:
tree = {}
tree[1] = {x = 0, y = 0, links = {2}}
tree[2] = {x = 48, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}, links = {1, 3}}
tree[3] = {x = 96, y = 0, stats = {'6% Increased HP', 'hp_multiplier', 0.06}, links = {2, 4}}
tree[4] = {x = 144, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04, links = {3}}}
And although there is no particularly big problem in such an implementation, we can make it so that it is enough to determine the connections only once (in any direction), and then apply an operation that automatically makes the connections so that they are determined in the opposite direction.
We can do this by going through the list of all nodes, and then through all the links of each node. For each link found, we go to the corresponding node and add the current node to its links. For example, if we are in node 1 and see that it is connected to 2, then we go to node 2 and add node 1 to its list of links. Thus, we guarantee that when we have a definition in one direction, then there will be a definition and in the opposite direction. In code, it looks like this:
function SkillTree:new()
...
self.tree = table.copy(tree)
for id, node in ipairs(self.tree) do
for _, linked_node_id in ipairs(node.links or {}) do
table.insert(self.tree[linked_node_id], id)
end
end
...
end
First, it’s worth noting that instead of using a global variable,
tree
we copy it locally into an attribute self.tree
, and then use this attribute. In SkillTree, Node, and Line objects, we must replace the global references with the tree
local tree
SkillTree attribute . We must do this because we will change the definition of the tree by adding the numbers of certain nodes to the connection table, and in the general case (for the reasons explained in part 10) we do not want to change global variables in this way. This means that every time we enter the SkillTree room, we copy the global definition to the local one and use the local definition in the code. With this in mind, we will now go through all the nodes of the tree and create feedback nodes. Important to use inside the call
ipairs
node.links or {}
, because some nodes may have a relationship table defined. It is also important to note that we do this before creating Node and Line objects, although this is not necessary. In addition, it should be noted that sometimes in the table there
links
will be duplicate values. Depending on how the table is defined, tree
we will sometimes place the nodes bi-directionally, that is, the links will already be where they should be. This is actually not a problem, except that it can lead to the creation of multiple Line objects. To prevent this, we can walk through the tree again and make sure that all tables links
contain only unique values:function SkillTree:new()
...
for id, node in ipairs(self.tree) do
if node.links then
node.links = fn.unique(node.links)
end
end
...
end
Now the only thing left is to make sure that when you click on a node, we check whether it is connected to an already purchased node:
function Node:update(dt)
...
if self.hot and input:pressed('left_click') then
if current_room:canNodeBeBought(self.id) then
if not fn.any(bought_node_indexes, self.id) then
table.insert(bought_node_indexes, self.id)
end
end
end
...
end
And this will mean that if the mouse cursor is over the node and the player presses the left mouse button, then we check with the help of the
canNodeBeBought
SkillTree object function whether this node can be purchased (we will implement the function below). If it can be purchased, we add it to the global table bought_node_indexes
. Here we also make it impossible to add a node to this table twice. Although if we added it several times, it would not change anything and would not cause any bugs. The function
canNodeBeBought
works like this: it goes through the connected nodes to the node that was passed to it and check if any of them are inside the table bought_node_indexes
. If this is the case, then this node is connected to the already purchased one, that is, you can buy it:function SkillTree:canNodeBeBought(id)
for _, linked_node_id in ipairs(self.tree[id]) do
if fn.any(bought_node_indexes, linked_node_id) then return true end
end
end
This is what we achieved:

The last task we will consider is how to apply the effects of selected nodes to a player. This is simpler than it seems due to the way we structured everything in parts 11 and 12. Now the tree definition looks like this:
tree = {}
tree[1] = {x = 0, y = 0, links = {2}}
tree[2] = {x = 48, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}, links = {3}}
tree[3] = {x = 96, y = 0, stats = {'6% Increased HP', 'hp_multiplier', 0.06}, links = {4}}
tree[4] = {x = 144, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}}
As you can see, we have a second parameter value - a string that should point to a variable defined in the Player object. In our case, this is a variable
hp_multiplier
. If we return to the Player object and see where it is used hp_multiplier
, we will see the following:function Player:setStats()
self.max_hp = (self.max_hp + self.flat_hp)*self.hp_multiplier
self.hp = self.max_hp
...
end
It is used in the function
setStats
as a base HP multiplier, combined with some simple HP value, as we expected. We want the following behavior from the tree: for all nodes inside, bought_node_indexes
we apply their parameter to the corresponding player variable. That is, if there are nodes 2, 3, and 4 inside this table, then the player must have hp_multiplier
1.14 (0.04 + 0.06 + 0.04 + base equal to 1). We can relatively easily implement it like this:function treeToPlayer(player)
for _, index in ipairs(bought_node_indexes) do
local stats = tree[index].stats
for i = 1, #stats, 3 do
local attribute, value = stats[i+1], stats[i+2]
player[attribute] = player[attribute] + value
end
end
end
We define this function in
tree.lua
. As expected, we go through all the purchased nodes, and then by their parameters. For each parameter, we take the attribute ( 'hp_multiplier'
) and the value (0.04, 0.06), and then apply them to the player. In the example under discussion, the string is player[attribute] = player[attribute] + value
parsed to player.hp_multiplier = player.hp_multiplier + 0.04
or to player.hp_multiplier = player.hp_multiplier + 0.06
, depending on which node we loop around. This means that by the end of the external for we will apply all purchased passive skills to the player’s variables. It is important to note that various passive skills need to be handled a little differently. Some skills are of type boolean, others must be applied to variables that are objects of Stat, and so on. All of these differences must be handled outside of this function.
224. (CONTENT)Implement skill points. We have a global variable
skill_points
that stores the number of skill points a player has. When a player purchases a new node in the skill tree, this variable should decrease by 1. The player should not be able to buy more nodes than he has skill points. A player can buy no more than 100 nodes. If necessary, you can slightly change these numbers. For example, in my game the price of each node increases depending on how many nodes the player has already bought. 225. (CONTENT)Implement the stage before buying nodes, at which the player can refuse to buy. This means that the player can click on the nodes, as if buying them, but to confirm the purchase, he must click on the "Apply Points" button. When you click on the “Cancel” button, all selected nodes will be canceled. Here's what it looks like:

226. (CONTENT) Implement the skill tree. You can make this tree of any size suitable for you, but it is obvious that the larger it is, the more possible interactions in it will be and the more interesting it will turn out. For reference: here is what my tree looks like:

Remember to add each individual type of passive skill the appropriate behavior in the function
treeToPlayer
!END
And this is where the article ends. In the next part, we will look at the Console room, and the part after it will be the last. In the last part we will consider some aspects, one of them is loading and saving. We did not discuss one of the elements of the skill tree, namely saving the nodes purchased by the player. We want these nodes to remain purchased throughout the passage, as well as after the close of the game, so in the last part we will consider this function in more detail.
And as I said many times already, if you do not want, then you can not create a skill tree. If you performed all the actions from the previous parts, then you already have all the passive skills implemented from parts 11 and 12, and you can present them to the player in any form convenient for you. I decided to use a tree, but you can choose something else if creating a huge tree by hand seems like a bad idea to you.