Making a game with Claude code and Gemini

Author: JJustis | Published: 2026-01-16 20:03:08
Article Image 1

🎯 What You're Going to Build

By the end of this comprehensive guide, you will have created a complete, playable voxel game from scratch. Your game will include:

✅ Procedural Voxel World: Endless or finite terrain generated algorithmically.
✅ Full Player Controls: Movement, jumping, mining blocks, and placing blocks.
✅ Optimized Voxel Engine: A performant system for rendering and managing thousands of blocks.
✅ Simple Enemy AI: NPCs that can patrol, chase the player, and attack.
✅ Procedural Models: Systems to generate structures like trees, houses, or caves dynamically.

We'll be using Unity and C#, the industry-standard tools that power countless games.

Phase 1: Foundation & Setup

Step 1: Install Prerequisites

  1. Download and Install Unity Hub: Get it from the official Unity website.
  2. Install Unity Editor: From Unity Hub, install Unity 2021.3 LTS or a newer LTS version. During installation, include the Windows/Mono/IL2CPP Build Support modules.
  3. Install a Code Editor: Visual Studio (comes with Unity) or VS Code are perfect.

Step 2: Create Your Project

  1. Open Unity Hub and click New Project.
  2. Select the 3D (Core) template.
  3. Name your project (e.g., "MyVoxelGame") and choose a location.
  4. Click Create Project. Your development environment is ready.

Step 3: Project Structure

Create these essential folders in your Unity Project window (Right-click `Assets` > Create > Folder):
Assets/
├── Scripts/          # All C# code
├── Prefabs/          # Reusable objects
├── Materials/        # Block textures and colors
├── Scenes/           # Your game levels
└── Settings/         # Configuration files

Phase 2: Building the Voxel Engine Core

Step 4: Create the Block Data System

In the `Scripts/` folder, create a C# script called `BlockType.cs`. This is a simple data container.
// Scripts/BlockType.cs
using UnityEngine;

[CreateAssetMenu(fileName = "New Block", menuName = "Voxel/Block")]
public class BlockType : ScriptableObject
{
    public string blockName = "Grass";
    public bool isSolid = true;

    // Tile coordinates on a texture atlas (advanced) or just a color
    public Color topColor = Color.green;
    public Color sideColor = Color.gray;
    public Color bottomColor = Color.black;
}
Right-click in the Project window (`Assets/`), go to Create > Voxel > Block to make BlockType assets for Grass, Dirt, Stone, etc.

Step 5: Create the Chunk System (The Heart)

Create `Chunk.cs`. A "Chunk" is a 16x16x16 group of blocks that the engine manages efficiently.
// Scripts/World/Chunk.cs
using UnityEngine;

public class Chunk : MonoBehaviour
{
    public BlockType[,,] blocks = new BlockType[16, 16, 16]; // 3D array
    private MeshFilter meshFilter;
    private MeshCollider meshCollider;
    public bool needsUpdate = true; // Flag for regeneration

    void Start() {
        meshFilter = GetComponent();
        meshCollider = GetComponent();
        GenerateBlocks(); // Fill with initial data
        UpdateMesh();     // Create the visual mesh
    }

    void GenerateBlocks() {
        // Simple flat world: top layer is grass, next few are dirt, rest stone
        for (int x = 0; x < 16; x++) {
            for (int z = 0; z < 16; z++) {
                int stoneHeight = 4;
                int dirtHeight = 5;
                for (int y = 0; y < 16; y++) {
                    if (y == dirtHeight) {
                        blocks[x, y, z] = // Reference your Grass BlockType asset
                    } else if (y < dirtHeight && y > stoneHeight) {
                        blocks[x, y, z] = // Reference your Dirt BlockType asset
                    } else if (y <= stoneHeight) {
                        blocks[x, y, z] = // Reference your Stone BlockType asset
                    }
                    // Otherwise, block is null (air)
                }
            }
        }
    }

    public void UpdateMesh() {
        // This is a simplified placeholder. A real implementation would:
        // 1. Loop through all blocks.
        // 2. For each solid block, check its 6 neighbors.
        // 3. Only add a mesh face (quad) if the neighbor is air (not solid).
        // 4. Combine all faces into one mesh and assign it to meshFilter.mesh
        // 5. Also set meshCollider.sharedMesh for physics.

        Debug.Log("Chunk mesh needs to be generated here.");
        // For now, we'll create a simple cube to see something.
        Mesh mesh = new Mesh();
        // ... (Complex mesh generation logic goes here) ...
        // meshFilter.mesh = mesh;
        needsUpdate = false;
    }
}

Step 6: Create the World Manager

Create `World.cs`. This script spawns and manages all the Chunks.
// Scripts/World/World.cs
using UnityEngine;

