LD46: Squishy squishy

It’s so squish!

That is not at all what I intended, but I kind of love it, so for the moment, it stays in.

To get this far, I had a heck of a time trying to figure out Godot’s physics engine, but I’m learning quickly!

Define the blocks

First, we have to figure out what the blocks look like. From Wikipedia:

So I have 7 shapes to define, each of which has four ‘blocks’ inside of it. So first I layed out a graphic:

Now I need to define how they relate in code. First, I was going to define what the neighbors of each was:

In this case, that means that in an I block, 1 has top, right, bottom, left neighbors of _ (none), _, 2, _ and so on. Seems verbose. So then I realized that I can calculate the neighbors myself. I just need to determine where they all are:

That’s much cleaner. It also includes the ‘central point’ of the shape that I will rotate around (which I don’t end up using).

Turning that into code:

# Tetromino.gd

# Define the shapes, values are the four coordinates and then a center point to rotate around
const SHAPES = {
	'I': [Vector2(0, 0), Vector2(0, 1), Vector2(0, 2), Vector2(0, 3), Vector2(0.5, 2.0)],
	'O': [Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(1.0, 1.0)],
	'T': [Vector2(0, 0), Vector2(1, 0), Vector2(2, 0), Vector2(1, 1), Vector2(1.5, 0.5)],
	'J': [Vector2(1, 0), Vector2(1, 1), Vector2(1, 2), Vector2(0, 2), Vector2(1.5, 2.5)],
	'L': [Vector2(0, 0), Vector2(0, 1), Vector2(0, 2), Vector2(1, 2), Vector2(0.5, 2.5)],
	'S': [Vector2(0, 1), Vector2(1, 1), Vector2(1, 0), Vector2(2, 0), Vector2(1.5, 0.5)],
	'Z': [Vector2(0, 0), Vector2(1, 0), Vector2(1, 1), Vector2(2, 0), Vector2(1.5, 0.5)]

From there, the actual hard part. I have the 4 Blocks already created (in the [previous post]https://blog.jverkamp.com/2020/04/18/ld46-tetris-is-working-sort-of/), but I have to move them around and stick them together:

# Tetromonio.gd

onready var bodies = [

# Helper functions to get the correct child nodes by index
func block(i):
	return get_node("Block" + str(i))
# Initialize a random shape
func init_random():
	var names = SHAPES.keys()
	var name = names[randi() % names.size()]

# Initialize the shape with one of the names
func init(shape):
	assert(shape in SHAPES)
	# Set positions
	for i in range(4):
		block(i).position = 16 * SHAPES[shape][i]
	# Get neighbors and create joints
	for i in range(4):
		for j in range(4):
			var xi = SHAPES[shape][i].x
			var yi = SHAPES[shape][i].y
			var xj = SHAPES[shape][j].x
			var yj = SHAPES[shape][j].y
			var bi = block(i)
			var bj = block(j)
			# Each only has to do one way, since we'll catch the other when bi  and bj swap
			var joined = false
			if xi + 1 == xj and yi == yj:
				bi.get_node("Body/PixelEngine").neighbors['right'] = bj
				joined = true
			elif xi - 1 == xj and yi == yj:
				bi.get_node("Body/PixelEngine").neighbors['left'] = bj
				joined = true
			elif xi == xj and yi + 1 == yj:
				bi.get_node("Body/PixelEngine").neighbors['bottom'] = bj
				joined = true
			elif xi == xj and yi - 1 == yj:
				bi.get_node("Body/PixelEngine").neighbors['top'] = bj
				joined = true
			# Only add joints one direction
			if joined and i < j:
				var midpoint = (bi.position + bj.position) / 2
				var pins = []
				if xi == xj:
					pins.append(midpoint + Vector2(0, 8))
					pins.append(midpoint + Vector2(0, -8))
					pins.append(midpoint + Vector2(8, 0))
					pins.append(midpoint + Vector2(-8, 0))
				for pin in pins:
					var joint = PinJoint2D.new()
					joint.position = pin

					joint.node_a = joint.get_path_to(bi.get_node("Body"))
					joint.node_b = joint.get_path_to(bj.get_node("Body"))

I know right?

It’s some pretty wacky code. Except for the joints, it came together pretty quickly. The joints proved to be quite the headache though. First, I was using a KinematicBody2D for the block, which … doesn’t really work with joints at all? So I switched to RigidBody2D . But that means I have to change the physics system (more on that in a bit). Then I made the joints, but with only one pin, so they rotated all over the place. It turns out what I really want is to join each pair of blocks on the two corners touching.

So, rewriting the input/physics engine. In a nutshell:

# Tetromino.gd

func _physics_process(delta):
	# Keyboard controls
	for body in bodies:
		if Input.is_action_pressed("ui_right"):
		if Input.is_action_pressed("ui_left"):
		if Input.is_action_pressed("ui_up"):
			body.apply_central_impulse(-0.1 * GRAVITY)
		if Input.is_action_pressed("ui_down"):
		if Input.is_action_pressed("ui_rotate_left"):
		if Input.is_action_pressed("ui_rotate_right"):
	var settled = true
	for body in bodies:
		if not body.sleeping:
			settled = false
	# If we hit something, start a counter, if that goes long enough, lock the block
	if settled:
		velocity = Vector2.ZERO
		stuck_time += delta
		if stuck_time > LOCK_TIME:
		stuck_time = 0

I actually really like how RigidBody2D s work. I should play with those more. I do have to apply the forces to all of the 4 blocks, otherwise they start rotating like mad when you don’t want them to (I mean, I do want them to, but not by themselves!).

The body.sleeping actually works pretty well. Sometimes it gets stuck, but I can push that down the road a bit. I think I’ll add a keybinding to Let It Go1 and move on to the next block without waiting.

It… works?

What we do end up getting though is amusingly squishy, which I love. I’m totally going to keep it for now.

Anyways, off to a block, and then it’s time to actually add some falling sand!

EDIT: I was working on adding a few keybindings (ESC restarts and ENTER forces you to the next tile) and this happened:

is_action_**just**_pressed y’all. That is all.

  1. I finally got around to watching Frozen 2. The movie is insane, but I actually realy enjoy the music. Man it’s stuck in my head. ↩︎