Recently, our team at Octanta Studio released the Touch Effect System asset. Initially, we were looking for a way to implement a heat trail from touches, and the most optimized way to do this in Unity for mobile turned out to be through UI shaders.
The mechanism is as follows: the shader fully controls the behavior of the graphic effect, the material from the shader is applied to a UI particle, and the particle is reused at touch locations. Later, we realized that this solution is not only for trails but also for regular touch effects that do not require drawing, are flexible in customization, and reduce CPU load.
Why Touch Effects are Needed
At first glance, touch effects are decorative, like a bow on the side. However, they are one of the important details necessary for improving UX, like any other VFX. The user gets immediate and tangible feedback on their actions. According to our observations, touch effects are especially common in Japanese F2P mobile games as a means of increasing engagement. In some games, monetization is built around "skins" for touch effects (e.g., Fruit Ninja). In others, they are used as a pleasant little addition that enhances immersiveness (e.g., touching the ground in Gwent). Here are some interesting studies on this topic:
On how tactile feedback (haptics + visual effects) solves the problem of the "incorporeality" of games, reducing the distance between the person and the screen. Link 1
On the positive impact of tactile feedback on reward perception and behavioral motivation. Although the article is more about vibration as a means of encouraging sales, this also applies to visual effects. Link 2
On how the audio-visual experience increases engagement and can stimulate clicks/purchases - a common practice in live-service/gacha games. Link 3
On how visual effects adjust other methods of responding to user input. Link 4
Besides direct dialogue with the user through the screen, visual feedback can be used for other development purposes: for example, for recording mobile screens during testing; for creating tutorial hints that simulate touches on necessary buttons; for replacing heavy world effects.
It is important that touch effects do not slow down the game or interfere with UI readability, so priority #1 when creating the Touch Effect System was optimization, and #2 was flexibility in customization. In addition to the prepared shaders and settings, we provide a guide for other developers on how to independently implement any touch effect using AI.
Why UI Shaders are the Best Solution for Touch Effects in Unity
- Visual Flexibility. Effects - gradients, glows, animations - are calculated mathematically. Colors, highlights, blur, or outlines can be adjusted with sliders. There is no need to draw or keep packs of textures in the project.
- Minimal Size. The shader for the circle example weighs ~6 KB, the material ~1.3 KB. That's all that's needed for the effect.
- Performance. Animations are computed inside the shader, without changing Transform or Canvas components. This reduces CPU load. Relevant for mobile.
- Since it's UI, the effects work on top of any graphics, regardless of the pipeline (URP/HDRP/Built-in).
Writing Your Own Shader for a Touch Effect
Here is an example of a shader for a basic touch effect in the form of a circle. There is also an example of a more complex shader for a stain effect with random shape generation. And an example of a complex shader with animation that simulates touching a matrix. The last two are shown in the video.
Shader "UI/TouchPointCircle"
{
Properties
{
// Core system properties - managed by TouchGlowUI script
[HideInInspector] _MainTex ("Texture", 2D) = "white" {}
[HideInInspector] _TimeNow ("Time Now", Float) = 0
[HideInInspector] _StartTime ("Start Time", Float) = 0
[HideInInspector] _Lifetime ("Lifetime", Float) = 2.0
// Core animation properties
[Toggle] _Scaling ("Disappearing Scaling", Float) = 0
[Toggle] _Fading ("Disappearing Fading", Float) = 1
// Core layer properties - solid center of the particle
_CoreColor ("Core Color", Color) = (1, 1, 1, 1)
_CoreSize ("Core Size", Range(0.1, 0.4)) = 0.25
_CoreOpacity ("Core Opacity", Range(0.0, 1.0)) = 1.0
// Glow layer properties - soft outer illumination
_GlowColor ("Glow Color", Color) = (1, 1, 1, 1)
_GlowBlur ("Glow Blur", Range(0.0, 1.0)) = 0.3
_GlowOpacity ("Glow Opacity", Range(0.0, 1.0)) = 0.3
// Ring layer properties - optional outer ring structure
_RingColor ("Ring Color", Color) = (1, 1, 1, 1)
_RingThickness ("Ring Thickness", Range(0.01, 0.1)) = 0
_RingOpacity ("Ring Opacity", Range(0.0, 1.0)) = 0
// Platform optimization and touch behavior properties
[HideInInspector] _UseMobileOptimization ("Use Mobile Optimization", Float) = 0
_OneTouchHoldAge ("OneTouch Hold Age", Range(0.0, 1.0)) = 0
[Toggle] _HoldingForbidden ("Holding Forbidden", Float) = 0
}
SubShader
{
// UI transparency rendering configuration
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" }
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
ZWrite Off
ZTest LEqual
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
// Shader property declarations
sampler2D _MainTex; float4 _MainTex_ST;
float _TimeNow, _StartTime, _Lifetime;
fixed4 _CoreColor, _GlowColor, _RingColor;
float _CoreSize, _CoreOpacity, _GlowBlur, _GlowOpacity, _RingThickness, _RingOpacity;
float _Scaling, _Fading;
float _UseMobileOptimization;
v2f vert (appdata v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
// === PARTICLE LIFETIME MANAGEMENT ===
float age = (_TimeNow - _StartTime) / _Lifetime;
if (age < 0.0 || age >= 1.0) return float4(0, 0, 0, 0);
// === COORDINATE SYSTEM AND DISTANCE CALCULATION ===
// Circle uses standard Euclidean distance from center
float distance = length(i.uv - 0.5);
// === SIZE SCALING OVER LIFETIME ===
float sizeScale = 1.0 - age;
if(_Scaling == 0)
{
sizeScale = 1;
}
// === LAYER GENERATION SYSTEM ===
// Core layer - solid center with sharp edges
float core = step(distance, _CoreSize * sizeScale);
// Glow layer - soft outer illumination with blur control
float glowRadius = 0.35 * sizeScale;
float glowSoftness = 0.1 * _GlowBlur;
float glow = 1.0 - smoothstep(glowRadius - glowSoftness, glowRadius + glowSoftness, distance);
// Ring layer - optional outer ring structure
float ringRadius = min(0.42 * sizeScale, 0.42);
float ringInner = ringRadius - _RingThickness;
float ringOuter = ringRadius;
float ring = smoothstep(ringInner - 0.02, ringInner, distance) *
(1.0 - smoothstep(ringOuter, ringOuter + 0.02, distance));
// === FADE EFFECTS ===
// Edge fade prevents artifacts at texture boundaries
float2 edgeDistance = min(i.uv, 1.0 - i.uv);
float edgeFade = smoothstep(0.0, 0.05, min(edgeDistance.x, edgeDistance.y));
// Time fade creates natural particle death animation
float timeFade = 1.0 - age;
if(_Fading == 0)
{
timeFade = 1;
}
// === LAYER COMPOSITION ===
// Calculate alpha values for each layer
float coreAlpha = core * _CoreOpacity * timeFade * edgeFade;
float glowAlpha = glow * _GlowOpacity * timeFade * edgeFade;
float ringAlpha = ring * _RingOpacity * timeFade * edgeFade;
// === OPTIMIZED COLOR BLENDING ===
// Determine layer dominance without branching using step functions
float finalAlpha = max(max(coreAlpha, glowAlpha), ringAlpha);
float coreWeight = step(max(glowAlpha, ringAlpha), coreAlpha);
float ringWeight = step(glowAlpha, ringAlpha) * (1.0 - coreWeight);
float glowWeight = (1.0 - coreWeight) * (1.0 - ringWeight);
// Blend colors based on layer weights
float3 finalColor = _CoreColor.rgb * coreWeight +
_RingColor.rgb * ringWeight +
_GlowColor.rgb * glowWeight;
return float4(finalColor, finalAlpha);
}
ENDHLSL
}
}
Fallback "UI/Default"
}
To create your own touch effect based on these, upload them all to your neural network as examples. And use a prompt like this, provide a description of what kind of touch effect is needed:
Help me in system development: briefly, clearly, without emojis and unicode symbols inside, do not invent anything yourself. We will work sequentially, from task to task. We will work with the following: study how the touch effect system works. Our task is to create a new type of particle. Unity UI shader. I will show you existing ones as references, and you will make a new particle. It is important to preserve the general architecture - life cycle, the ability to disable movement, margins from edges, overall smoothness, etc. The effect should be like: ...
The general architecture is important:
- The life cycle is needed to animate the touch effect from appearance to complete fade-out. By moving this logic to the shader, there is no need to animate (scale/fade) objects on the scene itself. There is no need to adjust complex and diverse material properties, only update the lifetime so that the particle animates itself according to the shader formula.
- The ability to disable movement determines whether the touch effect will follow the touch or appear and fade in one place independently of it.
- Margins from edges are needed so that the effect fits within the square UI particle and is fully visible.
Testing
- Create a material from your new shader (Right-click > Create > Material).
- Assign this material to the GlowMaterial slot of the TouchGlowUI script (e.g., on a prefab like TouchCircle).
- Test in the scene.
The AI might not get it perfect on the first try. Iteration is key. Tweak your prompt and the description based on the results.
Once the visual is right, explore the Input Controller's advanced settings:
- Trigger effects only on UI elements or objects with specific tags.
- Call effects manually from your own scripts for specific game events.
- Pair the effect with a sound.
- Transform a one-touch effect into a Trail by emitting multiple particles in sequence - this is exactly how our heat trail is implemented.
Contacts
Touch Effect System Asset page: https://u3d.as/3C7x
If you need:
- A custom touch effect developed specifically for your project.
- Technical support while building your own.
Reach out to us:
- Email: octantastudio@gmail.com.
- Discord: Join Octanta Studio channel https://discord.gg/6SPxKpFZFC and use branch ‘to-developers’. We will provide an answer or create a special YouTube tutorial.
Top comments (0)