64M35 4-2: A Practice Passion Project

64M35 4-2 (read: “Games for two”)
Captain’s log No.1, 2021-03-05T23:08:00Z:

If anyone is listening (or not), I’ve decided to make a couch co-op game to practice making a comprehensive game for Windows. Utilising simple controls (Directional keys, an action button and pause), particle effects, (OpenGL) materials, sound and a fun level selection system, I wish to create a fun game for two people on one keybord or two controllers, learning how to use Defold and, more importantly, make a game along the way.

The general idea is to make a lot of mini-games for two players accessible from a “central hub” level select menu. The mini-games would be simple and recognisable, such as Pong, a Wolfenstein 3D-esque level, tower defense, football etc.

I tackled the same idea three years ago, but I lost my drive after clashing with the physics trying to bounce the ball off the paddles in Pong.

I’ll see how much I’ll be able to do, studying at uni at the same time.
Feel free to follow me along the way, learn with me or drop suggestions.

End log, 2021-03-05T23:17:00Z

11 Likes

Captain’s log No.2, 2021-03-05T23:23:00Z:
P0N6 AKA: Pong

I’ve been working on this for about three days before making a topic on this forum, so I’m logging my progress here.

T+3days:
I created this project.
I ran through the files, checking out what they had to offer and reacquainted myself with Defold and the different files available.
I set up inputs in input/game.input_binding for WASD, Arrow keys and Controller and edited the game.project file a bit.
I drew simple shapes and set up game object files for Pong.

T+2days:
I started work on the scripts. Programmed the paddles first making them actually move. Referencing APIs, Manuals and Examples from Defold Learn I slowly built my code, excessively studying it if it failed. Then the ball.

ball.script
function init(self)
       --[[excerp from the code]]--
    math.randomseed(os.time())
    math.random();math.random();math.random();    --clear consistent RNG buffer
    local dir = math.random(0, 360)	--set initial direction
    while dir == 90 or dir == 270 do
    	dir = math.random(0, 360)
    end
    dir = math.rad(dir)
    self.speed = 1000
    self.vel = vmath.vector3(self.speed * math.cos(dir), self.speed * math.sin(dir), 0)
end

This is what the code looks like now. It used to be a lot more messy, until I figured out velocity is a vector. I used to define the ball’s velocity with a speed and a directional angle. That means that my update(self, dt) function handled velocity calculating each movement separately using math.sin() and math.cos().
That worked until I had to program the ball “reflecting” (same angle in, same angle out) of the walls. See, me using the angle to move meant I had to make separate calculations for each of the four quadrants the ball was oriented towards, leaving me with clunky code. Now, well, it’s much simpler:

function reflect(self)	--linear relfection of ball on out of bounds
	self.vel.y = 0 - self.vel.y
end

After reflecting the ball off the walls (see details^) I added the ability for the ball to bounce off the paddles. Since I switched to a civilised vector math, it was easy. I dove into the on_message(self, message_id, message, sender) function and message_id = "contact_point_response".
I wanted to make the bounce directional based on where on the paddle the ball lands, instead of the boring constant angle, giving the game some strategy. So, I calculate the excentricity of the hit:

ball.script#bounce()
function on_message(self,message_id,message,sender)
    local pad = message.other_position
    local bal = message.position
    local where = bal.y - pad.y	--where on paddle:  excentricity of the hit to paddle's center
    local touch = where / 70	--ratio excentricity:half_paddle_size =~ excentricity [%]
    bounce(self,touch)
end
function bounce(self, touch)	--directional bounce off paddles; touch: where on the paddle the ball touches
    local dirX = 0
    if self.vel.x > 0 then	--direction left_right
        dirX = -1
    else
        dirX = 1
    end
    local dir = math.rad(75) * touch	--y rotational direction based on paddle hit location
    self.vel.x = math.cos(dir) * dirX * self.speed
    self.vel.y = math.sin(dir) * self.speed
end   

T+1day_yesterday:
I deleted the bit of code within ball.script that restricted the game object between y=50 and y=700 and replaced it with another game object that had a sprite and collision object. After I remembered to set it to “static” and after a long and painful process of trouble-shooting leading to me also remembering what masks on a collision object are for, I was set to start. And boy was it painful.
For some reason my reflect(self) code seemingly broke. I figured since the physics object sends out multiple events, I should make sure it reflects once, thus saving the last border under self.lstBrdr=message.other_id, since the ball can’t hit the same border twice. Nothing resolved the issue and all the code seemed fine.

Long story short, turns out the if not command works under very specific conditions in Lua that I hadn’t realised.

