Blog
Posts
  • MarbleMaze: Deep Dive — Procedural Maze Generation with Kruskal's Algorithm

    This is a deep dive into the procedural level generator behind MarbleMaze: Galactic Stars. Every level the player encounters is generated at runtime — no hand-crafted layouts, no level files. Each one is unique, yet fully reproducible from a single integer seed.

    The four steps established for this generation are all described in this post.
    Read the full process below…

    The Goal

    The generator needs to produce a perfect maze — a grid of cells connected by passages where every cell is reachable and there are no loops. On top of that, it needs to:

    • Place hazard tiles (ice, spikes, doors, moving platforms) in a way that feels hand-crafted
    • Guarantee start and end positions
    • Scatter collectible stars at meaningful distances from each other
    • Do all of this deterministically from a single seed (the level index), so the same level always looks the same

    The whole pipeline runs in four sequential steps, orchestrated by a single static entry point:

    // Generator.cs
    public static Grid GenerateMaze(int levelIndex, GeneratorParameters_SO p, out int usedSeed)
    {
        usedSeed = levelIndex == -1
            ? (p.inputSeed == -1 ? Random.Range(int.MinValue, int.MaxValue) : p.inputSeed)
            : levelIndex;
    
        var rng = new System.Random(usedSeed);
    
        Grid grid = GridFactory.CreateWallGrid(p.gridWidth, p.gridHeight);
    
        // 1. Perfect maze
        var carved = MazeGenerator.GenerateKruskalMaze(p.gridWidth, p.gridHeight, rng);
    
        // 2. Start / End
        var start = GridUtils.GetStartPosition(grid, p);
        var end   = GridUtils.ResolveEndPosition(grid, p, rng);
        GridUtils.MarkStartAndEnd(grid, start, end);
    
        // 3. Tile rules
        MazeGenerator.ApplyMaze(grid, carved, p, rng);
    
        // 4. Stars
        StarPlacer.PlaceStars(grid, p, carved, start, end, rng);
    
        return grid;
    }

    Passing levelIndex directly as the seed is the simplest possible reproducibility guarantee — regenerating level 42 will always produce the same maze.


    Step 0 — The Grid & CellData

    Before anything is generated, GridFactory.CreateWallGrid initialises the grid with every cell marked as empty (a wall). Generation then works by carving passages through it.

    // GridFactory.cs
    public static Grid CreateWallGrid(int width, int height)
    {
        Grid grid = new Grid(width, height);
        for (int x = 0; x < width; x++)
            for (int y = 0; y < height; y++)
                grid.SetCell(x, y, new CellData { isEmpty = true, ground = GroundType.Floor });
        return grid;
    }

    Each cell in the grid is a CellData struct — a value type stored directly in a 2D array. No heap allocations during traversal, no GC pressure on mobile:

    // CoreData.cs
    [System.Serializable]
    public struct CellData
    {
        public bool isEmpty;                   // true = wall, false = walkable
        public GroundType ground;              // Floor, Ice, Piques, MovingPlatform …
        public OverlayType overlay;            // None, Start, End, Star
        public bool isEnd;
        public bool isHorizontal;             // orientation flag for line hazards
        public bool requiresTwoSolidNeighbours;
    }

    The Grid class wraps CellData[,] and exposes two accessor styles. GetCell returns a copy (safe for reading), while GetCellRef returns a ref to the struct for in-place mutation without boxing or a round-trip copy:

    public ref CellData GetCellRef(int x, int y) => ref cells[x, y];

    This pattern appears everywhere in the generator — every tile rule modifies cells through ref accessors to keep the pipeline allocation-free.


    Step 1 — Kruskal’s Algorithm for Perfect Mazes

    Kruskal’s algorithm builds a minimum spanning tree of a graph. Applied to a grid, it builds a spanning tree of all cells — which is exactly what a perfect maze is: every cell connected, no loops, exactly one path between any two points.

    The Even-Cell Trick

    A key implementation detail: cells only live at even coordinates. A 10×10 grid has cells at (0,0), (0,2), (2,0), (2,2) … and so on. The odd coordinates are the walls between cells.

    When two cells are connected by removing the wall between them, three positions become walkable:

    • cell A (even, even)
    • cell B (even, even)
    • the wall between them — the midpoint (A + B) / 2

    This is the standard “cell + passage” encoding for grid mazes.

    // MazeGenerator.cs
    public static HashSet<Vector2Int> GenerateKruskalMaze(int width, int height, System.Random rng)
    {
        // 1. Collect cells at even coordinates only
        var cells = new List<Vector2Int>();
        for (int x = 0; x < width; x += 2)
            for (int y = 0; y < height; y += 2)
                cells.Add(new Vector2Int(x, y));
    
        // 2. Build the edge list (connections between adjacent cells, 2 apart)
        var edges = new List<Edge>();
        foreach (var c in cells)
        {
            TryAddEdge(c, Vector2Int.right * 2);
            TryAddEdge(c, Vector2Int.up * 2);
        }
    
        // 3. Shuffle edges for randomness
        GridUtils.Shuffle(edges, rng);
    
        // 4. Kruskal: union cells, carve passages for accepted edges
        var uf = new UnionFind<Vector2Int>(cells);
        var carved = new HashSet<Vector2Int>();
    
        foreach (var e in edges)
        {
            if (!uf.Union(e.a, e.b)) continue; // already connected — skip
    
            carved.Add(e.a);                    // cell A
            carved.Add(e.b);                    // cell B
            carved.Add((e.a + e.b) / 2);       // wall between them
        }
    
        return carved;
    }

    Union returns false when the two cells already share a root — meaning connecting them would create a loop. By skipping those edges, Kruskal’s guarantees the result is loop-free.

    The even-cell maze grid and Kruskal's carving

    Grid & Kruskal’s — The even-cell encoding is shown as a 7×7 coordinate grid. Cell nodes sit at even coordinates (marked with dots), walls at odd ones. The teal path traces a sample spanning tree carved by the algorithm — notice how every connected passage lights up three tiles: cell A, cell B, and the midpoint wall between them.


    Step 2 — Union-Find Data Structure

    The correctness of Kruskal’s depends entirely on efficiently answering one question: “Are these two cells already connected?”

    This is exactly the Union-Find (or Disjoint Set Union) data structure. I implemented it as a generic class so it can work with any comparable type — Vector2Int in this case:

    // UnionFind.cs
    public class UnionFind<T>
    {
        private readonly Dictionary<T, T> parent;
        private readonly Dictionary<T, int> rank;
    
        public UnionFind(IEnumerable<T> nodes)
        {
            parent = new Dictionary<T, T>();
            rank   = new Dictionary<T, int>();
    
            foreach (var n in nodes)
            {
                parent[n] = n; // each node is its own root initially
                rank[n]   = 0;
            }
        }
    
        public T Find(T x)
        {
            if (!parent[x].Equals(x))
                parent[x] = Find(parent[x]); // path compression
            return parent[x];
        }
    
        public bool Union(T a, T b)
        {
            T rootA = Find(a);
            T rootB = Find(b);
    
            if (rootA.Equals(rootB)) return false; // same tree — would create a cycle
    
            // Union by rank: attach the shorter tree under the taller one
            if      (rank[rootA] < rank[rootB]) parent[rootA] = rootB;
            else if (rank[rootA] > rank[rootB]) parent[rootB] = rootA;
            else { parent[rootB] = rootA; rank[rootA]++; }
    
            return true;
        }
    }

    Two classical optimisations are in play:

    Path compression (in Find): after traversing up to the root, every node along the path is re-pointed directly at the root. Future lookups on the same node take O(1).

    Union by rank (in Union): the tree with the smaller depth is attached under the taller one, keeping trees balanced and preventing worst-case O(n) chains.

    Together these give O(α(n)) amortised per operation, where α is the inverse Ackermann function — effectively constant for any input size you’d encounter in practice.

    Union-Find: path compression and union by rank

    Union-Find — The left side shows path compression in action: a 4-deep chain (A→B→C→D) collapses to a flat star shape after a single Find(E), so every subsequent lookup is O(1). The right side shows union by rank: Tree B (rank 1) is attached under Tree A (rank 2) rather than the other way around, keeping the merged tree balanced.


    Step 3 — Tile Decoration (3-Pass System)

    The carved HashSet<Vector2Int> from Kruskal’s only tells us which cells are walkable — it says nothing about what kind of floor tile each cell should be.

    MazeGenerator.ApplyMaze runs three passes in sequence, each adding a layer of tile variety with progressively stricter context requirements.

    Pass 1 — Base Tiles

    Every carved cell gets a ground type via weighted random selection from the active hazard list. The weights are normalised at runtime so the ratios sum to at most 1.0 — any remainder becomes plain Floor:

    private static void ApplyBaseTiles(Grid grid, HashSet<Vector2Int> carved,
        TileDatabase_SO db, System.Random rng)
    {
        var hazards = db.SimpleHazards.ToList();
    
        foreach (var pos in carved)
        {
            ref var cell = ref grid.GetCellRef(pos);
            if (cell.overlay != OverlayType.None) continue; // don't overwrite Start/End
    
            cell.isEmpty = false;
    
            float total   = hazards.Sum(h => h.ratio);
            float scale   = Mathf.Min(1f, total) / total; // normalise to ≤ 1.0
            double roll   = rng.NextDouble();
            float cumulative = 0f;
    
            foreach (var h in hazards)
            {
                cumulative += h.ratio * scale;
                if (roll < cumulative) { cell.ground = h.groundType; break; }
            }
        }
    }

    Pass 2 — Two-Solid-Neighbour Tiles

    Some hazards (e.g. wall spikes) only make sense when the cell has at least two solid (non-empty) neighbours — otherwise they’d appear floating in mid-air. This pass collects all eligible candidates, shuffles them, then fills up to a ratio target:

    private static bool IsValidTwoSolidPlacement(Grid grid, Vector2Int pos)
    {
        int solidNeighbours = 0;
        foreach (var d in Directions4)
        {
            var n = pos + d;
            if (!grid.IsInside(n)) continue;
            var neighbor = grid.GetCell(n.x, n.y);
    
            // Reject if a neighbour is already a two-solid tile — prevents clustering
            if (neighbor.requiresTwoSolidNeighbours) return false;
    
            if (!neighbor.isEmpty) solidNeighbours++;
        }
        return solidNeighbours >= 2;
    }

    The anti-clustering check (requiresTwoSolidNeighbours) prevents two of these tiles from sitting next to each other, which would look and play poorly.

    Pass 3 — Line Tiles (3-cell Hazards)

    The last pass places 3-cell hazards (e.g. a moving platform with two edge pieces). These require three consecutive walkable cells in a row or column:

    // Collect valid center positions for horizontal and vertical lines
    if (x-1 >= 0 && x+1 < grid.Width &&
        IsWalkable(grid, x-1, y) && IsWalkable(grid, x, y) && IsWalkable(grid, x+1, y))
        candidates.Add((new Vector2Int(x, y), horizontal: true));

    After placement, all three cells are recorded in a bool[,] reserved array to prevent overlapping lines from the same or subsequent passes:

    private static void ApplyLine(Grid grid, bool[,] reserved,
        (Vector2Int center, bool horizontal) c, HazardTileDefinition_SO hazard)
    {
        // Center cell gets the primary ground type
        ref var centerCell = ref grid.GetCellRef(c.center);
        centerCell.ground = hazard.groundType;
        centerCell.isHorizontal = c.horizontal;
    
        // Side cells get a different "edge" type (e.g. platform endcap)
        if (c.horizontal)
        {
            SetSide(grid, c.center.x - 1, c.center.y, hazard);
            SetSide(grid, c.center.x + 1, c.center.y, hazard);
            Reserve(reserved, c.center.x - 1, c.center.y, c.center.x, c.center.y, c.center.x + 1, c.center.y);
        }
        // ... vertical case mirrors this
    }

    This three-pass design — base → context-aware → structural — mirrors how a level designer would layer detail: first paint the floor, then add wall-hugging elements, then place the large set-pieces.

    The three-pass tile decoration system

    3-pass decoration — Each panel shows the same maze at a different stage. Pass 1 lays down random floor types. Pass 2 uses the neighbour context (the red arrows) to place a spike tile that has ≥2 solid neighbours. Pass 3 places a 3-cell moving platform across three consecutive walkable cells, with a reserved array preventing overlaps.


    Step 4 — Star Placement

    Stars use a two-pass placement strategy to balance the ideal spacing constraint against the reality that small or heavily decorated mazes might not have enough spread-out candidates.

    public static void PlaceStars(Grid grid, GeneratorParameters_SO p,
        HashSet<Vector2Int> walkable, Vector2Int start, Vector2Int end, System.Random rng)
    {
        var placed     = new List<Vector2Int>();
        var candidates = walkable.Where(c => c != start && c != end).ToList();
        GridUtils.Shuffle(candidates, rng);
    
        // Pass 1: strict — enforce minStarDistance between all placed stars
        foreach (var pos in candidates)
        {
            if (placed.Count >= p.starCount) break;
            if (grid.GetCell(pos.x, pos.y).overlay != OverlayType.None) continue;
            if (grid.GetCell(pos.x, pos.y).isEmpty) continue;
            if (grid.GetCell(pos.x, pos.y).requiresTwoSolidNeighbours) continue;
            if (placed.Any(s => Vector2Int.Distance(s, pos) < p.minStarDistance)) continue;
    
            grid.GetCellRef(pos.x, pos.y).overlay = OverlayType.Star;
            placed.Add(pos);
        }
    
        // Pass 2: relaxed — drop the distance constraint to fill the quota
        if (placed.Count < p.starCount)
        {
            foreach (var pos in candidates)
            {
                if (placed.Count >= p.starCount) break;
                if (grid.GetCellRef(pos.x, pos.y).overlay != OverlayType.None) continue;
                if (grid.GetCell(pos.x, pos.y).isEmpty) continue;
                if (grid.GetCell(pos.x, pos.y).requiresTwoSolidNeighbours) continue;
                if (placed.Contains(pos)) continue;
    
                grid.GetCellRef(pos.x, pos.y).overlay = OverlayType.Star;
                placed.Add(pos);
            }
        }
    }

    Stars also refuse to land on two-solid-neighbour tiles — those are already decorated with context-dependent hazards and would be confusing to navigate.

    The two-pass star placement strategy

    Star placement — Pass 1 shows the minimum-distance exclusion circles around each placed star, with a rejected candidate (red ✕) that falls inside one. Pass 2 shows a small maze where no spread-out candidates remain, so the quota is filled anyway by placing stars close together — guaranteeing the level never breaks.


    Configuration: The GeneratorParameters_SO

    All generation parameters live in a ScriptableObject, making them adjustable in the Unity inspector without touching code:

    public class GeneratorParameters_SO : ScriptableObject
    {
        public int gridWidth  = 10;
        public int gridHeight = 10;
        public bool randomEnd = true;
        public int  endMaxHeightPercent = 20; // end stays in the bottom 20% of the grid
        public int  inputSeed = -1;           // -1 = random; set for fixed test levels
        public TileDatabase_SO tileDatabase_SO;
        [Range(0, 20)] public int starCount = 3;
        [Range(1, 10)] public int minStarDistance = 2;
    }

    The RuntimeLevelProgression system modifies these parameters before each level — growing the grid size, adjusting hazard ratios, and changing star count as the player advances. That system is covered in its own deep-dive.


    Summary

    The summary of the system is the following:

    ComponentRole
    GridFactoryInitialises an all-wall grid
    UnionFind<T>Cycle detection for Kruskal’s; path compression + union by rank
    MazeGenerator.GenerateKruskalMazeBuilds the carved passage set
    MazeGenerator.ApplyMaze3-pass tile decoration
    GridUtilsStart/end placement, shuffle, safety queries
    StarPlacer2-pass star placement with distance fallback
    GeneratorPipeline entry point, seed management
    GeneratorParameters_SODesigner-configurable generation settings

    The key design choices that hold everything together:

    • Seeded System.Random rather than UnityEngine.Random — deterministic, no global state
    • Struct-based CellData in a 2D array — allocation-free pipeline, no GC spikes on mobile
    • ref accessors — in-place mutation without boxing
    • 3-pass decoration — each pass has access to the output of the previous, enabling context-aware rules
    • 2-pass star placement — strict constraint with a guaranteed fallback prevents levels from breaking on small grids
    ← Back to Project Overview Next: Adaptive Difficulty Engine →

    Created on February 2026
  • Occlusion Cutout Effect - AlchemisTeddy 🧪🐻

    After experimenting with a stencil-based see-through effect Unity Tips – Creating a Stencil See-Through Effect in Unity 6, I quickly noticed limitations. In scenarios such as tight tunnels or parallel wall intersections, the stencil solution introduced artifacts and did not feel robust.

    For a second attempt, I drew inspiration from Baldur’s Gate III’s beautiful occlusion cutout system.

    Luckily, Mojang’s Senior Technical Artist Brendan “Sully” Sullivan had already broken down the technique in Unreal Engine 80.lv article, which served as a strong reference.

    The challenge was now clear: How do we reproduce this effect inside Unity?

    The full process is described below…
    Created on September 2025
  • Unity Tips - Creating native DLLs in C++

    During my time at Lab4Tech, I prepared for several certifications including:

    To apply what I was training for, I initiated a side project that would bridge C++ and C# in Unity.
    This led me to develop a native DLL for Unity focused on procedural generation; a subject I had already explored in depth.

    Created on September 2025
  • Unity Character System - AlchemisTeddy

    When building a character-driven game, the core systems that govern player interaction are probably the most important part of the experience.
    They define the entire feel of the game.

    In this technical deep dive, we’ll deconstruct a well-structured, component-based character architecture built in Unity.
    We’ll analyze how it leverages a point-and-click NavMeshAgent for movement, a custom camera controller, an event-driven input system, and a robust, interface-based framework for interactions and persistence.

    Let’s begin by dissecting the core components that define the player’s immediate experience starting with the
    3Cs (Character, Camera, Controls).

    Created on September 2025
  • Lab - AlchemisTeddy 🧪🐻

    Welcome to AlchemisTeddy’s Lab, a whimsical experiment where alchemy meets teddy-bear charm.

    This project was created in just five days during the Swiss Game Academy, with the challenge of building a functional and visually engaging prototype under tight time pressure.

    Explore our environment in this Sketchfab viewer, spin, zoom, and get a closer look at the details.
    You’ll also find links to other creations that blend art, programming, and technical artistry.

    The Challenge

    To showcase the importance of collaboration, we structured our workflow around functionality first.
    This meant defining the essential features early on, ensuring that gameplay and interaction always had priority.

    The Process

    • Programming & Features: I outlined the required mechanics and implemented them step by step.
    • Art & Concepts: My teammate provided base sketches and models, which we refined together for integration.
    • Iteration & Polish: Each asset was tested, adjusted, and enhanced with hand-painted details to make interactive objects stand out and improve the overall player experience.

    Closing Thoughts

    This project reminded us how much collaboration fuels creativity.
    In just five days, AlchemisTeddy’s Lab grew from sketches on paper into a playful interactive world and we had a lot of fun bringing it to life.

    Created on September 2025
  • Unity Tips - Creating a Stencil See-Through Effect in Unity 6

    In many top-down or isometric games, walls can obstruct the player’s view of the action.
    A common solution is to make obstructing geometry transparent or temporarily cut away when it blocks the line of sight.

    In this post, I’ll cover how I implemented a see-through wall system in Unity 6, combining HLSL shaders, URP configuration, and a lightweight C# layer management script.

    I’ll also share performance considerations and ideas for extending this technique.

    Stencil Setup with HLSL

    The core of this effect relies on the stencil buffer.
    We define a shader that marks walls in the stencil pass, giving us control over which parts of the geometry should later be rendered differently.

    Shader "Unlit/Cutter"
    {
        Properties
        {
            [IntRange] _StencilID ("Stencil ID", Range(0,255)) = 0
        }
        SubShader
        {
            Tags { "RenderType"="Opaque" "Queue"="Geometry-1" "RenderPipeline"="UniversalPipeline"}
    
            Pass
            {
                Blend Zero One
                ZWrite Off
    
                Stencil
                {
                    Ref [_StencilID]
                    Comp Always
                    Pass Replace
                }
            }
        }
    }

    This shader doesn’t render visible pixels; it only writes stencil values.
    Later, the URP pipeline uses these values to selectively apply transparency.

    Configuring the URP Renderer

    Next, I extended the URP Asset Renderer.
    Two Renderer Features were added:

    • Cutter Pass → Applies the stencil writes.
    URP AssetRenderer setup for SeeThrough feature
    • SeeThrough Pass → Overrides wall rendering when the stencil is active.
    URP AssetRenderer setup for Cutter feature

    This allows walls to remain fully opaque by default, but become see-through as soon as the script switches their layer.

    Runtime Layer Switching

    To control which walls become transparent, I wrote a simple C# script. It checks the player’s position relative to wall colliders and assigns them to either the opaque layer or the see-through layer.

    void Update()
    {
        float playerZ = m_player.transform.position.z;
        float playerY = m_player.transform.position.y;
    
        foreach (Collider wallCollider in m_wallColliders)
        {
            // Y-axis rule: if the player is above the wall, always keep it opaque
            if (playerY > wallCollider.bounds.max.y - m_playerAboveWallThreshold)
            {
                wallCollider.gameObject.layer = m_opaqueLayer;
                continue;
            }
    
            // Z-axis rule: if the wall is in front of the player, make it see-through
            if (wallCollider.bounds.min.z < playerZ || wallCollider.bounds.max.z < playerZ)
            {
                wallCollider.gameObject.layer = m_seeThroughLayer;
            }
            else
            {
                wallCollider.gameObject.layer = m_opaqueLayer;
            }
        }
    }

    This ensures walls in front of the camera fade out, while walls behind or under remain visible.

    Player Setup

    The final piece of the system is the Cutter object.
    In this implementation, it’s a simple sphere attached to the player character and assigned to the Cutter layer.

    Cutter object setup on the player

    As the player moves, this sphere continuously updates the stencil buffer, ensuring that any obstructing walls are correctly masked out in real time.

    Retrospective

    Performance Considerations

    While this system works well in small to mid-scale levels, there are a few things to watch out for:

    • Physics iteration cost → Iterating through many wall colliders in Update() can become expensive. A spatial partitioning structure (e.g., Physics.OverlapSphere) could reduce checks.
    • Overdraw → Transparent walls increase GPU overdraw. Using cutout shaders or depth-based dithering could mitigate this if necessary.
    • Stencil conflicts → If your project already uses the stencil buffer for UI, outlines, or decals, allocate unique IDs to avoid collisions.
    • Batching → Switching layers may break static batching. Consider using material property overrides instead of layers if batching is critical.

    Possible Extensions

    There are multiple ways this effect could be extended depending on the game’s needs:

    • Smooth transitions → Instead of instantly swapping materials, interpolate alpha or use a dithering fade for a cleaner look.
    • Multiple players/units → Extend the script to handle visibility relative to multiple characters.
    • Line-of-sight system → Instead of relying solely on axis checks, perform raycasts from the camera to the player for more precise occlusion.
    • Artist control → Expose thresholds (distance, opacity curve, fade speed) in the inspector so designers can tweak per-level.

    Conclusion

    This system is relatively lightweight but dramatically improves readability in games where the camera doesn’t follow the player directly. By combining stencil operations, URP renderer features, and runtime logic, we can create a wall-cutting effect that feels seamless to both designers and players.

    For production, I’d recommend iterating on the fade mechanics and exploring GPU-based approaches for larger levels—but as a foundation, this approach is flexible, efficient, and easy to extend.

    Created on September 2025
  • Unity Tips - New Input System - Mouse Events

    Working with Unity’s new Input System can sometimes feel frustrating at first, even though the long-term benefits in terms of cross-platform support are significant.
    A useful tip I want to share is how to update Unity’s older approach for handling mouse events so that it remains compatible with the new Input System.

    Previously, this could be done with simple MonoBehaviour methods such as:

    private void OnMouseEnter()
    {
        // Example: Debug.Log($"Mouse is over {this.name}");
    }
    
    private void OnMouseExit()
    {
        // Example: Debug.Log($"Mouse has exited {this.name}");
    }

    With the new Input System, a few additional steps are required.

    First, the script must include using UnityEngine.EventSystems so that the pointer events can be detected.

    Second, the class needs to implement the interfaces IPointerEnterHandler and IPointerExitHandler.

    For example:

    using UnityEngine;
    using UnityEngine.EventSystems;
    
    /// <summary>
    /// Represents an item that exists in the game world and can be picked up.
    /// </summary>
    [RequireComponent(typeof(Collider))] // Ensures this object always has a collider
    public class WorldItem : MonoBehaviour, ICollectable, IPointerEnterHandler, IPointerExitHandler
    {
        // Class implementation
    }

    These interfaces require the following methods to be implemented:

    public void OnPointerEnter(PointerEventData eventData)
    {
        // Example: Debug.Log($"Mouse is over {this.name}");
    }
    
    public void OnPointerExit(PointerEventData eventData)
    {
        // Example: Debug.Log($"Mouse has exited {this.name}");
    }

    Finally, make sure the camera has a PhysicsRaycaster component attached and that you have an EventSystems in your hierarchy.

    Once these steps are completed, the script will recognize mouse interactions in the same way it did before the new Input System was introduced.

    The same update is required for all methods regarding Mouse events such as

    • IPointerClickHandler & OnPointerClick()
    • IPointerDownHandler & OnPointerDown()
    • IPointerUpHandler & OnPointerUp()

    The list of interfaces related to Mouse Events can be found here

    I hope this proves useful.

    Created on September 2025
  • Bottles Shader & Script - AlchemisTeddy 🧪🐻

    Developed as part of the asset range of AlchemisTeddy, a set of bottles combines hand-painted and shader-driven approaches. Five bottles have subtle shading variations, each featuring a specific hand-painted detail. In addition, two bottles use a custom shader.

    The shader was designed with flexibility in mind: it simulates a nebulous liquid that can be adjusted for fill level, allowing the same model to represent multiple potion types.
    The artist provided custom noise textures, which are integrated into the shader to ensure visual differentiation across the bottles.

    The entire set looks like this:

    This workflow highlights how art and code decisions were made together to achieve both creative direction and technical efficiency. The bottles are optimized for real-time use and form part of a larger playable project (WIP).

    Script

    The bottles include a custom Wobble.cs script that drives small liquid-like movements based on the object’s motion.

    • Motion-based input: The script tracks both linear velocity (movement in space) and angular velocity (rotation changes).
    • Procedural wobble: These values are converted into subtle sine-wave oscillations on the X and Z axes.
    • Shader communication: At runtime, the script passes the wobble values to the material via the WobbleX and WobbleZ shader properties.

    This setup ensures that the bottle contents appear reactive, tilting, sloshing, and “settling” naturally as the object moves in the scene. The wobble intensity is clamped and gradually recovers over time, giving a convincing physical feel without requiring expensive fluid simulations.

    Shader

    The custom shader handles the liquid simulation inside the bottles:

    • Fill-level control allows designers to reuse the same asset for multiple potion states.
    • Custom noise textures (hand-painted) add variation across bottles while still fitting the same visual style.
    • Integration with the wobble script makes the shader feel dynamic and physically reactive, without the overhead of actual fluid dynamics.

    This shader-driven workflow lets a single mesh and material cover many potion types. Efficient for memory and performant in real-time.

    Conclusion

    The bottles showcase how hand-painted detail and procedural motion can be combined into one asset. Instead of static props, they behave like interactive game objects: wobbling, tilting, and visually differentiating through shader variation.

    This workflow demonstrates a scalable approach: artists define the creative direction through textures and colors, while programmers add physical responsiveness and shader logic. Together, the result is both performant and immersive, ready for integration into a larger playable project.

    Created on September 2025
  • Storage Chest - AlchemisTeddy 🧪🐻

    This animated low-poly alchemist’s chest is designed for real-time gameplay.

    The model uses flat colors to ensure clear readability of interactive elements, while hand-painted details (potion bottle, mushroom) highlight secondary features for added clarity and interest.
    The animation was implemented with gameplay integration in mind, showcasing both technical setup and artistic direction. Optimized for performance, this asset represents an early test of the art–code pipeline within a larger playable WIP project.

    This game-ready asset is composed of 3 scripts and an animator system as explained below.

    Script

    The chest functionality relies on three primary scripts:

    • TreasureChest.cs – handles activation, damage, and persistence.
    • ItemSpawner.cs – spawns items along a parabolic arc with visual feedback.
    • ChestAnimator.cs – links the chest’s open/close state to an Animator.

    This separation ensures modularity: each script has a clear responsibility, making the system easy to extend or reuse.

    TreasureChest.cs

    The TreasureChest script implements multiple small interfaces (IActivatable, IDamageable, ISaveable) to define its behavior without forcing unused methods.
    It controls:

    • Opening/closing in response to player interaction.
    • Item spawning when opened.
    • Damage handling via TakeDamage.
    • State persistence for saving/loading game sessions.

    Examples

    Toggling the chest open state and spawning items is handled elegantly with events:

        /// <summary>
        /// Activates the chest component
        /// </summary>
        /// <param name="activator"></param>
        public void Activate(GameObject activator)
        {
            // Return if chest has health
            if (currentHealth > 0) return;
            OnChestChangeState?.Invoke(isOpen = !isOpen);
    
            if (items.Count > 0)
            {   // Spawn Items in the items list
                foreach (ItemData item in items)
                {
                    itemSpawner.SpawnItem(item);
                }
                items.Clear();
            }
        }

    Persistence is managed through a lightweight string-based serialization system, mapping item IDs back to assets at load time:

    public Dictionary<string, string> CaptureState()
    {
        List<string> itemIDs = items.Select(item => item.ItemID).ToList();
        string itemStateString = string.Join(",", itemIDs);
    
        return new Dictionary<string, string>
        {
            { "isOpen", isOpen.ToString() },
            { "items", itemStateString }
        };
    }

    This design demonstrates clean event-driven programming, modularity, and runtime + persistent state management without bloating the script.

    ItemSpawner.cs

    ItemSpawner allows items to be launched along a customizable parabolic arc, providing dynamic feedback to the player.

    Key features include:

    • Randomized landing positions within a radius.
    • Configurable arc height and travel duration.
    • Coroutine-based movement with optional trail effects.
    • Editor Gizmos for setup visualization.

    Example

    This code snippet show how the arc is drawn in the Editor with Gizmos.

       /// <summary>
       /// Draws Gizmos in the editor when the object is selected to visualize the spawn trajectory.
       /// </summary>
       private void OnDrawGizmos()
       {
           // Ensure we have a spawn point to draw from.
           if (spawnPoint == null) return;
    
           // --- Draw the Landing Zone ---
           Handles.color = Color.green;
           // The third parameter is the "normal" or the direction the circle should face. Vector3.up makes it flat on the XZ plane.
           Handles.DrawWireDisc(landingPoint.position, Vector3.up, landingRadius);
    
           // --- Draw the Arc Path ---
           Gizmos.color = Color.cyan;
           Vector3 previousPoint = spawnPoint.position;
    
           // Loop through a number of steps to draw the arc.
           for (int i = 1; i <= gizmoPathResolution; i++)
           {
               // Calculate the 't' value (normalized progress) for this step.
               float t = (float)i / gizmoPathResolution;
    
               // Use the same math as the coroutine to calculate the point on the arc.
               Vector3 linearPosition = Vector3.Lerp(spawnPoint.position, landingPoint.position, t);
               float arc = 4 * arcHeight * (t - (t * t));
               Vector3 currentPoint = linearPosition + new Vector3(0, arc, 0);
    
               // Draw a line from the previous point to the current one.
               Gizmos.DrawLine(previousPoint, currentPoint);
               previousPoint = currentPoint;
           }
       }

    This system is modular, so any item with a prefab can be launched without additional scripting, and designers can adjust trajectories visually in the editor.

    Animation

    Chest animations are handled by a small, dedicated script: ChestAnimator.cs. Its job is simple but crucial: respond to chest state changes and update the Animator parameters.

    public void OpenChest(bool value)
    {
        animator.SetBool(isOpenHash, value);
    }

    The animator listens to the same OnChestChangeState event from TreasureChest.cs, ensuring decoupling between gameplay logic and animation. This event-driven approach allows the animation system to be reused or swapped without touching the chest’s core logic.

    Optimization

    A small but important optimization is applied in the animation system:

    void Start()
    {
        isOpenHash = Animator.StringToHash("isOpen");
    }

    By caching the parameter hash once at startup, the script avoids repeated string lookups each frame. This is much more performant in real-time gameplay, especially when multiple chests exist in the scene. It’s a simple change that demonstrates attention to both technical efficiency and scalable design.

    Design Considerations

    This setup demonstrates modular, scalable architecture:

    • TreasureChest: gameplay logic, persistence, damage.
    • ItemSpawner: visual and interactive feedback for items.
    • ChestAnimator: purely visual animation tied to game state.

    By separating responsibilities and using events, the system is robust, easy to debug, and ready to extend with new features such as alternative animations, loot systems, or multiplayer synchronization.

    Conclusion

    The alchemist’s storage chest is more than a decorative prop, it’s a fully integrated gameplay system.

    • The scripts handle activation, durability, persistence, and item spawning, making the chest modular and adaptable to different design needs.
    • The animation system cleanly ties into gameplay events, with optimizations that ensure smooth, scalable performance in real-time scenes.

    Together, these elements show how art and code converge in a single prefab. The chest is readable, performant, and ready for integration into a larger playable environment, demonstrating a pipeline that balances technical rigor with creative direction.

    Created on September 2025
  • Crafting Table - AlchemisTeddy 🧪🐻

    This stylized low-poly prefab represents an alchemist’s crafting table, designed as part of a larger playable project. The asset balances functionality and aesthetics: flat colors clearly highlight interactive elements for gameplay, while hand-painted details and a custom cauldron shader add artistic depth and atmosphere.

    Built through close collaboration between art and programming, this piece reflects our ability to merge technical constraints with creative direction. It demonstrates a pipeline suitable for real-time applications, optimized performance, and modular scene integration.

    This crafting table features two scripts and one shader as explained below.

    Scripts

    The crafting table relies on two key scripts: one managing the overall crafting logic, and another handling individual ingredient slots.
    Together, they form the core gameplay loop: the player places items on stations, activates the table, and, if a valid recipe exists, receives the crafted result.

    This modular approach makes the system highly reusable: ingredient stations can be added or removed without rewriting the crafting logic, and the whole table integrates seamlessly with the player’s inventory and save system.

    CraftingTable.cs

    The CraftingTable script serves as the central brain of the system. When the player interacts with the table, the script collects all items currently placed on connected ingredient stations and compares them against the list of available recipes in the player’s inventory.

    If a recipe match is found, the system consumes the ingredients, clears the stations, and either:
    Grants the crafted item directly to the player’s inventory, or Spawns the item in the world through an optional ItemSpawner component.

    This separation ensures flexibility: designers can choose whether results should be instantly stored or physically spawned into the scene.

    The Key responsibilities of this script include:

    • Querying all ingredient stations.
    • Validating combinations against available recipes.
    • Handling success and failure feedback.
    • Maintaining modularity via the IActivatable interface.

    For example, the recipe-matching function is deliberately kept small and clear:

    /// <summary>
    /// Checks the provided ingredients against all available recipes.
    /// </summary>
    /// <returns>The matching CraftingRecipe, or null if no match is found.</returns>
    private CraftingRecipe FindMatchingRecipe(List<ItemData> ingredients)
    {
        foreach (var recipe in playerInventory.GetAvailableRecipes())
        {
            // Check if the recipe can be crafted with the ingredients.
            // Also ensure the number of ingredients matches to prevent crafting with extra items on the table.
            if (recipe.ingredients.Count == ingredients.Count && recipe.CanCraft(ingredients))
            {
                return recipe;
            }
        }
        return null;
    }

    This concise check ensures no false positives occur if extra items are placed, while still making it easy to extend with new recipe logic.

    This orchestration keeps the crafting logic clean and self-contained, while remaining adaptable to future extensions (e.g., timed crafting, animations, or multiplayer interactions).

    IngredientStation.cs

    Each IngredientStation acts as a slot where players can place or remove items. The script ensures only valid ingredient-type items can be placed, instantiates a corresponding 3D prefab for feedback, and returns items to the inventory if removed.

    From a gameplay perspective, this makes the system intuitive: the player sees the ingredients laid out visually on the table, ready for combination.

    From a technical perspective, the script includes:

    • Interaction handling: opening the inventory UI when empty, returning items when full.
    • Visual updates: spawning item prefabs at a defined slot location.
    • Persistence: implementing ISaveable to record which item was present at save time and restoring it on load.
    • Editor feedback: using OnDrawGizmos to visualize station states (empty vs filled) directly in the Unity scene view.

    The placement method shows this defensive approach clearly:

        /// <summary>
        /// Places an item on this station.
        /// </summary>
        public void PlaceItem(ItemData item, PlayerInventoryManager placerInventory = null)
        {
            // We should only accept items that are ingredients.
            if (item.itemType == ItemType.Ingredient)
            {
                currentItem = item;
                Debug.Log($"Placed {item.itemName} on station {gameObject.name}.");
    
                // Update visual model.
                if(currentWorldItem != null) { Destroy(currentWorldItem); currentWorldItem = null; }
                currentWorldItem = Instantiate(currentItem.prefab, worldItemPosition.transform.position, Quaternion.identity, worldItemPosition.transform);
                currentWorldItem.GetComponent<WorldItem>().enabled = false;
                currentWorldItem.layer = 0;
            }
            else
            {
                Debug.LogWarning($"{item.name} is not an ingredient and cannot be placed here.");
                // If a non-ingredient was somehow selected, give it back to the player.
                placerInventory.AddItem(item);
            }
        }

    It validates the item type, cleans up any existing world object, and re-instantiates the prefab in the correct position, all while gracefully handling invalid cases.

    To support saving and loading, the station also implements lightweight persistence:

    Saving

        /// <summary>
        /// Captures the state of the ingredient station for saving.
        /// </summary>
        /// <returns>A dictionary containing the ID of the item on the station, or an empty dictionary if there is no item.</returns>
        public Dictionary<string, string> CaptureState()
        {
            var state = new Dictionary<string, string>();
            // Check if there is an item currently on the station.
            if (currentItem != null)
            {
                // If there is, save its unique ItemID string.
                // use of a clear key like "currentItemId" to know what this data represents.
                state.Add("currentItemId", currentItem.ItemID);
            }
            // If currentItem is null, simply return an empty dictionary.
            // The absence of the key on load will tell the station was empty.
            return state;
        }

    Loading

        /// <summary>
        /// Restores the state of the ingredient station from loaded data.
        /// </summary>
        /// <param name="state">The dictionary containing the saved data.</param>
        public void RestoreState(Dictionary<string, string> state)
        {
            // Check if the loaded data contains a value for our item.
            if (state.TryGetValue("currentItemId", out string savedItemId))
            {
                // If an ID was saved, we need to find the corresponding ItemData asset.
                // /!\ This lookup logic should be centralized for efficiency /!\
                // For simplicity we can use Resources.FindObjectsOfTypeAll here at the moment.
                var allItems = Resources.FindObjectsOfTypeAll<ItemData>();
                ItemData foundItem = null;
                foreach (var itemAsset in allItems)
                {
                    if (itemAsset.ItemID == savedItemId)
                    {
                        foundItem = itemAsset;
                        break; // Found the item, no need to search further.
                    }
                }
                if (foundItem != null)
                {
                    // If matching ItemData asset was found, place it on the station.)
                    PlaceItem(foundItem);
                }
                else
                {
                    Debug.LogWarning($"IngredientStation {gameObject.name} could not find an ItemData asset with saved ID: {savedItemId}. Station will be empty.");
                    currentItem = null; // Ensure station is empty if item not found.
                    if(currentWorldItem != null) Destroy(currentWorldItem);  
                }
            }
            else
            {
                // If no ID was found in the save data, it means the station was empty.
                // Ensure the currentItem is null.
                currentItem = null;
                if (currentWorldItem != null) Destroy(currentWorldItem);
            }
        }

    This approach keeps data handling minimal while still robust. Items are identified by their unique ID, making it straightforward to restore them into the scene, or cleanly reset the station if the asset can’t be found.

    This focus on modularity means the same component could be reused in different contexts such as potion brewing, blacksmithing, or even non-crafting interactions like puzzle pedestals.

    Design Considerations

    Both scripts emphasize extensibility and clarity.
    By relying on interfaces (IActivatable, ISaveable) and modular components, the system remains lightweight, testable, and adaptable. Designers can extend the crafting experience by simply creating new recipes or stations, without touching core logic.

    In practice, this allows the crafting table to serve as more than a one-off object: it becomes a scalable foundation for a wide range of interactive systems across the project.

    Shader

    While the scripts define the crafting logic, the shader brings the table’s visual identity to life. The cauldron isn’t just a static prop—it reacts dynamically, helping players instantly recognize that something magical is happening.

    The shader was designed with two goals in mind:

    • Gameplay readability: clear feedback when crafting is possible or in progress.
    • Stylized aesthetics: hand-painted detail balanced with procedural motion.

    Cauldron Shader

    At its core, the cauldron shader blends flat stylized colors with animated surface effects. By using time-based noise and custom rim lighting, the shader simulates bubbling liquid that feels alive while still fitting into a low-poly art direction.

    Key features include:

    • Color zones: a simple gradient creates depth without relying on heavy textures.
    • UV distortion: subtle scrolling noise adds surface movement to mimic liquid.
    • Emission highlights: glowing edges emphasize the magical energy of the brew.
    • Parameter control: intensity, speed, and color can be tuned to match the recipe or environment.

    Because the shader was built with real-time constraints in mind, it remains lightweight, optimized for performance while still delivering strong atmosphere.

    Integration

    The shader is applied directly to the cauldron mesh and linked to the crafting logic, making it easy to trigger visual states such as:

    • Idle (calm, faint bubbling).
    • Active (stronger glow, faster distortion).
    • Success (a flash or pulse upon crafting).

    This dynamic response turns the crafting table into more than just a container of ingredients: it becomes a living part of the scene, reinforcing feedback loops between art, design, and code.

    Conclusion

    The alchemist’s crafting table represents more than a single asset, it’s a proof of concept for our pipeline.

    By combining modular scripts with a lightweight stylized shader, we created an object that is both functional for gameplay and rich in artistic detail.

    The scripts ensure modularity, persistence, and flexibility, making the system easy to expand with new recipes or stations.
    The shader adds atmosphere and communicates game states visually, keeping the player immersed without extra UI clutter.

    This balance of technical clarity and artistic direction is central to how we approach real-time asset creation. Each piece is designed not just to look good, but to integrate smoothly into a playable environment, optimized, extensible, and ready for iteration.

    In short: the crafting table is a small example of how code and art converge to create interactive storytelling elements in games.

    Created on September 2025
© 2026 Samuel Styles