Captain’s log No.5,2022-05-29T23:05:00Z
Gui – the necessary evil
T+3days
I left off the last post with an issue…
…and I firmly believe that the Defold community is one of the most nice and helpful communities out there
NOTE to SELF * (and anyone else): Make sure to name your collections on the collection screen.
*or rather: msg.post(".", "name your collections!")
T+2days
intro sequence
I made a very simple intro sequence
Intro sequence
‘Made with Defold’
gui.animate(self.defold_logo, gui.PROP_COLOR, to, gui.EASING_INOUTSINE, 5, 1, my_anim, gui.PLAYBACK_ONCE_PINGPONG)
‘Made by tyblackbird’
gui.animate(self.my_logo, gui.PROP_COLOR, to, gui.EASING_INOUTSINE, 5, 1, finish, gui.PLAYBACK_ONCE_PINGPONG)
I might tweak the durations at some point as it is a bit long. My “logo” is currently just a blank box node and I will also change the defold logo to fit the style of the game later on.
Also I added the most important feature to any intro sequence: the skip function
--the "I'M SICK OF THE HARD WORK THE DEV PUT INTO THIS AND WANT TO SKIP" function
function on_input(self, action_id, action)
gui.cancel_animation(self.defold_logo, gui.PROP_COLOR)
gui.cancel_animation(self.my_logo, gui.PROP_COLOR)
finish()
end
At first I wanted the intro.gui
to simply be overlayed onto the main_menu.gui
in the main.collection
to try and avoid creating unnecessarily small collection files but then I realised if the player decided to skip with ‘Ctrl,’ ‘Enter,’ or ‘X’ on the controller, the game would automatically load the first level. To avoid painful coding, I decided to just initiate the intro.collection
before the main one.
However – and I don’t know if that’s just Defold’s quirky quick build function, – but Skipping with ‘Enter’ would sometimes still load the first level before the main menu even appeared, and weirder still the ‘Ctrl+B’ shortcut to quick-build would sometimes act as input to skip the intro alltogether.
pause menu
The pause menu should appear when the player presses ‘Esc,’ ‘P’ or ‘Start’ on the controller. The r_u_sure
node only if you’re about to exit the level to menu or desktop.
H o w e v e r – I was coding this at 1AM and so I was running on sloppy autopilot. For some reason the pause menu was up when the level was loaded and would only go away after TWO ‘Esc’ presses. Turns out I was trying to gui.set_enabled()
with a string instead of a node.
gui.set_enabled("node", false) --> no no, baad
gui.set_enabled(gui.get_node("node"), false) --> yeeess
The navigation was fairly simple, and I’d already done a more complicated system for navigating the main menu grid of buttons via keyboard.
...
elseif action_id == hash("Down") then
--move the matrix down
if not ovrly_active then --but only if the 'Are you sure' screen is NOT active
self.location.x = self.location.x + 1
if self.location.x > self.MAX.x then
self.location.x = 1
end
end
elseif action_id == hash("Right") then
--move the matrix right
if ovrly_active then --but only if the 'Are you sure' screen IS active
self.location.y = self.location.y + 1
if self.location.y > self.MAX.y then --and make sure you're not selecting node number 5'928
self.location.y = 1
end
end
...
The navigation was functioning, now the only thing left to do was, well, actually pause the game.
Lucky for me Defold’s documentation is fairly exhaustive and, like I said before, the community is very helpful.
a simple msg.post("proxy:/controller", "set_time_step", {factor = 0, mode = 0})
is all it takes.
Just remember that unloading the level doesn't automatically unpause the game
function final(self)
--[[
Since you can only exit through the pause menu, the game remains paused when you reload a level.
And since the manager_proxy controls literally everything, everything remains paused until you
press the pause button twice.
This is in place to remove the confusion.
Fun fact, this is the first time I'd used the final(self) function on this project. :)
It's ten lines long and only one actually does something. lol
]]--
msg.post(manager_proxy, "set_time_step", unpause)
end
Funny thing about that is, function on_input
doesn’t care about simulation speed. While the pause screen is up and you navigate the menu with the arrow keys, the game moves with them. And since collision IS part of the simulation, the game objects can just fly out of bounds and break.
I tried to be cheeky about it and send a nuke message like: msg.post("level:", "release_input_focus")
except a url without a path is completely useless. And honestly I couldn’t have been bothered to find a way to find any game objects in a collection using Lua. I don’t want to do it manually, because I made a pause.go to just chuck into any ol’ level I needed. *
(Also trying to code my way into finding the `pause.go` parent collection was a whole ordeal)
Guys… I’m not good at this.
First I tried using the go.get_parent(".")
function, except I was within a .gui_script
file and that’s illegal! So I made a pause.script
file that would find its parent on init and immediately msg it to the gui_script, except I realised that it returns aN ID NOT A URL AND THAT DOESN’T HELP ME AT ALL!!
So I deleted the .script
again and found what I’d been looking for…
self.parent = msg.post().socket
Who knew an empty msg.post() function returned your own url?
Anyway, like I said, I did all that for nothing, because I couldn’t macro-release_input_focus
throughout the whole collection and I wasn’t going to take a pedestrian approach either.
Also, I’m surprised any amount of code from that day even works, because the comments are atrociously poorly spelled.
T+1day_yesterday
loading screen and finishing the main menu
…
I have written some poor code in my life, but now I have seen hell.
*
Pictured above: Hell
Okay, so I know Pong isn’t a big game to load – takes less than half a second on average, but I have plans you know, aspirations. And since I’m working on all these back-end functionalities, might as well make a loading screen.
gui.animate(self.basic_icon, "rotation.z", 360, gui.EASING_LINEAR, 2, 0, nil, gui.PLAYBACK_LOOP_FORWARD)
Done.
“But how does it work?” you may ask– It doesn’t. It breaks my game.
Sure, I may have almost entirely copy-pasted the collectionProxy code from the manual, but I thought I understood it enough to alter it to accomodate a loading screen.
Idea: I load the loading.collection
proxy (I actually named it this time) but I don’t send the enable message. Instead I only enable it before I send a load request and disable it again after I get the proxy_loaded
response.
Reality: It loaded and enabled itself immediately and underlaid the intro sequence. If I tried disabling it it sent an error. If I so much as looked at the loading.collection
funny it somehow sent a load_main_menu
message on EACH and EVERY INPUT. (see ‘Hell’ for reference)
The horrible wall of errors in the screenshot is just “couldn’t load menu, because it’s already loading” and “cannot disable loading_proxy because it was never loaded” about 60 times a second any time I touched my keyboard. And because it kept reloading the main menu, the screen was flashing and the navigation was stuck on level 1
I commented any code that mentioned loading.
I took the L and decided to stop for the day, when I remembered I hadn’t fully finished the keyboard navigation on the main menu. I thought, naïve and young: “It shouldn’t take long, I have a vague idea of what I need to do.”
three hours later
This is the very rough main menu I had made to just get the job done.
And this is the list written to represent it.
self.matrix = {
{"pong_box", "lvl2_box", "lvl3_box", nil; },
{"lvl4_box", "lvl5_box", "lvl6_box", nil; },
{nil, nil, nil, "exit_box"; };
} -- self.active_node = self.matrix[row][collumn]
So… TL;DR I got it working. A bunch of thinking and a few dozen lines of code and a bit of dumb maths. I am genuinely proud and excited about how I solved this issue, which is why this is your last chance to skip all of that.
Don't say I didn't warn you.
As you may know, Lua absolutely hates nil
values. The funny thing is, you could still navigate to the exit button fine. gui.get_node(nil)
is not a terminal error. But it’s confusing to press ‘down’ and lose your active node in the ether. I know how the matrix is constructed, but anyone else wouldn’t know how to properly exit the game (which, I mean, means they’re stuck playing my game forever )
So when you press a button and the self.active_node = nil
You need to get out of that. And preferably to a usable node in the direcition you pressed.
A few issues with that: If I go into the direction pressed indefinitely, I will completely bypass exit. I needed a way to find the most suitable node, perpendicular to the direction pressed. But problem! I can easily tell the function which direction I came from and where I’m going, but massively long if statements didn’t quite feel right. It’s a small menu, but still. But problem! I can’t really run any simple mathematic equations on “Left, Right, Up, Down” or a nested list.
So I thought I had an idea, and I started coding, and realised it wasn’t a good solution and got to delete it all.
And I’m staring at the screen (as you do) and realise something. Lua allows you to check if a variable has ANY value.
a = "something"
if a then ... --> true
b = nil
if b then ... -->false
So my matrix {{"node", "node1", nil}, ...}
is basically {{true, true, false},...}
AND more importantly: {{1, 1, 0}, ...}.
And those are numbers. For maths. So if I could devise a system that could assign each number a new value, I could then just find the highest number via for loop and save the position. So. I gingerly take a piece of paper with the zest of a child, trying to find good evaluations for straigh-on and to-the-side and … quickly realise I got excited for nothing. I had no idea how to do what I had envisioned. Sometimes the highest value would land onto a nil node. And that defeats the purpose of this whole operation. If you for example clicked “Left:” I assigned the left directly-across node a +1 value. The Up direction would get +2 and Down +3. That meant a valid node above would share the same high value as a nil node below. And I’d almost given up on this idea…
The answer, as to most things in life, was multiplication. I can add these values as described to the matrix, and after multiply it with the original matrix. The zeroes representing nill nodes cancel out the numbers. The ones representing valid nodes would be completely inert and I would get my code.
A quick example of the system
--this is the symbolised version of the matrix.
--each one represents a viable node, each zero nil
mock_matrix = {
{1, 1, 1, 0},
{1, 1, 1, 0},
{0, 0, 0, 1}
}
--[[if I, for example go from]] F to 5,-- my direction is to the right, my location at 5.
--the movement loops around and so I add +1 to D
--I add +2 to G and +3 to 4
--Keep in mind, this function is called only when you land on a nil node.
{A, B, C, 4}
{D, E, F, 5} -->
{1, 2, 3, G}
--addition phase
--as you see, the nil node top-right is valued the same as the bottom-right valid node.
{1, 1, 1, 3}
-> {2, 1, 1, 0} ->
{0, 0, 0, 3}
--multiplication phase
--this phase takes care of that.
{1, 1, 1, 0}
{2, 1, 1, 0}
{0, 0, 0, 3}
selected_node = "exit_node"
And this is the implementation:
--within function on_input()
if action_id == hash("lefUp") or action_id == hash("riUp") or action_id == hash("ctrUp") then
--move the matrix up
self.row = self.row - 1
if self.row < 1 then
self.row = self.MAXROW
end
if check_viable(self) then
-- cool that's all
else
a = get_viable(self, "U")
self.row = a.x
self.collumn = a.y
end
...
--local functions
function check_viable(self)
local from = self.matrix[self.row][self.collumn]
--is the selected option not nil?
if from then
--good. we're done here.
return true
end
end
function get_viable(self, drctn)
--it isn't a viable option? sit down, this'll be a while.
local vert = 0
local hor = 0
--mapping target=/=original direction to number
if drctn == "U" then
vert = -1
elseif drctn == "D" then
vert = 1
elseif drctn == "R" then
hor = 1
elseif drctn == "L" then
hor = -1
end
-- a numerical representation for which spaces are okay.
local mock_matrix = {
[0]={[0]=1, 1, 1, 0},
[1]={[0]=1, 1, 1, 0},
[2]={[0]=0, 0, 0, 1},
}
-- mathematical manipulation of the matrix to determine the best landing position
local origin = {x = self.row -1, y = self.collumn -1} --> i have just realised i got rows and collumns backwards on the x and y, but this is how the whole thing is coded so far, and i aint gonna change it.
mock_matrix[origin.x][origin.y] = 0
local sec_opt = mock_matrix[(origin.x+vert)%self.MAXROW][(origin.y+hor)%self.MAXCOLLUMN]
sec_opt = sec_opt * (sec_opt + 1)
mock_matrix[(origin.x+vert)%self.MAXROW][(origin.y+hor)%self.MAXCOLLUMN] = sec_opt
local fir_opt = mock_matrix[(origin.x+hor)%self.MAXROW][(origin.y+vert)%self.MAXCOLLUMN]
local ffir_opt = mock_matrix[(origin.x-hor)%self.MAXROW][(origin.y-vert)%self.MAXCOLLUMN]
fir_opt = fir_opt * (fir_opt + 3)
ffir_opt = ffir_opt * (ffir_opt + 2)
mock_matrix[(origin.x+hor)%self.MAXROW][(origin.y+vert)%self.MAXCOLLUMN] = fir_opt
mock_matrix[(origin.x-hor)%self.MAXROW][(origin.y-vert)%self.MAXCOLLUMN] = ffir_opt
--[[
VIABLE POSITION CRITERIA:
directly across is two spaces away from origin, therefore it is valued least at n(n+1)
one perpendicular direction is valued at n(n+2)
the third direction is valued at n(n+3)
if 'directly across' had priority over 'across', then the exit_node would never get chosen, because it borders
on all sides to a nil value.
Which perpendicular direction is valued more is completely arbitrary.
If the evaluated direction is marked 0 then n(n+a) = 0
If the evaluated direction is marked 1 then n(n+a) > 0
Because in some cases I could try evaluating a direction marked nil outside of the matrix, I used
'(origin.x+hor)%self.MAXROW' -> which returns the remainder of division with the MAXINDEX
Since a%a = 0 and a%a ~= a, I had to alter the indexing in mock_matrix so that it started with 0
In the return statement I then add 1 to both x and y.
]]
--find the highest number in the mock_matrix and save its indexes
local high_index = {x = 0, y = 0}
local high_num = 0
for ix, list in pairs(mock_matrix) do
for iy, num in pairs(list) do
if num >= high_num then
high_num = num
high_index.x = ix + 1
high_index.y = iy + 1
end
end
end
return high_index
end
Now… This solution isn’t without issues. Because the first_option
and ffirst_option
directions are completely arbitrary you sometimes get strange behaviour. If you press ‘Down’ on the bottom-right node, you’ll somehow end up at the top-left. I understand how the maths does that, but I think it’ll be fine. It’s perfectly okay for what it is.
But it’s still so cool!!!
Just look at those slick one-liners
mock_matrix[(origin.x+hor)%self.MAXROW][(origin.y+vert)%self.MAXCOLLUMN] = fir_opt
Oooooooooh mamma!
Anyway, I did warn you I was gonna be long.
Oh also, I finally put an actual program termination function to the game.
sys.exit(0)
– I don’t understand why I was putting that off for five days.
T+0days_today
And here we are.
So I have a new issue, that I don’t know how to solve. Maybe I could try to implement a loading screen via guis and not collections. I’m not sure I want to bang my head against it anyway.
Future plans: Finish all the unfinished pong.collection
functionality and start work on other things.
End log, 2022-05-30T01:27:00Z
PS.: Can I just say, I love the little “mini map” of code in the editor