if not self.lstBrdr == message.other_id then  --> breaks everything
if not (self.lstBrdr == message.other_id) then -->works flawlessly

Yup… Two parenthesis made all the difference.

I also set up a simple score-keeping gui, that changes based on how many times the paddles have hit the ball and also changes colour based on which was last.
…once I figured out the msg.post(id, message_id, message) function, of course, or rather the addressing bit and how to write an id in the first place.

T-0_current:
This is the state of it right now:


You are now up-to-date

End Log, 2021-03-06T00:30:00Z

13 Likes

Captain’s log No.3, 2021-03-07T13:37:00Z
Still P0N6

T+1day_yesterday
What I’d made thus far worked great. I found some bugs, which I’ll get into later, but it worked well.
Until it didn’t… When I added stuff.


score.gui_script
I added four more text nodes to score.gui and set up code that would serve me later. I added nodes that would display how often the player had won total, reading from a save_file and saving it back onto it each time that player won/when the level got unloaded - haven’t decided yet. The other two nodes would display the current victories in the on-going game. Added code to the goal.go object that would send an update to the gui script and later to reset.go#reset.script as well.

And that’s where it all broke

The gui#scoreTxt node didn’t update anymore at all. I tried trouble-shooting by shoving pprint()-s everywhere. All the necessary functions got called, which they should, because I hadn’t changed them and they worked before.
For whatever reason msg.post(hash("/gui"), hash("score_update"), update = {colour, score}), which worked flawlessly the day before, resulted in an empty message sent to the gui object. EMPTY! No clue why!
To add insult to injury, when I moved onto programming the current score nodes, the update intended for paddle bounces updated the winning text of player 2 specifically??

Messaging system
--[[ball.script]]--
local update = {colour = self.colours[self.index[self.lstHit]], score = self.score}
msg.post(hash("/gui"), hash("score_update"), update)
--[[Goal.script]]--
local message = {id = self.id, updt = 1}
msg.post(hash("/gui"), hash("current_win"), message)
--[[score.gui_script]]--
function on_message(self, message_id, message, sender)
	pprint("gui", message_id, message, sender)
	if message_id == hash("score_update") then
		gui.set_color(self.scoreTxt, message.colour)
		gui.set_text(self.scoreTxt, tostring(message.score))
	end
	if message_id == hash("current_win") then
		update_crrnt(self, message)
	end
end

I checked about a million times for typos and other errors, and I have exactly 0 clue why the update goes through to "current_win". The Goal.script update, by the way, also updates the current win node, as intended. I don’t know… I moved on, to change my environment and not get beat up by this.


reset.go
I added a reset game object with nothing but a reset.script inside. First I wanted to just reload the collection, but since I’m utilising current_win text nodes in the gui and reloading the entire collection would lose all data without a .temp file (and I didn’t want to bother with that too, just for this) I decided I’d reset each game objects position and re-initialise them.
I quickly learned Lua’s for loops and tables aren’t exactly friends so I had to work around that, and then I learned that Lua doesn’t have any try-catch structures either, and pcall() and xcall() don’t do a n y t h i n g in my case, so I had to work around that too.
In the end, the script doesn’t work. It doesn’t even reset positions of objects, one way or another, let alone re-initialise them.


bug_report

  • Two people can’t move up/down at once - I though that may’ve been an issue with my keyboard, but the issue arises as well when I plug in my controller. I have no idea how to tackle that and no wish to deal with it just yet.
  • If you squeeze the ball between the wall and paddle it easily glitches out - I should be able to fix that, but I’m not quite ready for hundreds of lines of code
  • You can glitch your player out of bounds if you hold up and down simultaneously Doesn’t work on the bottom border, just the top, - I guess Up trumps Down, but I need fix that.
  • the fact that nothing works - yeah…

End log, 2021-03-07T14:06:00Z

5 Likes

I really appreciate that you share your project updates like this. It is great to follow along and see your learning experience. Thank you!

I suggest that you share your project as a zip file (exclude the .internal, .git and build folders). Me or someone else can take a look and help you move forward with your project.

2 Likes

This should be it. I’m very puzzled why this doesn’t work as it should anymore.

Zipped project (657.9 KB)

I should say, my code might be messy and overcomplicated, but it’s well commented.
And since this is a learning process, some things may be built in strange ways and I wouldn’t even know.

But if anyone is willing to check this out, I’d be very grateful. :slight_smile:

Ok, so one thing that stands out to me is the fact that you are using global functions in your scripts. Variables (and function definitions) in Lua are global by default. To make a function or variable local you put the local keyword before the declaration:

