Wednesday, February 28, 2024

Stacker for Vita

Alright, this entry comes after the release of the game, again. So, why bother?
Easy, it adds flavour and meaning to some design decissions, and it helps to understand and improve or reuse the code and/or the methods found. In addition, you cam see here a few of the problems during the developement, and how they were overcome (when/if they did).

First of all, a bit of history:

The idea of making this homebrew comes from 2 or so years ago, more or less before the hiatus. I was thinking about some options that weren't too difficult, so I can practice a bit. Then I remember playing this game https://en.wikipedia.org/wiki/Stacker_(arcade_game) , however, at that time, I didn't remember the name and I had some of the core mechanics foggy. I put it in the wardrobe, and just after the D&D sheet, I saw out of the blue a youtube recomendation of someone playing exactly this, and it came the domino effect.

So, I was able to review what I have and the parts I was lacking. At first, I was going to make it like another 2D platformer game (your usual 2d platformer controls, with gravity and stuff). Luckily, talking with a friend of mine, he asked me about the design ideas referencing the snake game among other options, which lead me to the easier and logical (proper) way to do this: An array with pairs to contain the position of the image and the type of block (empty or full). [Thanks for that, you know who you are]

The idea is as you see below:

The array is a list of the elements stacked.
1: full block
0: empty block
(I knew I was going to need the id of each instance for later use with the images, but at first I just filled the array with one element per position. I simply printed the array on screen for the testing purposes.)

If you have any background on computer science, you might have guessed what comes next, scroll through elements as if you were to store a matrix. Count the elements, every X number is a row, each Position is a column. With that, you have the full stacker idea ready to be implemented.

Since I didn't write any of this process down properly (too many times doing this kind of matrix arrangements prior), I will print_debug one or two iteration of the game's code and explain it:

```
Number of columns: 7 | Number of blocks: 3
Array: [[1, 1578], [1, 1581], [1, 1584], [0, 1587], [0, 1589], [0, 1591], [0, 1593]]
```


As you can see, there is a list of 7 elements. This composes a line or row, with each pair of elements being "a column".  (take each pair as a block: full/empty and its Identification number)

After the idea of using an array, and the obvious variables, comes the code for the new row function:


```
    for i in (cols):
        if numNewBlocks > 0: # Add an Draw blocks to the row
            numNewBlocks = numNewBlocks - 1 # Update the num of blocks left to add
            # Draw block on screen, according to raw an column
            var newOcupy = ocupiedSquares.instance()
            add_child(newOcupy)
            newOcupy.position = Vector2((900-bCoordinates.x) - (line * bCoordinates.x), i * bCoordinates.y + 48)
            newOcupy.set_visible(true)
            gameWindow.push_back([1,newOcupy.get_instance_id()])
        else: # fill the rest of the row with empty spaces
```




You can see, it's just a loop appending one element each time. To make it efficient, It starts adding the full blocks left, leaving the remaining blocks empty until a row is completed, instead of juggling with numbers.

As soon as we have the new row, the next step is making the row blocks shift.

```
Array before the shift: [[1, 1535], [1, 1538], [1, 1541], [0, 1544], [0, 1546], [0, 1548], [0, 1550]]

Array after the shift: [[0, 1550], [1, 1535], [1, 1538], [1, 1541], [0, 1544], [0, 1546], [0, 1548]]
```



This time, the cols are shift/moved one position to the right, if it's the last one of the row, it goes back to the beginning of the row. I decided to go with circular shifting because it's easier to deal with and godot's methods to work with arrays felt more like a stack than an array, which meant, using pure iterators and side changing directions could make the complexity skyrocket. (if you want to try that, you need to track the head of the full block string/snake and the size, number of blocks, of the snake/string. then you use those to see where is shifting to).


```
func shiftRow(row):
    for i in refreshTime: pass # This is a hacky way to control the refreshing times, so it can be played by a human
    var rowStart = row * cols
    var elem1 = gameWindow.pop_at(rowStart) # current elem in check
    gameWindow.insert(rowStart, elem1)
    var elem2 = 0 # To avoid error (wrong element checked) with insertions
    for i in (cols):
        # Uptdate the array
        if (rowStart + i) < (rowStart + cols -1): # If we haven't reached the end of the row
            elem2 = gameWindow.pop_at(rowStart + i + 1) # after the first pop the same "i" is the next elem
            instance_from_id(elem1[1]).position = Vector2((900-bCoordinates.x) - (line * bCoordinates.x), (i + 1) * bCoordinates.y + 48)
            gameWindow.insert(rowStart + i + 1, elem1)
            elem1 = elem2
        else: # if this was the last element of the row
            elem2 = gameWindow.pop_at(rowStart) # first element of the row is the next elem
            instance_from_id(elem1[1]).position = Vector2((900-bCoordinates.x) - (line * bCoordinates.x), 0 * bCoordinates.y + 48)
            gameWindow.insert(rowStart, elem1)
            elem1 = elem2
```



So far it looks easy right? That's because it' already done haha. It's not rare to do silly mistakes like put one element
"front" when should be "back" and viceversa. Same for making comparisons with the id instead of the 1/0 for full/empty block. That's why debugging helps a lot, it makes you find and fix those mistakes.