public class World : MonoBehaviour
{
    public GameObject chunkPrefab; // We'll make this next
    public int worldSize = 4; // 4x4 chunks (64x64 blocks)

    void Start() {
        GenerateWorld();
    }

    void GenerateWorld() {
        for (int x = 0; x < worldSize; x++) {
            for (int z = 0; z < worldSize; z++) {
                // Instantiate a chunk at the correct position
                Vector3 chunkPos = new Vector3(x * 16, 0, z * 16);
                GameObject newChunk = Instantiate(chunkPrefab, chunkPos, Quaternion.identity, transform);
                newChunk.name = "Chunk_" + x + "_" + z;
            }
        }
    }
}

Step 7: Set Up the Chunk Prefab

  1. In Unity, create an empty GameObject in your scene.
  2. Add a Mesh Filter and a Mesh Renderer component to it.
  3. Add the `Chunk.cs` script to it.
  4. Drag this GameObject from the Hierarchy into your `Assets/Prefabs/` folder to create a Prefab.
  5. Delete the original from the Hierarchy.
  6. Select your main World GameObject (or create one), add the `World.cs` script, and drag your new Chunk Prefab into its `chunkPrefab` slot.
Press Play. You won't see anything yet because the mesh generation isn't complete, but the system is in place.

Phase 3: Player Controller & Interaction

Step 8: Create the First-Person Player

Create `PlayerController.cs`.
// Scripts/Player/PlayerController.cs
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
    // Movement
    public float walkSpeed = 5f;
    public float runSpeed = 10f;
    public float jumpForce = 8f;
    public float gravity = -20f;
    private Vector3 velocity;
    private CharacterController controller;
    private float verticalRotation = 0f;
    public float lookSpeed = 2f;
    public float lookLimit = 80f;

    // Block Interaction
    public float reachDistance = 5f;
    public LayerMask blockLayer;
    private BlockType selectedBlockType; // Assign in Inspector

    void Start() {
        controller = GetComponent();
        Cursor.lockState = CursorLockMode.Locked; // Lock mouse to screen
    }

    void Update() {
        HandleMovement();
        HandleMouseLook();
        HandleBlockInteraction();
    }

    void HandleMovement() {
        float speed = Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed;
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

        Vector3 move = (transform.right * x + transform.forward * z).normalized;
        controller.Move(move * speed * Time.deltaTime);

        // Jump
        if (controller.isGrounded) {
            if (velocity.y < 0) velocity.y = -2f; // Small downward force when grounded
            if (Input.GetButtonDown("Jump")) {
                velocity.y = Mathf.Sqrt(jumpForce * -2f * gravity);
            }
        }
        velocity.y += gravity * Time.deltaTime; // Apply gravity
        controller.Move(velocity * Time.deltaTime);
    }

    void HandleMouseLook() {
        float mouseX = Input.GetAxis("Mouse X") * lookSpeed;
        float mouseY = Input.GetAxis("Mouse Y") * lookSpeed;

        verticalRotation -= mouseY;
        verticalRotation = Mathf.Clamp(verticalRotation, -lookLimit, lookLimit);

        Camera.main.transform.localRotation = Quaternion.Euler(verticalRotation, 0, 0);
        transform.Rotate(Vector3.up * mouseX);
    }

    void HandleBlockInteraction() {
        Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, reachDistance, blockLayer)) {
            // Debug: Draw the ray
            Debug.DrawRay(ray.origin, ray.direction * reachDistance, Color.red);

            // PLACE BLOCK (Right Mouse Button)
            if (Input.GetMouseButtonDown(1)) {
                Vector3 placePos = hit.point + hit.normal * 0.5f; // Place adjacent to hit face
                Vector3Int blockCoord = new Vector3Int(
                    Mathf.FloorToInt(placePos.x),
                    Mathf.FloorToInt(placePos.y),
                    Mathf.FloorToInt(placePos.z)
                );
                // TODO: Call a World function to set the block at blockCoord to selectedBlockType
                Debug.Log("Placing block at: " + blockCoord);
            }

            // MINE/BREAK BLOCK (Left Mouse Button)
            if (Input.GetMouseButtonDown(0)) {
                Vector3Int breakCoord = new Vector3Int(
                    Mathf.FloorToInt(hit.point.x - hit.normal.x * 0.01f), // Slight offset to get the block we hit
                    Mathf.FloorToInt(hit.point.y - hit.normal.y * 0.01f),
                    Mathf.FloorToInt(hit.point.z - hit.normal.z * 0.01f)
                );
                // TODO: Call a World function to set the block at breakCoord to null (air)
                Debug.Log("Breaking block at: " + breakCoord);
            }
        }
    }
}

