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); }
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.