--
-- GLOBAL
--

foovar = "This is a global variable. It can be accessed from any script."

function foofun()
    print("This is a global function. It can be called from any script")
end

--
-- LOCAL
--
local foovar = "This is a local function. It is available in the current scope and after it has been declared"

local function foofun()
    print("This is a local function. It can be called from the current scope and after it has been declared")
end

The use of global functions becomes a problem in your project since you have multiple definitions of the score() function (once in Goal.script and once in ball.script). The result is that one of the function will be replaced by the other when it is declared. And when you call score() from Goal.script and ball.script unexpected things may happen since you are not calling the function you expect to be calling.

My recommendation: Go back and change all of the non-lifecycle function (init, update, on_message etc) to local functions and make sure you declare things in the right order.

If two functions need to call each other you can solve it like this:

local a = nil
local b = nil

a = function()
    b()
end

b = function()
    a()
end
4 Likes

I was completely unaware global functions extended across the entire Lua runtime.

I made most of them local (a few might come in handy globally) and made sure they would be defined in time to be called.

One I just placed at the top after function init(self) the rest I called in advance with:

function init(self)
    local score
    local speedUp
end
***
function score(self)
    --blah blah blah
end
...

Everything I’d made until now works just the way I need it to.
Thank you so much! You saved me a few headaches.
: )

EDIT: Nevermind, silly mistake, fixed it already :sweat_smile:

Captain’s log No.4, 2022-05-27T13:20:00Z
The Backbone (I guess)

T+4days
It’s been a while, huh? I’d lost time, lost motivation for this, but hadn’t lost interest.

To regain some motivation I wanted to overhaul the visual footprint of the project – easier to work on a pretty project than a placeholder-sprite-filled one.


Idea: sketch drawing! Works well with the nature of the project, since its sole purpose is to teach me the mechanics of game development and it would look interesting with a simplistic visual pattern. Crude geometric shapes made in half a second in Photoshop didn’t really sit with me.

Problem: It’s a lot easier to sketch by, well, sketching than by running my mouse across my desk, so I guess that part will still have to look crude until I can either borrow or buy a graphic tablet for my computer (which is a priority for academic purposes anyway).


T+3days
After about a year of not opening the project, I had to reacquaint myself with Defold (again) and with my project. I do not like to delete things, so while I was doing some necessary housework for bad ideas and bad code, I created a few ‘legacy’ folders and a lot of --[[comments]]--. (some scripts have hundreds of lines and about 50 of them actually functioning un-commented)
27-05-22_legacy folders
I’ll probably have to delete all of it at some point. But that’s a future me problem.


T+2days
What now? Well I do need a main menu and could maybe use one of those intro sequences with massive logos saying "Built with Defold" and "Made by blackbird" (the name I usually go by online)

So let's make a main menu.


BOOM! Done!

Now I’m sleepy.


T+1days_yesterday
I’ve missed programming. Even if after all this time 50% of it is looking up the Defold Manual, Examples and API. Some things do not change though – most of the time was still spent staring at the code and reading it slowly out loud, thinking where I’d gone wrong.

What is a ‘Proxy’ in Defold?

manager:controller#manager.script
-- i left out the init() function, because it does not really matter for this blog --
function on_message(self, message_id, message, sender)
	if message_id == hash("load_menu") then
		show(self, "#PRXY_main")
	elseif message_id == hash("load_level") then
		show(self, translate(self, message.gui_node))
	elseif message_id == hash("proxy_loaded") then
		self.current_proxy = sender
		msg.post(sender, "enable")
	elseif message_id == hash("proxy_unloaded") then
		print("Unloaded ", sender)
	end
end

function show(self, proxy)
	if self.current_proxy then
		msg.post(self.current_proxy, "unload")
		self.current_proxy = nil
	end
	msg.post(proxy, "async_load")
	--print("I done did it pardner")
end

--translate the gui node that initiated an action to a usable proxy name 
function translate(self, gui_node)
	return self.names[gui_node]
--[[ this was defined in init()
self.names = {["pong_box"] = "#PRXY_pong", }
]]--
end

The language I’d spent most time on in my life is Python. And Lua is just different enough to have me doubt the simplest expressions and peruse official Lua documentation.

for loops of all things got me beat.

why I am this confused with Lua
python
for i in range(1,10):
     i += 1
for thing in list:
    something(thing)
lua
for i=1,10,1 do
     something(i)
end
for index, item in ipairs(list) do
    something(item)
end

but what my brain wanted to do…

for i in 1..10 do
   sth(i)