Step 9: Set Up the Player in Unity

  1. Create a new GameObject called "Player".
  2. Add a Character Controller component.
  3. Add the `PlayerController.cs` script.
  4. Create a child GameObject called "Camera" and position it at eye level (0, 1.7, 0). Add a Camera component to it.
  5. Assign a BlockType asset (like Dirt) to the script's `selectedBlockType` field.
  6. In the Layer dropdown (top-right), create a new layer called "Block". Assign this layer to your Chunk Prefab.
  7. Back on the PlayerController script, set the `blockLayer` to the new "Block" layer.

Phase 4: Procedural Models & World Generation

Step 10: Simple Tree Generator

Let's create a system that can place a simple voxel tree. Create `ProceduralTree.cs`.
// Scripts/World/ProceduralTree.cs
using UnityEngine;

public static class ProceduralTree
{
    public static void GenerateAt(World world, Vector3Int rootPosition, BlockType trunkBlock, BlockType leavesBlock) {
        int trunkHeight = Random.Range(4, 7);

        // Generate trunk
        for (int y = 0; y < trunkHeight; y++) {
            // Call world.SetBlock(rootPosition.x, rootPosition.y + y, rootPosition.z, trunkBlock);
        }

        // Generate leaves (simple sphere)
        int leafRadius = 2;
        for (int x = -leafRadius; x <= leafRadius; x++) {
            for (int y = -leafRadius; y <= leafRadius; y++) {
                for (int z = -leafRadius; z <= leafRadius; z++) {
                    // Simple distance check for a sphere
                    if (Mathf.Sqrt(x*x + y*y + z*z) <= leafRadius) {
                        Vector3Int leafPos = rootPosition + new Vector3Int(x, y + trunkHeight, z);
                        // Call world.SetBlock(leafPos.x, leafPos.y, leafPos.z, leavesBlock);
                    }
                }
            }
        }
    }
}

Step 11: Implement the World.SetBlock() Method

This is a crucial function. Add it to your `World.cs` script.
// Inside World.cs
public void SetBlock(int worldX, int worldY, int worldZ, BlockType block) {
    // 1. Calculate which chunk this block is in
    int chunkX = Mathf.FloorToInt(worldX / 16f);
    int chunkZ = Mathf.FloorToInt(worldZ / 16f);

    // 2. Calculate the block's local position within that chunk
    int localX = worldX - (chunkX * 16);
    int localY = worldY;
    int localZ = worldZ - (chunkZ * 16);

    // 3. Find the chunk GameObject
    string chunkName = "Chunk_" + chunkX + "_" + chunkZ;
    GameObject chunkObj = GameObject.Find(chunkName); // Not optimal, but simple for now

    if (chunkObj != null) {
        Chunk chunkScript = chunkObj.GetComponent();
        if (localY >= 0 && localY < 16) {
            chunkScript.blocks[localX, localY, localZ] = block;
            chunkScript.needsUpdate = true; // Mark for mesh update
            // For immediate update: chunkScript.UpdateMesh();
        }
    }
}
Now you can call `world.SetBlock()` from your PlayerController and your Tree generator!

Phase 5: Simple Enemy AI

Step 12: Create the AI Controller

Create `SimpleEnemyAI.cs`. This will give us a enemy that patrols, chases, and attacks.
// Scripts/AI/SimpleEnemyAI.cs
using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]
public class SimpleEnemyAI : MonoBehaviour
{
    public enum AIState { Patrol, Chase, Attack };
    public AIState currentState = AIState.Patrol;

    private NavMeshAgent agent;
    private Transform playerTarget;

    // Patrol
    public Vector3 patrolCenter;
    public float patrolRadius = 10f;
    private float patrolTimer = 0f;
    public float patrolWaitTime = 2f;

    // Detection
    public float sightRange = 15f;
    public float attackRange = 2f;

    // Attack
    public float attackDamage = 10f;
    public float attackCooldown = 1f;
    private float lastAttackTime = 0f;

    void Start() {
        agent = GetComponent();
        playerTarget = GameObject.FindGameObjectWithTag("Player").transform;
        patrolCenter = transform.position; // Patrol around spawn point
        SetRandomPatrolPoint();
    }

    void Update() {
        float distanceToPlayer = Vector3.Distance(transform.position, playerTarget.position);

        // STATE TRANSITIONS
        if (distanceToPlayer <= attackRange) {
            currentState = AIState.Attack;
        } else if (distanceToPlayer <= sightRange) {
            currentState = AIState.Chase;
        } else {
            currentState = AIState.Patrol;
        }

        // STATE ACTIONS
        switch (currentState) {
            case AIState.Patrol:
                Patrol();
                break;
            case AIState.Chase:
                Chase();
                break;
            case AIState.Attack:
                Attack();
                break;
        }
    }