Once we have the row filling and shifting, we can start with the big league, the stacking code. To make things easier to test and develope, we start making this synchronous and secuencial to the main process. This means, that any part of the mechanics done prior would be inside and at the end of this code snippet, so you can test the secuence step by step, instead of all at once.

Below it's an example of the results of making the stack row function once. The first array is composed by the first two original rows, the second is how the blocks changed according to the location they were then the button was pressed.

```
Array after the 1st stack: [[1, 1605], [0, 1608], [0, 1610], [0, 1612], [0, 1614], [1, 1599], [1, 1602]]

   pressToStack()
Array after the 2nd stack: [[1, 1605], [0, 1608], [0, 1610], [0, 1612], [0, 1614], [1, 1599], [1, 1602], [0, 1654], [0, 1656], [0, 1658], [0, 1660], [0, 1645], [1, 1648], [1, 1651]]

   pressToStack()
Array after the 3rd stack:[[1, 1605], [0, 1608], [0, 1610], [0, 1612], [0, 1614], [1, 1599], [1, 1602], [0, 1654], [0, 1656], [0, 1658], [0, 1660], [0, 1645], [1, 1648], [1, 1651], [0, 1690], [0, 1693], [0, 1696], [0, 1698], [0, 1700], [0, 1702], [0, 1704]]

```


See how those who fell are now 0s, and those stacked are still 1s? (the number of remaining full blocks has reduced accordingly too)


Let's see the code snippet now. However, since I am using the final version, you'll see only the finalized function code. In any case, this is how you decide what stacks or falls. (The part about new instances and drawing would have come after the comparisons are properly working to avoid confusion).


```
    if line != 0: # The first line always stacks
        # Compare previous and current row to see what blocks that stack and update accordingly
        var lineStart = line * cols
        var prevLineStart = (line - 1) * cols
        for i in (cols): # Compare the elements of both rows one by one
            var elem1 = gameWindow.pop_at(prevLineStart + i) # grab and save the elem to compare
            gameWindow.insert(prevLineStart + i, elem1) # but don't modify the array
            var elem2 = gameWindow.pop_at(lineStart + i) # grab and save the elem to compare
            # COMPARE FIRST ELEMENT OF THE PAIR, THE 2 ONE ARE ALWAYS DIFFERENT IMAGE INSTANCES
            if elem1[0] == elem2[0]: # elem was either empty or stack, so reinsert in same place
                gameWindow.insert(lineStart + i, elem2)
            if elem1[0] != elem2[0]: # always empty if they are different
                if elem2[0] == 1: # If it was a block falling,
                    soundOFF()
                    if !$Sounds/BGM.is_playing():
                        $Sounds/BGM.stream = falling
                        $Sounds/BGM.play()
                    blocks = blocks - 1 # update the counter
                    elem2[0] = 0
                    instance_from_id(elem2[1]).get_node("Image").set_visible(false)
                    instance_from_id(elem2[1]).get_node("Image2").set_visible(true)
                # Insert the empty block in the row
                gameWindow.insert(lineStart + i, elem2)
```



After that, as you see, you use the current line, the screen size, and the blocks size to calculate the coordinates to draw the selected block for each iteration of the loop. Then you use the 0 or 1 to decide which type of node (ocupied, empty), and which image of them display (full, falls, empty)


When I finished that part, It was time for betatesting. Besides, I was lucky and I had a meeting with a few friends, who were kind and eager to try the game (betatesters, yay!). Me and them played the game and came to conclusion that it was unclear what each block was, hence why the 3 blocks had to be named in a screen prior to the game.



Here, the code for the game's algorithym was done. However the program can have a bit more. We can put some of the variables as global, so they can be autoloaded and accessed through the settings menu. This lets the player or dev create an editor for the game, which you can use to select how many columns and blocks you want, and/or how much speed you want them to move.


```
extends Node


# Global variables, to make them editable via settings
var cols = 7
var blocks = 3 # number of currently moving blocks
# Time for the shift refresh (hacky method I had to come up with), I know it eats resources
var refreshTime = 900000 # use refreshTime = 300000 for the vita
```



Each var will be updated with 2 buttons, one for up and another for down. Here's the cols example:


```
func _on_ColsUP_pressed():
    if cols < 7:
        cols += 1
        get_node("/root/Autoload").cols = cols
        get_node("RectangleCols/Cols").text = "COLUMNS\n" + str(cols)


func _on_ColsDOWN_pressed():
    if cols > 2:
        cols -= 1
        get_node("/root/Autoload").cols = cols
        get_node("RectangleCols/Cols").text = "COLUMNS\n" + str(cols)
# Fix block limits after cols edits, for limit cases
    if blocks >= cols:
        blocks = cols -1
        get_node("/root/Autoload").blocks = blocks
        get_node("RectangleBlocks/Blocks").text = "FULL BLOCKS\n" + str(blocks)
```



And that summarized the project of the stacker. You can go to the github to read and try the full projects code, and also play the released game.


Gihub's link to the full project (and releases): https://github.com/Bunkai9448/StackerVitaGodot

No comments:

Post a Comment