end
for item in list do
   sth(item)
end

Don’t know why, don’t ask.

also pro-tip about local functions:

If you’re pedantic like me and want your function init(self) at the top and don’t want to start scripts with local functions;

local func1
local func2

function init(self)
   func1(a)
   func2(b)
end

This works if you need to call these functions immediately. Because what I’d been doing before is:

function init(self)
   --everything else i needed done immediately
   local func1
   local func2

   func1(a)
end

This returns an error, because I tried to call a nil value at line 6. I guess local functions need more than a few lines’ time to get acquainted with everyone. Apparently works perfectly fine, so long as you don’t call the functions in the init() space.

The reason I brought these two issues up (the for loops and forward-announcing local functions) is because the main_menu.gui didn’t want to recognise the “touch” action. (I defined LMB as “touch”)

Now, I never intended this game to use the mouse anyway, so I started work on a system to allow the keyboard or controller.

Allowed controls

W A S D LCtrl
↑ ← ↓ → RCtrl
↑ ← ↓ → ▢

self.matrix

Please refer to the first screenshot under 'T+2days’

self.matrix = {
	{"pong_box", "lvl2_box", "lvl3_box", nil; },
	{"lvl4_box", "lvl5_box", "lvl6_box", nil; },
	{nil, nil, nil, "exit_box"; }; 
}

This nested list represents the rows and collumns in which the buttons are arranged. I can move vertically and horizontally with ease using self.matrix[x][y]

Oh, also! Apparently in Lua, indexes start at 1, not 0.

function reset(self) in 'manager:controller#manager.script'

In order to show which box was selected, I set all of the to alpha=0.7 and would only make the selected box fully opaque. Here’s where the confusingfor loops come to play

inactive_alpha = 0.7
active_alpha = 1
function reset(self) 
	for i, rw in pairs(self.matrix) do
		for ii, clm in pairs(rw) do
			--reset each node to inactive_alpha=0.7
			if clm then
                 gui.set_alpha(gui.get_node(clm), inactive_alpha)
			end
		end
	end
end

Although I found the gui.set_alpha(node, number) function on the API it didn’t exist? Autocomplete didn’t recognise it and it returned an error saying I tried to access nil value.

-- work-around
if clm then
	local node = gui.get_node(clm)
	local xyzw = gui.get_color(node)
	xyzw.w = inactive_alpha
	gui.set_color(node, xyzw)
end
-- if you use the action key on an active node then
msg.post("manager:controller#manager", "load_level", {gui_node = self.current})
-- from main:main_gui#main_gui

T-0days_today

ERROR:GAMEOBJECT: Instance 'manager:controller#manager' could not be found when dispatching message 'load_level' sent from main:/main_gui#main_gui

I swear, I’ll be learning how to use the msg.post() function for the entirety of this project.

How the proxy is set up

27-05-22_pathways
both the main and manager collections are in the same directory
27-05-22_bootstrap
manager.collection is loaded immediately and never unloaded

-- function init(self) in 'manager:controller#manager.script'
function init(self)
	msg.post(".", "acquire_input_focus")
	self.current_proxy = nil
	msg.post("#", "load_menu")
end

main.collection is immediatelly loaded

if I change the msg.post() in main_gui.gui_script:

msg.post("manager:controller#manager", "load_level", {gui_node = self.current})
or
msg.post(msg.url("manager:controller#manager"), "load_level", {gui_node = self.current})
-->ERROR:SCRIPT: /main/main_menu.gui_script:95: Could not send message 'load_level' from 'main:/main_gui#main_gui' to 'manager:controller#manager'.
-->stack traceback:
-->  [C]:-1: in function post
msg.post(hash("manager:controller#manager"), "load_level", {gui_node = self.current})
-->ERROR:GAMEOBJECT: Instance 'manager:controller#manager' could not be found when dispatching message 'load_level' sent from main:/main_gui#main_gui

I do not see a reason, why the message can’t be delivered, since both collections are definitely loaded.


Anyway. I’m looking forward to working on this again for a few weeks until I inevitably dip again for months or years. :slight_smile:

End log, 2022-05-27T14:53:00Z

5 Likes

:grin:
You are not alone, this sounds all very familiar to me, esp. this:

Not that I coded all of my life, I started very late, but those few years I used Java a bit, but mostly Python and that’s the language I am still thinking in.

2 Likes

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!") :wink:


T+2days
intro sequence

I made a very simple intro sequence

Intro sequence

30-05-22_made with defold
‘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

30-05-22_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 :wink: )

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 :slight_smile:
30-05-22_pause minimap

3 Likes