Blog
Posts
  • 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…
    Read More
    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.

    Read More
    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).

    Read More
    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.

    Read More
    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.

    Read More
    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.

    I hope this proves useful.

    Read More
    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.

    Read More
    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.

    Read More
    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.

    Read More
    Created on September 2025
  • Unity Tips - Inspector Attributes

    For the Swiss Game Academy, I had to build a project to teach students some good practices to use in Unity. The first chapter of the course was focused on Attributes, ScriptableObjects, Enums and Inheritance.
    This post talks about the Unity Attributes.

    Example

    When building tools or data-driven systems in Unity, the Inspector can quickly become cluttered. Luckily, Unity provides attributes like [Header], [Tooltip], [Range], and more to make your scripts much more user-friendly!

    Here’s a quick example with a ScriptableObject Item:

    [CreateAssetMenu(fileName = "New Item", menuName = "Alchemist's Inventory/Item")]
    public class ItemData : ScriptableObject
    {
        [Header("Core Item Information")]
        [Tooltip("The name of the item shown in the UI.")]
        public string itemName;
    
        [Tooltip("The description of the item.")]
        [TextArea(3, 5)]
        public string description;
    
        [Tooltip("The monetary value of the item.")]
        [Range(0, 999)]
        public int value;
    
        [Space(15)]
        [Header("Item Graphical Settings")]
        [Tooltip("The icon that represents this item.")]
        public Sprite icon;
    
        [Tooltip("The prefab spawned in the world.")]
        public GameObject prefab;
    
        [Tooltip("Trail colors when the object is spawned.")]
        [ColorUsage(true, true)]
        public Color[] trailColors = new Color[2];
    }
    

    Result

    Here are the results:

    Before using attributesAfter using attributes

    In this case fields are still readable but this is a simple item. On much larger ones, the result might be much more confusing.

    Conclusion

    That is why we use Attributes like:

    • [Header("The name of the header")] To creates categories in the Inspector for readability
    • [Tooltip] To add hover descriptions (great for designers on your team)
    • [Space] to add visual separation between sections.
    • [Range] to add a slider, preventing invalid values
    • [TextArea] to let you write multi-line text fields
    • [ColorUsage] to enable HDR colors for vibrant effects

    These tiny attributes may look simple, but they dramatically improve workflow and collaboration, especially when working with non-programmers.
    There are multiple other attributes that can be useful and a github page lists some of the most useful ones HERE

    Read More
    Created on September 2025
© 2025 Samuel Styles