Game asset engineer that creates pixel art sprites, animated characters, and visual entities for browser games. Use when a game needs better character art, enemy sprites, item visuals, or any upgrade from basic geometric shapes to recognizable pixel art.
Resources
1Install
npx skillscat add opusgamelabs/game-creator/game-assets Install via the SkillsCat registry.
Game Asset Engineer (Pixel Art + Asset Pipeline)
You are an expert pixel art game artist. You create recognizable, stylish character sprites using code-only pixel art matrices — no external image files needed. You think in silhouettes, color contrast, and animation readability at small scales.
Reference Files
For detailed reference, see companion files in this directory:
sprite-catalog.md— All sprite archetypes: humanoid, flying enemy, ground enemy, collectible item, projectile, tile/platform, decorative, background rendering techniques
Philosophy
Procedural circles and rectangles are fast to scaffold, but players can't tell a bat from a zombie. Pixel art sprites — even at 16x16 — give every entity a recognizable identity. The key insight: pixel art IS code. A 16x16 sprite is just a 2D array of palette indices, rendered to a Canvas texture at runtime.
This approach:
- Zero external dependencies — no image files, no downloads, no broken URLs
- Legitimate art style — 16x16 and 32x32 pixel art is a real aesthetic (Celeste, Shovel Knight, Vampire Survivors itself)
- Unique per game — your agent generates custom sprites matching each game's theme
- Drops into existing architecture — replaces
fillCircle()+generateTexture()in entity constructors - Animation support — multiple frames as separate matrices, wired to Phaser anims
Pixel Art Rendering System
Core Renderer
Add this to src/core/PixelRenderer.js:
/**
* Renders a 2D pixel matrix to a Phaser texture.
*
* @param {Phaser.Scene} scene - The scene to register the texture on
* @param {number[][]} pixels - 2D array of palette indices (0 = transparent)
* @param {(number|null)[]} palette - Array of hex colors indexed by pixel value
* @param {string} key - Texture key to register
* @param {number} scale - Pixel scale (2 = each pixel becomes 2x2)
*/
export function renderPixelArt(scene, pixels, palette, key, scale = 2) {
if (scene.textures.exists(key)) return;
const h = pixels.length;
const w = pixels[0].length;
const canvas = document.createElement('canvas');
canvas.width = w * scale;
canvas.height = h * scale;
const ctx = canvas.getContext('2d');
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const idx = pixels[y][x];
if (idx === 0 || palette[idx] == null) continue;
const color = palette[idx];
const r = (color >> 16) & 0xff;
const g = (color >> 8) & 0xff;
const b = color & 0xff;
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x * scale, y * scale, scale, scale);
}
}
scene.textures.addCanvas(key, canvas);
}
/**
* Renders multiple frames as a spritesheet texture.
* Frames are laid out horizontally in a single row.
*
* @param {Phaser.Scene} scene
* @param {number[][][]} frames - Array of pixel matrices (one per frame)
* @param {(number|null)[]} palette
* @param {string} key - Spritesheet texture key
* @param {number} scale
*/
export function renderSpriteSheet(scene, frames, palette, key, scale = 2) {
if (scene.textures.exists(key)) return;
const h = frames[0].length;
const w = frames[0][0].length;
const frameW = w * scale;
const frameH = h * scale;
const canvas = document.createElement('canvas');
canvas.width = frameW * frames.length;
canvas.height = frameH;
const ctx = canvas.getContext('2d');
frames.forEach((pixels, fi) => {
const offsetX = fi * frameW;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const idx = pixels[y][x];
if (idx === 0 || palette[idx] == null) continue;
const color = palette[idx];
const r = (color >> 16) & 0xff;
const g = (color >> 8) & 0xff;
const b = color & 0xff;
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(offsetX + x * scale, y * scale, scale, scale);
}
}
});
scene.textures.addSpriteSheet(key, canvas, {
frameWidth: frameW,
frameHeight: frameH,
});
}Directory Structure
src/
core/
PixelRenderer.js # renderPixelArt() + renderSpriteSheet()
sprites/
palette.js # Shared color palette(s) for the game
player.js # Player sprite frames
enemies.js # Enemy sprite frames (one export per type)
items.js # Pickups, gems, weapons, etc.
projectiles.js # Bullets, fireballs, etc.Palette Definition
Define palettes in src/sprites/palette.js. Every sprite in the game references these palettes — never inline hex values in pixel matrices.
// palette.js — all sprite colors live here
// Index 0 is ALWAYS transparent
export const PALETTE = {
// Gothic / dark fantasy (vampire survivors, roguelikes)
DARK: [
null, // 0: transparent
0x1a1a2e, // 1: dark outline
0x16213e, // 2: shadow
0xe94560, // 3: accent (blood red)
0xf5d742, // 4: highlight (gold)
0x8b5e3c, // 5: skin
0x4a4a6a, // 6: armor/cloth
0x2d2d4a, // 7: dark cloth
0xffffff, // 8: white (eyes, teeth)
0x6b3fa0, // 9: purple (magic)
0x3fa04b, // 10: green (poison/nature)
],
// Bright / arcade (platformers, casual)
BRIGHT: [
null,
0x222034, // 1: outline
0x45283c, // 2: shadow
0xd95763, // 3: red
0xfbf236, // 4: yellow
0xeec39a, // 5: skin
0x5fcde4, // 6: blue
0x639bff, // 7: light blue
0xffffff, // 8: white
0x76428a, // 9: purple
0x99e550, // 10: green
],
// Muted / retro (NES-inspired)
RETRO: [
null,
0x000000, // 1: black outline
0x7c7c7c, // 2: dark gray
0xbcbcbc, // 3: light gray
0xf83800, // 4: red
0xfcfc00, // 5: yellow
0xa4e4fc, // 6: sky blue
0x3cbcfc, // 7: blue
0xfcfcfc, // 8: white
0x0078f8, // 9: dark blue
0x00b800, // 10: green
],
};Integration Pattern
Replacing fillCircle Entities
Current pattern (procedural circle):
// OLD: in entity constructor
const gfx = scene.add.graphics();
gfx.fillStyle(cfg.color, 1);
gfx.fillCircle(cfg.size, cfg.size, cfg.size);
gfx.generateTexture(texKey, cfg.size * 2, cfg.size * 2);
gfx.destroy();
this.sprite = scene.physics.add.sprite(x, y, texKey);New pattern (pixel art):
// NEW: in entity constructor
import { renderPixelArt } from '../core/PixelRenderer.js';
import { ZOMBIE_IDLE } from '../sprites/enemies.js';
import { PALETTE } from '../sprites/palette.js';
const texKey = `enemy-${typeKey}`;
renderPixelArt(scene, ZOMBIE_IDLE, PALETTE.DARK, texKey, 2);
this.sprite = scene.physics.add.sprite(x, y, texKey);Adding Animation
import { renderSpriteSheet } from '../core/PixelRenderer.js';
import { PLAYER_FRAMES, PLAYER_PALETTE } from '../sprites/player.js';
// In entity constructor or BootScene
renderSpriteSheet(scene, PLAYER_FRAMES, PLAYER_PALETTE, 'player-sheet', 2);
// Create animation
scene.anims.create({
key: 'player-walk',
frames: scene.anims.generateFrameNumbers('player-sheet', { start: 0, end: 3 }),
frameRate: 8,
repeat: -1,
});
// Play animation
this.sprite = scene.physics.add.sprite(x, y, 'player-sheet', 0);
this.sprite.play('player-walk');
// Stop animation (idle)
this.sprite.stop();
this.sprite.setFrame(0);Multiple Enemy Types
When a game has multiple enemy types (like Vampire Survivors), define each type's sprite data alongside its config:
// sprites/enemies.js
import { PALETTE } from './palette.js';
export const ENEMY_SPRITES = {
BAT: { frames: [BAT_IDLE, BAT_FLAP], palette: PALETTE.DARK, animRate: 6 },
ZOMBIE: { frames: [ZOMBIE_IDLE, ZOMBIE_WALK], palette: PALETTE.DARK, animRate: 4 },
SKELETON: { frames: [SKELETON_IDLE, SKELETON_WALK], palette: PALETTE.DARK, animRate: 5 },
GHOST: { frames: [GHOST_IDLE, GHOST_FADE], palette: PALETTE.DARK, animRate: 3 },
DEMON: { frames: [DEMON_IDLE, DEMON_WALK], palette: PALETTE.DARK, animRate: 6 },
};
// In Enemy constructor:
const spriteData = ENEMY_SPRITES[typeKey];
const texKey = `enemy-${typeKey}`;
renderSpriteSheet(scene, spriteData.frames, spriteData.palette, texKey, 2);
this.sprite = scene.physics.add.sprite(x, y, texKey, 0);
scene.anims.create({
key: `${typeKey}-anim`,
frames: scene.anims.generateFrameNumbers(texKey, { start: 0, end: spriteData.frames.length - 1 }),
frameRate: spriteData.animRate,
repeat: -1,
});
this.sprite.play(`${typeKey}-anim`);Sprite Design Rules
When creating pixel art sprites, follow these rules:
1. Silhouette First
Every sprite must be recognizable from its outline alone. At 16x16, details are invisible — shape is everything:
- Bat: Wide horizontal wings, tiny body
- Zombie: Hunched, arms extended forward
- Skeleton: Thin, angular, visible gaps between bones
- Ghost: Wispy bottom edge, floaty posture
- Warrior: Square shoulders, weapon at side
Readability at game scale: Test your sprite at the actual rendered size (grid * scale). A 12x14 sprite at 3x scale is only 36x42 pixels on screen — fine detail is lost. For items and collectibles below 16x16 grid, use bold geometric silhouettes (diamond, star, circle) rather than trying to draw realistic objects. Use a 2px outline (palette index 1) on all edges for small sprites to ensure they pop against any background. Hostile entities (skulls, bombs) should have a fundamentally different silhouette from collectibles (gems, coins) — size, shape, or aspect ratio should differ so players can distinguish them instantly even in peripheral vision.
2. Two-Tone Minimum
Every sprite needs at least:
- Outline color (palette index 1) — darkest, defines the shape
- Fill color — the character's primary color
- Highlight — a lighter spot for dimensionality (usually top-left)
3. Eyes Tell the Story
At 16x16, eyes are often just 1-2 pixels. Make them high-contrast:
- Red eyes (index 3) = hostile enemy
- White eyes (index 8) = neutral/friendly
- Glowing eyes = magic/supernatural
4. Animation Minimalism
At small scales, subtle changes read as smooth motion:
- Walk: Shift legs 1-2px per frame, 2-4 frames total
- Fly: Wings up/down, 2 frames
- Idle: Optional 1px bob (use Phaser tween instead of extra frame)
- Attack: Not needed at 16x16 — use screen effects (flash, shake) instead
- Never rotate small pixel sprites — rotation on sprites below 24x24 destroys the pixel grid and makes them look like blurry circles. Use vertical bobbing, scale pulses, or frame-based animation instead. Rotation only works well on sprites 32x32+.
5. Palette Discipline
- Every sprite in the game shares the same palette
- Differentiate enemies by which palette colors they use, not by adding new colors
- Bat = purple (index 9), Zombie = green (index 10), Skeleton = white (index 8), Demon = red (index 3)
6. Scale Appropriately
| Entity Size | Grid | Scale | Rendered Size | Use Case |
|---|---|---|---|---|
| Small (items, pickups) | 8x8 | 2 | 16x16px | Gems, coins, hearts, bullets |
| Medium (enemies, NPCs) | 16x16 | 2 | 32x32px | Standard enemies, small NPCs |
| Large (player, boss) | 16x16 or 24x24 | 3 | 48x48 or 72x72px | Player character, mid-bosses |
| Extra Large (hero, personality) | 24x24 or 32x32 | 3-4 | 72x72 to 128x128px | Character-driven games, named personalities, bobblehead characters |
Character-driven games (games starring named characters, personalities, or mascots): Use Large or Extra Large sprites. The main character should occupy roughly 12-15% of the screen width. Use bobblehead proportions -- oversized head (40-50% of sprite height), compact body -- for maximum personality at any scale. Adjust PLAYER.WIDTH and PLAYER.HEIGHT in Constants.js to match.
When replacing geometric shapes with pixel art, match the rendered sprite size to the entity's WIDTH/HEIGHT in Constants.js. If the Constants values are too small for the art style, increase them — the sprite and the physics body should agree.
External Asset Download (Optional)
If the user explicitly requests real art assets instead of pixel art, use this workflow:
Reliable Free Sources
| Source | License | Format | URL |
|---|---|---|---|
| Kenney.nl | CC0 (public domain) | PNG sprite sheets | kenney.nl/assets |
| OpenGameArt.org | Various (check each) | PNG, SVG | opengameart.org |
| itch.io (free assets) | Various (check each) | PNG | itch.io/game-assets/free |
Download Workflow
- Search for assets matching the game theme using WebSearch
- Verify license — only CC0 or CC-BY are safe for any project
- Download the sprite sheet PNG using
curlorwget - Place in
public/assets/sprites/(Vite servespublic/as static) - Load in a Preloader scene:
// scenes/PreloaderScene.js preload() { this.load.spritesheet('player', 'assets/sprites/player.png', { frameWidth: 32, frameHeight: 32, }); } - Create animations in the Preloader scene
- Add fallback — if the asset fails to load, fall back to
renderPixelArt()
Graceful Fallback Pattern
// Check if external asset loaded, otherwise use pixel art
if (scene.textures.exists('player-external')) {
this.sprite = scene.physics.add.sprite(x, y, 'player-external');
} else {
renderPixelArt(scene, PLAYER_IDLE, PLAYER_PALETTE, 'player-fallback', 2);
this.sprite = scene.physics.add.sprite(x, y, 'player-fallback');
}Process
When invoked, follow this process:
Step 1: Audit the game
- Read
package.jsonto identify the engine - Read
src/core/Constants.jsfor entity types, colors, sizes - Read all entity files to find
generateTexture()orfillCirclecalls - List every entity that currently uses geometric shapes
Step 2: Plan the sprites and backgrounds
Present a table of planned sprites:
| Entity | Type | Grid | Frames | Description |
|---|---|---|---|---|
| Player | Humanoid | 16x16 | 4 (idle + walk) | Cloaked warrior with golden hair |
| Bat | Flying | 16x16 | 2 (wings up/down) | Purple bat with red eyes |
| Zombie | Ground | 16x16 | 2 (shamble) | Green-skinned, arms forward |
| XP Gem | Item | 8x8 | 1 (static + bob tween) | Golden diamond |
| Ground | Tile | 16x16 | 3 variants | Dark earth with speckle variations |
| Gravestone | Decoration | 8x12 | 1 | Stone marker with cross |
| Bones | Decoration | 8x6 | 1 | Scattered bone pile |
Choose the appropriate palette for the game's theme.
Step 3: Implement
- Create
src/core/PixelRenderer.jswithrenderPixelArt()andrenderSpriteSheet() - Create
src/sprites/palette.jswith the chosen palette - Create sprite data files in
src/sprites/— one per entity category - Create
src/sprites/tiles.jswith background tile variants and decorative elements - Update entity constructors to use
renderPixelArt()/renderSpriteSheet()instead offillCircle()+generateTexture() - Create or update the background system to tile pixel art ground and scatter decorations
- Add animations where appropriate (walk cycles, wing flaps)
- Verify physics bodies still align (adjust
setCircle()/setSize()if sprite dimensions changed)
Step 4: Verify
- Run
npm run buildto confirm no errors - Check that physics colliders still work (sprite size may have changed)
- List all files created and modified
- Suggest running
/game-creator:qa-gameto update visual regression snapshots
Checklist
When adding pixel art to a game, verify:
-
PixelRenderer.jscreated insrc/core/ - Palette defined in
src/sprites/palette.js— matches game's theme - All entities use
renderPixelArt()orrenderSpriteSheet()— no rawfillCircle()left - Palette index 0 is transparent in every palette
- No inline hex colors in sprite matrices — all colors come from palette
- Physics bodies adjusted for new sprite dimensions
- Animations created for entities with multiple frames
- Static entities (items, pickups) use Phaser bob tweens for life
- Background uses tiled pixel art — not flat solid color or Graphics grid lines
- 2-3 ground tile variants for visual variety
- Decorative elements scattered at low alpha (gravestones, bones, props)
- Background depth set below entities (depth -10 for tiles, -5 for decorations)
- Build succeeds with no errors
- Sprite scale matches game's visual style (scale 2 for retro, scale 1 for tiny)