    void Patrol() {
        if (!agent.pathPending && agent.remainingDistance < 0.5f) {
            patrolTimer += Time.deltaTime;
            if (patrolTimer >= patrolWaitTime) {
                SetRandomPatrolPoint();
                patrolTimer = 0;
            }
        }
    }

    void SetRandomPatrolPoint() {
        Vector3 randomDirection = Random.insideUnitSphere * patrolRadius;
        randomDirection += patrolCenter;
        NavMeshHit hit;
        NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1);
        agent.SetDestination(hit.position);
    }

    void Chase() {
        agent.SetDestination(playerTarget.position);
    }

    void Attack() {
        agent.SetDestination(transform.position); // Stop moving
        transform.LookAt(new Vector3(playerTarget.position.x, transform.position.y, playerTarget.position.z));

        // Cooldown-based attack
        if (Time.time - lastAttackTime >= attackCooldown) {
            Debug.Log("Enemy attacks player for " + attackDamage + " damage!");
            // Here you would call playerTarget.GetComponent().TakeDamage(attackDamage);
            lastAttackTime = Time.time;
        }
    }

    // Visualize ranges in editor
    void OnDrawGizmosSelected() {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, sightRange);
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, attackRange);
        Gizmos.color = Color.blue;
        Gizmos.DrawWireSphere(patrolCenter, patrolRadius);
    }
}

Step 13: Set Up the Enemy in Unity

  1. Create a simple 3D GameObject for your enemy (e.g., a Capsule).
  2. Tag your Player GameObject as "Player".
  3. Add a NavMeshAgent component to the enemy.
  4. Add the `SimpleEnemyAI.cs` script.
  5. Open the Window > AI > Navigation window.
  6. Select your ground/terrain, mark it as Walkable, and click Bake.
  7. Press Play. Your enemy will now patrol, chase you when close, and "attack" when in range!

Phase 6: Bringing It All Together & Next Steps

Step 14: The Missing Piece - Real Mesh Generation

The core of a voxel engine is the `Chunk.UpdateMesh()` method. While implementing it fully is beyond a single code snippet, here is the essential structure to get you started:
void UpdateMesh() {
    List vertices = new List();
    List triangles = new List();
    List uvs = new List();
    int faceCount = 0;

    for (int x = 0; x < 16; x++) {
        for (int y = 0; y < 16; y++) {
            for (int z = 0; z < 16; z++) {
                if (blocks[x, y, z] != null && blocks[x, y, z].isSolid) {
                    // Check each of the 6 neighboring positions
                    // If neighbor is AIR (null or not solid), add a face
                    // Add 4 vertices and 6 triangles (2) for that face
                    // Assign UVs based on the block type
                    faceCount++;
                }
            }
        }
    }

    Mesh mesh = new Mesh();
    mesh.vertices = vertices.ToArray();
    mesh.triangles = triangles.ToArray();
    mesh.uv = uvs.ToArray();
    mesh.RecalculateNormals(); // Important for lighting
    meshFilter.mesh = mesh;
    meshCollider.sharedMesh = mesh; // For collision
}
To truly complete this: Search for tutorials on "Unity voxel mesh generation" or "greedy meshing" for highly optimized code.

Step 15: Where to Go From Here - Project Roadmap

You now have all the core systems. Here’s how to evolve your game:
  1. Complete the Mesh Generator: This is your #1 priority to see the world.
  2. Add a Block Inventory/Hotbar: Let players select from multiple block types.
  3. Implement Saving/Loading: Save the modified world to a file.
  4. Improve World Generation: Add Perlin noise for hills, caves, and biomes.
  5. Add More AI Behaviors: Different enemy types, pathfinding around obstacles.
  6. Polish: Add sound effects, particle effects for mining, a day/night cycle.

Essential Resources & Community

  • Sebastian Lague: His "Coding Adventure" YouTube series includes excellent voxel and procedural generation tutorials.
  • Unity Learn: Official tutorials on C# scripting and physics.
  • r/VoxelGameDev: A helpful Reddit community for voxel developers.
  • Brackeys (YouTube): Although ended, their beginner tutorials are timeless gold.

You Are Now a Voxel Game Developer

You have just built the architectural skeleton of a complete voxel game. While there is code to fill in—especially in the mesh generation—you now understand how all the critical pieces connect: the data-driven blocks, the chunk-based world, the interactive player, the procedural systems, and the simple AI.

The journey from here is one of iteration, learning, and polish. Start by getting a single cube to appear on screen from your chunk data. Then make a row of cubes, then a full layer. Each small victory will build your understanding and your game.

Start small, build often, and have fun.
© 2026 Secupgrade.com – Crafting worlds, one block at a time.
Unity Manual | C# Docs | VoxelDev Community