Category Archives: Dev Blog

pongodot day 9: trail

Today’s post will be short. I went and saw The Batman tonight, it was really good.

After getting home I wanted to try and add something quick to the game, so I decided I wanted to add a simple particle trail behind the ball.

This would be super simple in Unity, however, as far as I could find in Godot it gets a bit more simple. Shout out to picster on youtube, who made a great tutorial on this very subject that I based my trail on.

this was the basis of my task tonight

However, his full tutorial was way more complicated and pretty than I either had time to implement or really needed, but my trail is built off the same principle.

I made a trail scene, with the base node being a Line2D, the curve is similar, as is the gradient, but my texture is just a solid block, rather than the smokey texture he used for his smoke trails.

inspector

My script is also a lot more bare bones than his. Also, since my ball is moving constantly and I didn’t want to have an endless stretching trail behind it, I made a change to where I delete the old points after they had been around long enough. My full trail script is below.

extends Line2D

export var max_age = 2

var age_of_points = [0.0]


func _ready():
	set_as_toplevel(true)


func _process(delta):
	if age_of_points[0] > max_age:
		age_of_points.remove(0)
		.remove_point(0)
	for p in range(get_point_count()):
		age_of_points[p] += 5 * delta

func add_point(point_pos:Vector2, at_pos = -1):
	age_of_points.append(0.0)
	.add_point(point_pos, at_pos)

func _on_Decay_tween_all_completed():
	queue_free()

I also made a small change to the ball scene, linking in the trail under it, and adding a call to app_point in the trail as well.

extends KinematicBody2D

export (int) var speed = 300
export (int) var increase_by = 50
var current_speed

var velocity = Vector2()

onready var trail = $Trail

func _init():
	velocity.x = 1
	velocity.y = 1
	current_speed = speed

func _process(delta):
	trail.add_point(global_position)
	var move_by = velocity.normalized() * current_speed
	var coll = move_and_collide(move_by * delta)
	if (coll != null):
		var hit = coll.get_collider()
		if (hit.is_class("KinematicBody2D")):
			$PaddleHitEffect.play()
			handle_paddle_reflect(self, hit)
			current_speed += increase_by
		else:
			$WallHitEffect.play()
		velocity = velocity.bounce(coll.normal)

func handle_paddle_reflect(ball : KinematicBody2D, paddle: KinematicBody2D):
	var is_pos = velocity.y > 0
	if (velocity.y == 0):
		is_pos = ball.position.y > paddle.position.y
	var hit_from_center = abs(ball.position.y - paddle.position.y)
	if (hit_from_center <= 5):
		velocity.y = 0
	elif (hit_from_center <= 25):
		velocity.y = 1 if is_pos else -1
	elif (hit_from_center <= 75):
		velocity.y = 1.5 if is_pos else -1.5
	else:
		velocity.y = 2 if is_pos else -2

And with that I have a simple trail added to the ball as it bounces around.

trail effect

-Ike

pongodot day 8: sound

It’s time to add some noise to the game.

To start I made some sound effects using jsfxr, a free sound effect generator. You can check it and play around with it here.

jsfxr, a javascript port of the original sfxr, which uses flash so no longer works on modern browsers

I made 5 sound effects I intend to add to the game, a wall hit, a paddle hit, a score effect, as well as sound effects for when you start up the game and when you lose. I also wanted to add one for victory, but couldn’t generate anything I liked.

In Godot audio has been written with games in mind according to their documentation, and apparently is broken up into two main components: Audio Buses and Audio Streams.

Audio is something I never really understood in Unity, so I’m in the dark here on trying to connect it to things I already know.

For my purpose here, though, I think I can get away with only using the master Audio Bus, without worrying too much about the rest.

Audio Streams are basically the audio source, in other words, what will play my sounds. Reading into them, it seems there are different kinds of Audio Streams: AudioStreamPlayer, AudioStreamPlayer2d, and AudioStreamPlayer3d. We don’t care about positional sound, so we just need AudioStreamPlayer.

