Dana Vrajitoru
I254/C297 2D Games Programming

I254/C297 Lab 8 / Homework 8

Date: Wednesday, March 6, 2024.
Due date: Wednesday, March 20, 2024.

In this lab and homework, we will create match-3 bubble tap game similar to Bubble Brush. We will instantiate and delete objects dynamically through the code.

Lab Part

Ex. 1. In this lab we'll create a match-3 bubble tap game inspired from Godot's instantiating 2D demo: github.com/godotengine/godot-demo-projects/tree/3.5-9e68af3/2d/instancing.

Create a project called BubbleTap and a scene called main inside. Create a Node2D object called Root in the scene. Switch the scene editor to 2D.

a. Setup

Download the file

hw8.zip

Extract the files and add them to the project.

In Project Settings - Window, set the size of the window as 800x800, or something smaller if it doesn't fit on your screen. Also in Project Settings, Rendering, Environment, set the clear color as a dark gray. Then set the Project Settings, Physics, 2D, Gravity to 300.

Create a CharacterBody2D object call Bubble, with a Sprite2D bubble object as the child (use any of the bubble images) and call it Sprite. Add another child to the bubble of type CollisionShape2D. Set the scale of the sprite as .5 and align it with the top left corner of the screen. Then set the collision shape as a circle and adjust the position and radius to fit over the bubble.

Add a script to the Bubble object (not the sprite) derived from CharacterBody2D. Remove the jump and the horizontal move based on input from the user. Basically, we just set the velocity of the object based on gravity and then call move_and_slide.

Add a variable to the class called color and initialize it as 0. At this point, save the bubble as a scene.

Create a StaticBody2D object called Wall and add a Polygon2D node to it as a child. Create a box around the area that has an opening at the top, but leave an empty area below it where you can fit a button and some labels. . Set its color in the CanvasItem area, the Modulate box. Then add a CollisionPolygon2D child of the wall and draw it to fit over the visible polygon.

Move the bubble object towards to center of the area.

b. Bubble Script

Let's change the image displayed by the sprite in the code. This way, we can create multiple objects of type Bubble and each of them can display a different image. First, let's declare the image resource in the class. At the top of the class, below extends..., add the following declaration:

const bubbleBlue = preload("res://bubbleBlue.png")

Add a similar one for three more colors of your choice. Then add the function _ready:

func _ready():
    var sprite_ref = find_child("Sprite")
    sprite_ref.texture = bubbleGreen

or use another one of the colors you chose if you didn't define green at the top. Run the program to see if the color of the bubble changes when you start out.

Let's randomize the color of the bubbles in the function _ready. Add a static random number generator to the class. Then in the function _ready, generate a number between 0 and 2 and assign it to the color variable. Then add a conditional where if the color is 0, and set the sprite with the first color you chose, and if it's 1, set it with the second color, and if it's 2, the third one. Run the program a few times to see if you get different colors.

Rename the existing Bubble object as Bubble1. Use the bubble scene to create 5 more bubbles placed randomly over the area. Run the program to test this.

Add two more variables in the class: one called id initialized as 0, and another one called link, initialized as -1. Initialize the id in the function _ready the following way:

id = int(name.replace("Bubble", ""))

Now, when two bubbles collide and they have the same color, we're going to link them together. Add the following code to the function _ready before move_and_slide:

var collision_info = move_and_collide(velocity * delta, true)
if collision_info:
    var bubl = collision_info.get_collider()
    if not bubl.name.contains("Wall"):  
        if color == bubl.color:
            if id > bubl.id:
                link = bubl.id
            else:
                 bubl.link = id
            print(id, " --> ", link)
            print(bubl.id, " --> ", bubl.link)

Test this a few times (make sure some of your bubbles are close enough horizontally to end up colliding when they fall).

Now, let's randomize the initial position of the bubbles so that they start from above the screen and fall down. In the function _ready, assign to the position a random value between 40 and 760, and a y between -30 and -400.

c. Game Manager Script

Add a script to the root object. In this class, declare a variable called bubbles and initialize it with an empty array ( [] ). Then add a reference to the bubble scene:

const bubbleScn = preload("res://bubble.tscn")

