Kasper Andersson Brandt


Game Programmer

GitHub LinkedIn CV


Untitled Block Game


Sandbox
Solo project
Unity | C#



Download on itch.io

View full source code on GitHub

This was just a small project I made in a couple of days, because I thought it would be fun. I was only really interested in programming the building and breaking mechanic, so I used the first person character controller from the Standard Assets for movement.

I wrote two scripts: Builder.cs and Block.cs. The builder script sits on the same game object as the camera, and does a raycast forwards every update to see if it finds a block within building distance.


Ray ray = new Ray(transform.position, transform.forward);
if (Physics.Raycast(ray, out RaycastHit hit, buildDistance)) {
	Block block = hit.transform.GetComponent<Block>();
	if (block) {
		Vector3 newBlockPosition = showBlockHover(hit.transform.position, hit.point);
		if (Input.GetMouseButtonDown(0)) {
			addBlock(block.id);
			block.breakBlock();
		} else if (Input.GetMouseButtonDown(1) && blocks[selectedBlock].amount > 0) {
			blocks[selectedBlock].amount--;
			blockAmount.text = blocks[selectedBlock].amount.ToString();
			GameObject newBlock = Instantiate(blocks[selectedBlock].block.gameObject, newBlockPosition, hit.transform.rotation);
			newBlock.GetComponent<Block>().placeBlock();
		}
	} else {
		blockHover.enabled = false;
	}
} else {
	blockHover.enabled = false;
}
		

Left-clicking on a block within distance breaks the block and adds a block of the same type to the player's inventory. The block type is identified by a string in the block script.


private void addBlock(string id) {
	for (int index = 0; index < blocks.Length; index++) {
		if (blocks[index].block.id == id) {
			blocks[index].amount++;
			blockAmount.text = blocks[selectedBlock].amount.ToString();
			return;
		}
	}
	Debug.LogError("Unknown block ID: " + id);
}
		

Right-clicking on a block within distance places a new block of the currently selected type on the hovered block side. The position of the new block is calculated in the same function that calculates the position for the indicator showing which block side is hovered.


private Vector3 showBlockHover(Vector3 blockPosition, Vector3 hitPosition) {
	blockHover.enabled = true;
	Vector3 newBlockPosition = blockPosition;
	Vector3 hoverPosition = blockPosition;
	Vector3 difference = blockPosition - hitPosition;
	Vector3 distance = new Vector3(Mathf.Abs(difference.x), Mathf.Abs(difference.y), Mathf.Abs(difference.z));
	if (distance.x > distance.y && distance.x > distance.z) {
		blockHover.transform.eulerAngles = new Vector3(0.0f, 0.0f, 90.0f);
		if (difference.x < 0.0f) {
			newBlockPosition.x += 1.0f;
			hoverPosition.x += 0.505f;
		} else {
			newBlockPosition.x -= 1.0f;
			hoverPosition.x -= 0.505f;
		}
	} else if (distance.y > distance.z) {
		blockHover.transform.eulerAngles = new Vector3(0.0f, 0.0f, 0.0f);
		if (difference.distance.y < 0.0f) {
			newBlockPosition.y += 1.0f;
			hoverPosition.y += 0.505f;
		} else {
			newBlockPosition.y -= 1.0f;
			hoverPosition.y -= 0.505f;
		}
	} else {
		blockHover.transform.eulerAngles = new Vector3(90.0f, 0.0f, 0.0f);
		if (difference.z < 0.0f) {
			newBlockPosition.z += 1.0f;
			hoverPosition.z += 0.505f;
		} else {
			newBlockPosition.z -= 1.0f;
			hoverPosition.z -= 0.505f;
		}
	}
	blockHover.transform.position = hoverPosition;
	return newBlockPosition;
}
		

The builder script's update function also handles the player input for changing block selection, and updates the UI accordingly. Blocks in the players inventory are represented by an array of the struct InventoryBlock, which holds a reference to a block prefab and the amount of blocks the player has. This array is filled with block prefabs in the editor. The block UI is made up of seven cube renderers on a camera space canvas, with their own directional light.


//inside the update function
	if (Input.GetKeyDown("q") || Input.mouseScrollDelta.y < 0.0f) {
		selectedBlock--;
		if (selectedBlock < 0) {
			selectedBlock = blocks.Length - 1;
		}
		updateBlockUI();
	} else if (Input.GetKeyDown("e") || Input.mouseScrollDelta.y > 0.0f) {
		selectedBlock++;
		if (selectedBlock >= blocks.Length) {
			selectedBlock = 0;
		}
		updateBlockUI();
	}
//-----

private void updateBlockUI() {
	blockUI.material = blocks[selectedBlock].block.GetComponent<Renderer>().sharedMaterial;
	blockAmount.text = blocks[selectedBlock].amount.ToString();
	int blockIndex = selectedBlock;
	for (int index = 0; index < blockUILeft.Length; index++) {
		blockIndex--;
		if (blockIndex < 0) {
			blockIndex = blocks.Length - 1;
		}
		blockUILeft[index].material = blocks[blockIndex].block.GetComponent<Renderer>().sharedMaterial;
	}
	blockIndex = selectedBlock;
	for (int index = 0; index < blockUIRight.Length; index++) {
		blockIndex++;
		if (blockIndex >= blocks.Length) {
			blockIndex = 0;
		}
		blockUIRight[index].material = blocks[blockIndex].block.GetComponent<Renderer>().sharedMaterial;
	}
}

[System.Serializable]
public struct InventoryBlock {
	public Block block;
	public int amount;
}
		

The block prefabs have a cube renderer, a box collider, an animator, an audio source, and the block script. As well as the block ID, the block script also handles playing an animation and a sound when placing and breaking the block, and destroying the game object after the breaking animation is done.


public void placeBlock() {
	animator.SetTrigger("place");
	audioSource.clip = placeSound;
	audioSource.Play();
}

public void breakBlock() {
	animator.SetTrigger("break");
	audioSource.clip = breakSound;
	audioSource.Play();
	GetComponent<Collider>().enabled = false;
	StartCoroutine(breakWait());
}

private IEnumerator breakWait() {
	float waitUntil = Time.time + 0.3f;
	while (Time.time < waitUntil) {
		yield return null;
	}
	Destroy(gameObject);
}
		

Reflections



All in all I'm happy with what I created, but there's definitely things I would have done differently if this was a bigger project. One thing is all the hardcoded values, which certainly could become a problem if I worked on this for longer and wanted to change how things work or add other types of blocks.

Another thing I think is a bit weird is that every block has an audio source. It would perhaps be more optimal to have just one audio source for all blocks and move it to a block's position when playing a sound, though that could also be weird if sounds are played too quickly after each other and get cut off.

It's also a bit weird how showBlockHover returns a position for a potential new block, but it does makes sense to do those calculations together and I didn't feel like giving the function a really long name to describe what it does better.

If this was a bigger project with more blocks, I would change addBlock to add another block type to the inventory instead of logging an error if you try to pick up a block type you don't already have, since it would be pretty annoying to add all blocks to the inventory in the editor, and maybe it wouldn't make sense for the player to have all types of blocks from the start.

Either way, I managed to make what I set out to make, and I think the animations, sound effects, and UI design turned out pretty nice for a programmer, so I'm happy.