To start, let’s hook up the start game effect.

nothing super impressive, but it’s there.

I’ve generated this WAV file that’s been imported. Now, in the scene we add in a new AudioStreamPlayer for this effect.

In the inspector I will set the stream by selecting new AudioStream sample, then drag my wav file over.

Now for the code. I just need to tell the effect to play when the player presses start.

in GameController.gd

And that works perfectly! Now to do that with the other effects.

For the wall and paddle hit sfx this gets a bit trickier, since the GameController does not know when this happens. Instead, in the Ball game scene we can add those in.

Scene layout and code at once.

After a quick test I decided to switch them, I like them better the other way around, but here are the effects anyway:

wall_hit.wav – used for paddle hits
paddle_hit.wav – used for wall hits

Scoring and game over were relatively simple as well, as that code is also centralized in the Game Controller.

point.wav – yes, I know it’s coin pickup
lose.wav – GAME OVER
I should eliminate the duplicate code on a future refactor.

Some inefficiencies in here, for sure, which I hope to tackle in a refactor at some point, but for now I’m treating this like quick and dirty prototyping.

With that I have the sound effects all working. I still want to add a few more, one for launching the ball, for instance, and one for victory, but those can wait for another day.

-Ike

pongodot day 7: GAME OVER

Today’s post is going to be pretty short. The goal today was to add a start screen and a victory/defeat condition.

For the start screen, into our main Scene tree I added in a new label, with very similar settings to the first. From code I can easily turn this label on and off. As a child node to the main label I added another label with smaller text that says “Press R to Start” to tell the player how to start the game. Now when the game boots up this is what you see.

PRESS R TO START

From here I added a bit of code to the game controller and to the PlayerPaddle that essentially makes it so nothing happens when the game is not in an “active state”

This is the new process loop.
the _reset() call earlier handles starting up a new game and setting the paddle to active, basically

Player Paddle.gd has a similar change with a new variable, is_active that we check in _process before obeying any input.

In that first block you’ll likely notice a check to a new action as well. In our input map I also added an action to start the game by pressing ‘r’. In the future I might just instead have it also happen on spacebar, but for now this works.

back in GameController.gd

Here I’ve added in a max_score, set to 3 for now for testing purposes, as well as a center_text variable. This is where I can easily set the text for that main label, like you see in the above screengrab.

Finally, at the bottom of GameController.gd there is some logic that handles checking for victory/loss.

as well as changes to our goal triggers

And with that it’s now possible to win (or lose).

3 is low for a score goal, but this is just for testing.

Sorry for the condensed nature of today’s post, but I didn’t have a lot of time for today’s changes. Hopefully future posts won’t be as rushed.

Here is now the current version, in full, of the GameController script.

extends Node2D

var ballscene = load("res://Ball.tscn")
onready var comPaddle = get_node("Com Paddle")
onready var playerPadd = get_node("Player Paddle")
var current_ball

var is_active

var center_text

export (int) var max_score = 3
var playerScore = 0
var comScore = 0

func _init():
	is_active = false
	center_text = "PONGODOT"

func _process(_delta):
	if (!is_active):
		$CenterLabel.visible = true
		$CenterLabel.text = center_text
		if Input.is_action_just_pressed("start"):
			_reset()
	if (is_active):
		$CenterLabel.visible = false
		$PlayerScoreLabel.text = str(playerScore)
		$ComScoreLabel.text = str(comScore)
		if Input.is_action_just_pressed("launch"):
			if current_ball != null:
				return
			current_ball = ballscene.instance()
			current_ball.position.x = playerPadd.position.x + 20
			current_ball.position.y = playerPadd.position.y
			add_child(current_ball)
			comPaddle.ball = current_ball

func _end_game():
	is_active = false
	playerPadd.is_active = false

func _reset():
	playerScore = 0
	comScore = 0
	is_active = true
	playerPadd.is_active = true