Let's collect references to all the bubble objects already created. In the function _ready, add the following code:

for child in get_children():
    if child.name.contains("Bubble"):
        bubbles.append(child)
print(bubbles)

We want to keep the array bubbles sorted by the id, to make looking for ids more efficient, as well as selecting the sets of continuous color. Add the following code:

func bubble_index(id):
    var first = 0
    var last = len(bubbles)-1
    var mid := 0
    while first <= last:
        mid = (first + last) / 2
        if bubbles[mid].id == id:
            return mid
        elif bubbles[mid].id > id:
            last = mid - 1
        else:
            first = mid + 1
    return mid
    
func add_bubble(b):
    if len(bubbles) == 0:
        bubbles.append(b)
        return
    var i = bubble_index(b.id)
    if b.id > bubbles[i].id:
        bubbles.insert(i+1, b)
    else:
        bubbles.insert(i, b)

The first function is what we call a binary search, which is an efficient search for a value in a sorted array. The second function adds the bubble to the array at an index that keeps the array in order of the id.

Replace the call to append with a call to add_bubble(child).

After this, let's add 20 more bubble objects dynamically. Add the following loop in the function _ready:

for i in 20:
    var bubl = bubbleScn.instantiate()
    add_child(bubl)
    bubl.id = i + 10
    add_bubble(bubl)

d. Mouse Click

Next, we want to be able to detect a mouse click over a bubble. When that happens, we want to collect not only that bubble, but any other bubble connected to it directly or indirectly that has the same color.

Open the bubble scene and click on the Bubble object (root of the scene). In the Inspector, expand Input, and inside, turn on the Pickable property. This will allow the bubble to detect the mouse over it and report mouse clicks.

Then click on the Node tab and connect the signal input_event to a function in the bubble script (default name is fine). In this function, add the following:

if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT \
and event.is_pressed():
    print("Clicked on ", id)

The bubble clicked on can deal with its own fate, but to be able to remove multiple bubbles, we need the root script to be notified. Let's add the following functions to the main script:

func get_bubble(id):
    var i = bubble_index(id)
    if bubbles[i].id == id:
        return bubbles[i]
    else:
        return null

func get_top_link(b):
    while b and b.link != -1:
        b = get_bubble(b.link)
    return b

func merge_sets(b1, b2):
    var top1 = get_top_link(b1)
    var top2 = get_top_link(b2)
    if not top1 :
        top1 = b1
    if not top2:
        top2 = b2
    if top1.id < top2.id: 
         top2.link = top1.id
    elif top1.id > top2.id: 
        top1.link = top2.id

func pick_bubble(bubble):
    var top_link = get_top_link(bubble)
    for i in range(len(bubbles)-1, -1, -1):
        var b = bubbles[i]
        if get_top_link(b) == top_link:
            bubbles.erase(b)
            b.queue_free()

Note that on the last else (not included) in merge_sets, the two bubbles already belong to the same set because they have the same top bubble, so we don't need to merge them.

Then in the function _physics_process, replace the code comparing the ids and setting the link with the call to

root.merge_sets(self, bubl)

Create a reference to the Root object in the script bubble called root (drag-ctrl-release). Then in the function input_event, in case of a mouse click, call the function

root.pick_bubble(self)

Now when you click on a bubble, both that bubble and any other bubbles that touch it of the same color or are part of a continuous chain of bubbles of the same color will be removed.

The detected sets when we click on a bubble should work fine now.

Homework Part

Ex. 2. a. Score

Add a score variable to the class main and initialize it as 0 in the function _ready. Then in the function pick_bubble, count how many bubbles are being removed, and add the square of that value to the score. Add a label to display the score and update it after adding something to it.

b. New Game Button

Add a button to the scene starting a new game, and link it to a function in the main script doing

get_tree().reload_current_scene()

c. Number of Bubbles

Adjust the number of bubbles created in the main script so that they fill the play area. Optional, a timer event can be added to the main script where a new bubble is released every 2-3s.

Homework Submission

Take a screen shot of your running program, showing the content of the screen while the game is running, and save it as png or jpg. Create a zip file of the project folder (the screenshot can be inside). Submit both of them to Canvas, Assignments - Homework 8.