Part of these game systems:
Stack Mechanic
Tap-to-place stacking game mechanic where a moving block must be stopped to align with the previous block. Features overhang trimming, perfect placement detection, and stack height tracking.
How to Use
1
Create an empty GameObject and attach the StackMechanic script
2
Create a simple cube prefab and assign it to the blockPrefab field
3
Adjust blockHeight, moveSpeed, and moveRange in the inspector
4
Set the perfectThreshold for how close counts as a perfect placement
5
Wire up onBlockPlaced, onPerfectPlacement, and onGameOver events for UI and effects
Source Code
C#
using UnityEngine;
using UnityEngine.Events;
public class StackMechanic : MonoBehaviour
{
[Header("Block Settings")]
[SerializeField] private GameObject blockPrefab;
[SerializeField] private float blockHeight = 0.2f;
[SerializeField] private float moveSpeed = 3f;
[SerializeField] private float moveRange = 3f;
[Header("Placement")]
[SerializeField] private float perfectThreshold = 0.05f;
[SerializeField] private Color perfectColor = Color.yellow;
[Header("Events")]
public UnityEvent onBlockPlaced;
public UnityEvent onPerfectPlacement;
public UnityEvent onGameOver;
public UnityEvent<int> onStackHeightChanged;
public int StackHeight { get; private set; }
public int PerfectStreak { get; private set; }
public bool IsGameOver { get; private set; }
private GameObject currentBlock;
private GameObject previousBlock;
private float previousBlockSizeX;
private float previousBlockPosX;
private bool movingRight = true;
private bool isPlaying;
private void Start()
{
StartGame();
}
public void StartGame()
{
// Clear existing blocks
foreach (Transform child in transform)
Destroy(child.gameObject);
StackHeight = 0;
PerfectStreak = 0;
IsGameOver = false;
isPlaying = true;
// Create base block
previousBlock = CreateBlock(Vector3.zero, 2f);
previousBlockSizeX = 2f;
previousBlockPosX = 0f;
SpawnNextBlock();
}
private void Update()
{
if (!isPlaying || IsGameOver || currentBlock == null) return;
// Touch or click to place
if (Input.GetMouseButtonDown(0) || (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began))
{
PlaceBlock();
return;
}
// Move current block
Vector3 pos = currentBlock.transform.localPosition;
if (movingRight)
pos.x += moveSpeed * Time.deltaTime;
else
pos.x -= moveSpeed * Time.deltaTime;
if (pos.x >= moveRange) movingRight = false;
if (pos.x <= -moveRange) movingRight = true;
currentBlock.transform.localPosition = pos;
}
private void SpawnNextBlock()
{
float yPos = (StackHeight + 1) * blockHeight;
Vector3 spawnPos = new Vector3(-moveRange, yPos, 0f);
currentBlock = CreateBlock(spawnPos, previousBlockSizeX);
movingRight = true;
}
private GameObject CreateBlock(Vector3 position, float sizeX)
{
GameObject block;
if (blockPrefab != null)
{
block = Instantiate(blockPrefab, transform);
}
else
{
block = GameObject.CreatePrimitive(PrimitiveType.Cube);
block.transform.SetParent(transform);
}
block.transform.localPosition = position;
block.transform.localScale = new Vector3(sizeX, blockHeight, 1f);
return block;
}
private void PlaceBlock()
{
float currentPosX = currentBlock.transform.localPosition.x;
float currentSizeX = currentBlock.transform.localScale.x;
float overhang = currentPosX - previousBlockPosX;
float absOverhang = Mathf.Abs(overhang);
// Check if completely missed
if (absOverhang >= currentSizeX)
{
GameOver();
return;
}
// Perfect placement check
if (absOverhang <= perfectThreshold)
{
PerfectStreak++;
currentBlock.transform.localPosition = new Vector3(
previousBlockPosX,
currentBlock.transform.localPosition.y,
0f);
Renderer rend = currentBlock.GetComponent<Renderer>();
if (rend != null)
rend.material.color = perfectColor;
onPerfectPlacement?.Invoke();
}
else
{
PerfectStreak = 0;
// Trim the block
float newSizeX = currentSizeX - absOverhang;
float newPosX = previousBlockPosX + overhang * 0.5f;
currentBlock.transform.localScale = new Vector3(
newSizeX, blockHeight, 1f);
currentBlock.transform.localPosition = new Vector3(
newPosX,
currentBlock.transform.localPosition.y,
0f);
// Spawn falling piece
SpawnFallingPiece(currentPosX, currentBlock.transform.localPosition.y,
absOverhang, overhang > 0);
previousBlockSizeX = newSizeX;
previousBlockPosX = newPosX;
}
if (absOverhang <= perfectThreshold)
{
previousBlockSizeX = currentBlock.transform.localScale.x;
previousBlockPosX = currentBlock.transform.localPosition.x;
}
StackHeight++;
previousBlock = currentBlock;
currentBlock = null;
onBlockPlaced?.Invoke();
onStackHeightChanged?.Invoke(StackHeight);
SpawnNextBlock();
}
private void SpawnFallingPiece(float blockPosX, float yPos, float pieceSize, bool rightSide)
{
GameObject piece = GameObject.CreatePrimitive(PrimitiveType.Cube);
piece.transform.SetParent(transform);
float piecePosX = rightSide
? blockPosX + (previousBlockSizeX - pieceSize) * 0.5f + pieceSize * 0.5f
: blockPosX - (previousBlockSizeX - pieceSize) * 0.5f - pieceSize * 0.5f;
piece.transform.localPosition = new Vector3(piecePosX, yPos, 0f);
piece.transform.localScale = new Vector3(pieceSize, blockHeight, 1f);
Rigidbody rb = piece.AddComponent<Rigidbody>();
rb.mass = 0.5f;
Destroy(piece, 3f);
}
private void GameOver()
{
IsGameOver = true;
isPlaying = false;
Rigidbody rb = currentBlock.AddComponent<Rigidbody>();
rb.mass = 0.5f;
onGameOver?.Invoke();
}
}