func _delete_ball():
	if current_ball != null:
		current_ball.queue_free()
		current_ball = null
		comPaddle.ball = null

func _on_PlayerGoal_body_entered(body):
	_delete_ball()
	comScore += 1
	_check_victory()

func _on_ComGoal_body_entered(body):
	_delete_ball()
	playerScore += 1
	_check_victory()

func _check_victory():
	if playerScore >= max_score:
		center_text = "You Win!"
		_end_game()
	elif comScore >= max_score:
		center_text = "Game Over"
		_end_game()

See you tomorrow

-Ike

pongodot day 6: scoring

Time to handle scoring. The first step is to create goals. For this we are going to use an Area node, attached to it is a CollisionShape2D that defines the region. We create one of these on each side of the field. Not sure why I’m saying we, I did it.

GOOOOOOOOOOOL!

Now, it’s time to learn a little bit about signals in Godot. Signals are Godot’s version of an observer pattern, they allow you to have a trigger on one node be reacted to from another. According to Godot’s documentation this is to help limit coupling and keeps your code more flexible. In our use case here, for example, we can send a signal from the goals to the game controller, allowing it to react to the ball entering the zone.

We’ll start with the player goal. In the editor we’ll go to the node tab to expose the available signals.

Double clicking on body_entered brings up this screen, where we hope to connect to the script on the game scene Node.

Not really a need to change the receiver method name, so I click Connect.

And now the editor has added a new function to our GameController script where we’ll process the signal.

For testing purposes we’ll start with something simple. When the ball enters the area, we’ll delete it. Also, I’m going to take this chance to fix some of last week’s tech debt, and make it so only one ball exists at a time.

comPaddle and ball are a bit too coupled at the moment, so it’s causing extra headaches.

With that we can now delete the ball, as well as prevent another ball from being fired while a ball is active.

Next, time to add a score. For this we are going to have our GameController handle and track the score for both the player and the com. When the PlayerGoal is entered we need to make sure to increment the Com score (and vice-versa for the other goal).

Next we need a way to display the score. I spent a bit of time to download a free pixel font, and spent a bit more time googling and learning how to import that font into Godot, but I finally got my two labels made, and I’m pretty happy with the look of them. Next I just need to change the label text for each to match the current score value associated with it.

$PlayerScoreLabel is another way in code to get the Node called PlayerScoreLabel

With this it’s just a matter of pressing go and seeing if it works.

And, almost immediately I ran into a problem.

No, this one isn’t a gif

Before even launching my first ball the score is already 3 to 2. It took me a bit to figure out the problem which is that my paddles are technically partially inside my scoring area, so they are triggering the scores to go up before the initialization even finishes…

The easy solution would be to simply move the paddles slightly so that they are not inside the score zone. The better solution was to learn a little bit about collision layers and masks in Godot.

By setting up separate collisions layers for the paddles and balls, and making the score area only care about the ball layer with a mask, it should solve my issue.

setting up the layers in project settings

In project settings I made three layers, named Paddles, Ball and Goals.

Player Paddle

In the inspector I can set the collision layers and masks for different object. For my Paddles I set them on layer 1, and have them mask on 1 and 2.

Ball

For the ball, it lives on layer 2, and has a mask on all three layers.

goals

And then the goals live on layer 3, and only listen to layer 2.

And that fixed my issue. Now, when I start up the game the score is 0 to 0. As much as I would have appreciated a bit of a handicap in my favor, better to fix the issue and have everything behave as I want it to.

And with that, we can finally play Pong!

pong

After each ball pressing spacebar will fire another ball from the player side. Of course, you can’t win or lose yet, as you play the scores will just increase with no real end to the game. But, that’s a problem for another day. Here is the final code for my GameController script after today:

extends Node2D

var ballscene = load("res://Ball.tscn")
onready var comPaddle = get_node("Com Paddle")
onready var playerPadd = get_node("Player Paddle")
var current_ball

var playerScore = 0
var comScore = 0

