| name | cocos-graphics-mote-system |
| description | Use when replacing sprite particles with procedural graphics rendering. |
Cocos Graphics Mote (Micro-Particle) System
Use when: you need dozens of small animated particles ("motes" — glowing dots representing other players, blessings, or ambient effects) but have NO sprite/particle assets and want visual feedback immediately via Graphics.
Core Concept
Instead of instantiating Sprite nodes in a pool (which requires a SpriteFrame asset), use a single cc.Graphics component that redraws all motes every frame. Each mote is data only — position, color, alpha, radius — rendered procedurally.
Step-by-Step
1. Animated Mote Data Structure
interface AnimatedMote {
id: string;
x: number;
y: number;
targetX: number;
targetY: number;
scale: number;
alpha: number;
life: number;
color: Color;
isSelf: boolean;
radius: number;
}
2. Component Structure
import { _decorator, Component, Graphics, Color, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('MoteCanvas')
export class MoteCanvas extends Component {
@property public driftSpeed: number = 20;
@property public updateInterval: number = 0.5;
private _gfx: Graphics | null = null;
private _motes: AnimatedMote[] = [];
private _timer: number = 0;
private _width: number = 960;
private _height: number = 640;
start() {
this._gfx = this.getComponent(Graphics) || this.addComponent(Graphics);
}
update(dt: number) {
this._timer += dt;
if (this._timer >= this.updateInterval) {
this._timer = 0;
this._syncFromDataSource();
}
this._updatePositions(dt);
this._updateLifecycles(dt);
this._drawBackground();
this._drawMotes();
}
}
3. Drawing a Single Mote (Glow + Core + Self-Highlight)
private _drawMote(g: Graphics, m: AnimatedMote): void {
if (m.alpha <= 0.01) return;
const alpha255 = Math.floor(m.alpha * 255);
const r = m.radius + m.scale * 4;
g.fillColor = new Color(m.color.r, m.color.g, m.color.b, Math.floor(alpha255 * 0.15));
g.circle(m.x, m.y, r * 2.5);
g.fill();
g.fillColor = new Color(m.color.r, m.color.g, m.color.b, Math.floor(alpha255 * 0.8));
g.circle(m.x, m.y, r);
g.fill();
if (m.isSelf) {
g.strokeColor = new Color(255, 215, 0, Math.floor(alpha255 * 0.8));
g.lineWidth = 2;
g.circle(m.x, m.y, r + 3);
g.stroke();
}
}
4. Position Drift Animation
private _updatePositions(dt: number): void {
for (const m of this._motes) {
const dx = m.targetX - m.x;
const dy = m.targetY - m.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 2) {
m.x += (dx / dist) * this.driftSpeed * dt;
m.y += (dy / dist) * this.driftSpeed * dt;
} else {
const margin = 20;
const halfW = this._width / 2 - margin;
const halfH = this._height / 2 - margin;
m.targetX = m.x + (Math.random() - 0.5) * 80;
m.targetY = m.y + (Math.random() - 0.5) * 80;
m.targetX = Math.max(-halfW, Math.min(halfW, m.targetX));
m.targetY = Math.max(-halfH, Math.min(halfH, m.targetY));
}
}
}
5. Fade In/Out Lifecycle
private _updateLifecycles(dt: number): void {
for (const m of this._motes) {
const age = (Date.now() - m.birthTime) / 1000;
const duration = m.lifetime;
m.life = Math.min(1, age / duration);
if (age < 0.5) {
m.alpha = age / 0.5;
}
else if (age > duration - 2) {
m.alpha = Math.max(0, (duration - age) / 2);
} else {
m.alpha = 1;
}
}
this._motes = this._motes.filter(m => m.life < 1);
}
Color Palette for Activity-Based Motes
A common use case is coloring motes by "practice activity" or "event type":
const MOTE_COLORS: Record<string, Color> = {
meditation: new Color(180, 140, 255, 200),
chanting: new Color(255, 215, 0, 200),
offering: new Color(255, 180, 50, 200),
sutra: new Color(100, 200, 255, 180),
circumamb: new Color(255, 220, 180, 180),
};
When to Use vs. Avoid
Use Graphics motes when:
- You need visual feedback NOW during development
- No artist/asset pipeline available
- Mote count < 100 (Graphics redraw is cheap at this scale)
- You want unique colors per mote type without texture atlasing
- Target platforms are modern devices (desktop / recent mobile)
Switch to ParticleSystem / Sprite when:
- Mote count exceeds 200 (GPU batching becomes important)
- You need texture-based shapes (stars, flames, sparkles)
- Asset pipeline is ready and you have final art
- Targeting low-end mobile devices (Graphics redraw on CPU)
Key Takeaways (BuddhaHeart Project Learnings)
- Always import all classes.
getComponent(PracticeMoteCanvas) without import { PracticeMoteCanvas } throws ReferenceError in ES Module runtime.
- One Graphics draws everything. Unlike Sprite pools (N nodes = N draw calls), a single Graphics component draws N motes in 1 draw call.
- Layer must match Canvas. New Nodes default to
layer = 1073741824 (DEFAULT). Set node.layer = 33554432 (UI_2D) for the orthographic Camera to see them.
- No SpriteFrame needed. This is the killer feature — you can prototype particle-heavy scenes with zero asset imports.
- Handle resize. If your canvas size changes, update
_width / _height and re-clamp mote positions.