func _init():
	playerScore = 0
	comScore = 0

func _process(_delta):
	$PlayerScoreLabel.text = str(playerScore)
	$ComScoreLabel.text = str(comScore)
	if Input.is_action_just_pressed("launch"):
		if current_ball != null:
			return
		current_ball = ballscene.instance()
		current_ball.position.x = playerPadd.position.x + 20
		current_ball.position.y = playerPadd.position.y
		add_child(current_ball)
		comPaddle.ball = current_ball

func _delete_ball():
	if current_ball != null:
		current_ball.queue_free()
		current_ball = null
		comPaddle.ball = null

func _on_PlayerGoal_body_entered(body):
	_delete_ball()
	comScore += 1

func _on_ComGoal_body_entered(body):
	_delete_ball()
	playerScore += 1

Tomorrow I plan to add victory/defeat, and maybe a start screen.

-Ike

pongodot day 5: ball launching

Before getting into the meat of today’s changes, two small changes I made yesterday after I finished the main bulk of yesterday’s changes.

First off I wanted to make it so the ball reacts differently when you hit it with different parts of the paddle. I did this by comparing the height of the ball to the height of the KinematicBody2D that it collides with. Since only the paddles have that Node type I was able to check that I had hit a paddle with is_class()

lines 25-26 are a weird solution to a particular edge case that I’m not going to go into right now

I might play around more with the exact thresholds for where on the paddle we get different angles, but I was happy with this change.

The other small change I did was to add some sprites down the middle of the playfield.

No, this one doesn’t move.

Originally, my plan for today was to get the scoring mechanisms added in, but as I started to break down what that required I realized that it was a bit bigger of a task than my mantra of “one small change a day” for this month-long project. So, I’m breaking that down into smaller chunks.

Today, my goal is to be able to spawn a new ball into the scene, and have everything work. After a point is scored the ball is destroyed and then it is launched by the side that scored. We could simply move the ball, however, I think the system works better if we are able to spawn the ball anew.

So, to start with, we need to create a ball to spawn. There is one in our scene already, but this isn’t really what we are needing. Drawing on my Unity experience, in Unity I would here create a prefab, and it turns out, in Godot we do something similar.

In Godot everything is nodes. Our game scene is really just a node. So, what we need to do is essentially create a new scene that contains our ball node that we can then load in. This works similar to prefabs in Unity, if Unity Scenes were also basically prefabs.

Ball is it’s own scene now.

So, I copied the structure of the Ball in our previous scene, and created a new scene, Ball.tscn. This new scene is where our ball will be housed. Next, we need a way to spawn the ball in.

It’s time for a game controller. In the Game Scene, on the root node I added a new script, called GameController. This script will be responsible for tracking interactions between nodes, as well as eventually tracking and displaying the score. For now, however, it will simply spawn the ball.

short and sweet.

So, let’s go ahead and run the scene and see what happens.

ball is spawned in by the script at the start.

Now, you might notice this broke something. Our “COM” paddle was just following the ball, however, since there is no ball in the tree at the start it’s unable to get it, so we need to instead be able to tell the paddle that there is now a ball available.

We tell the paddle what the ball is

With this change, the com paddle is now able to properly follow the ball. But! We can still do better. Instead of simply spawning the ball at a given location at the start, we instead use that input we cleverly set up yesterday and didn’t use. We’ll wait until the player presses space, and then launch the ball a point a few pixels away from the player paddle. That script looks like this.

no, your script names are inconsistent when it comes to presence of spaces.

Alright, so I’m not totally confident this will work, but let’s go ahead and try it.

working as intended.

Alright, so you might have noticed a few problems there. I should probably instead check for the first frame the spacebar is pressed, and only spawn then, rather than, you know, every frame.

Some quick fixes to the script and it now looks like this:

Should be better.

And with that, the issue is fixed. I can still press the spacebar multiple times to spit out additional balls, but fixing that (as well as deleting balls when you score) is tomorrow’s problem.

there we go.

-Ike