novo
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
# 🎨 SPRITE SHEET SYSTEM
|
||||
**Novafarma - Sprite Atlas & Animation Guide**
|
||||
|
||||
**Date:** 10.12.2025
|
||||
**Status:** In Development
|
||||
|
||||
---
|
||||
|
||||
## 📊 **Current Sprite Sheets**
|
||||
|
||||
### **1. Player Walk Animation**
|
||||
```javascript
|
||||
// Loaded in PreloadScene.js line 94
|
||||
this.load.spritesheet('player_walk', 'assets/sprites/player_walk_strip.png', {
|
||||
frameWidth: 64,
|
||||
frameHeight: 64
|
||||
});
|
||||
|
||||
// Animation created (line 101-106)
|
||||
this.anims.create({
|
||||
key: 'player_walk_anim',
|
||||
frames: this.anims.generateFrameNumbers('player_walk', { start: 0, end: 5 }),
|
||||
frameRate: 12,
|
||||
repeat: -1
|
||||
});
|
||||
```
|
||||
|
||||
**Specs:**
|
||||
- **File:** `assets/sprites/player_walk_strip.png`
|
||||
- **Frame Size:** 64x64 pixels
|
||||
- **Frames:** 6 (0-5)
|
||||
- **Frame Rate:** 12 FPS
|
||||
- **Total Width:** 384px (6 frames × 64px)
|
||||
- **Total Height:** 64px
|
||||
|
||||
---
|
||||
|
||||
### **2. Zombie Walk Animation**
|
||||
```javascript
|
||||
// Loaded in PreloadScene.js line 95
|
||||
this.load.spritesheet('zombie_walk', 'assets/sprites/zombie_walk_strip.png', {
|
||||
frameWidth: 64,
|
||||
frameHeight: 64
|
||||
});
|
||||
|
||||
// Animation created (line 108-113)
|
||||
this.anims.create({
|
||||
key: 'zombie_walk_anim',
|
||||
frames: this.anims.generateFrameNumbers('zombie_walk', { start: 0, end: 5 }),
|
||||
frameRate: 8,
|
||||
repeat: -1
|
||||
});
|
||||
```
|
||||
|
||||
**Specs:**
|
||||
- **File:** `assets/sprites/zombie_walk_strip.png`
|
||||
- **Frame Size:** 64x64 pixels
|
||||
- **Frames:** 6 (0-5)
|
||||
- **Frame Rate:** 8 FPS (slower than player)
|
||||
- **Total Width:** 384px
|
||||
- **Total Height:** 64px
|
||||
|
||||
---
|
||||
|
||||
## 📦 **Sprite Sheet Format**
|
||||
|
||||
### **Horizontal Strip (Current)**
|
||||
```
|
||||
[Frame 0][Frame 1][Frame 2][Frame 3][Frame 4][Frame 5]
|
||||
64px 64px 64px 64px 64px 64px
|
||||
```
|
||||
|
||||
**Advantages:**
|
||||
- ✅ Simple to create
|
||||
- ✅ Easy to visualize
|
||||
- ✅ Small file for few frames
|
||||
|
||||
**Disadvantages:**
|
||||
- ❌ Wide files for many frames
|
||||
- ❌ No multi-direction support
|
||||
|
||||
---
|
||||
|
||||
### **Grid Layout (Recommended for Full Animations)**
|
||||
```
|
||||
[Idle 1][Idle 2][Idle 3][Idle 4]
|
||||
[Walk 1][Walk 2][Walk 3][Walk 4]
|
||||
[Run 1][Run 2][Run 3][Run 4]
|
||||
[Attack1][Attack2][Attack3][Attack4]
|
||||
```
|
||||
|
||||
**Advantages:**
|
||||
- ✅ Organized by animation type
|
||||
- ✅ Square-ish texture (better for GPU)
|
||||
- ✅ Easy to add new animations
|
||||
|
||||
---
|
||||
|
||||
### **Directional Grid (Best for Isometric)**
|
||||
```
|
||||
[DOWN] [LEFT] [RIGHT] [UP]
|
||||
[Frame0][Frame1] [Frame0][Frame1] [Frame0][Frame1] [Frame0][Frame1]
|
||||
|
||||
Example for Player (4 directions × 4 frames = 16 total):
|
||||
64px 64px 64px 64px 64px 64px 64px 64px
|
||||
┌───────┬───────┐┌───────┬───────┐┌───────┬───────┐┌───────┬───────┐
|
||||
│Down 0 │Down 1 ││Left 0 │Left 1 ││Right0 │Right1 ││Up 0 │Up 1 │
|
||||
├───────┼───────┤├───────┼───────┤├───────┼───────┤├───────┼───────┤
|
||||
│Down 2 │Down 3 ││Left 2 │Left 3 ││Right2 │Right3 ││Up 2 │Up 3 │
|
||||
└───────┴───────┘└───────┴───────┘└───────┴───────┘└───────┴───────┘
|
||||
|
||||
Total Size: 512px × 128px (8 columns × 2 rows)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **Recommended Sprite Sheets**
|
||||
|
||||
### **Priority 1: Player Complete** (64x64)
|
||||
```javascript
|
||||
// player_complete.png: 512x256 (8 cols × 4 rows)
|
||||
Rows:
|
||||
0: Walk Down (4 frames)
|
||||
1: Walk Left (4 frames)
|
||||
2: Walk Right (4 frames)
|
||||
3: Walk Up (4 frames)
|
||||
|
||||
// Total: 32 frames (8×4)
|
||||
// File size: ~50-100 KB
|
||||
```
|
||||
|
||||
**Loading Code:**
|
||||
```javascript
|
||||
this.load.spritesheet('player_complete', 'assets/sprites/player_complete.png', {
|
||||
frameWidth: 64,
|
||||
frameHeight: 64
|
||||
});
|
||||
```
|
||||
|
||||
**Animation Code:**
|
||||
```javascript
|
||||
// Walk Down
|
||||
this.anims.create({
|
||||
key: 'player_walk_down',
|
||||
frames: this.anims.generateFrameNumbers('player_complete', { start: 0, end: 3 }),
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
// Walk Left
|
||||
this.anims.create({
|
||||
key: 'player_walk_left',
|
||||
frames: this.anims.generateFrameNumbers('player_complete', { start: 8, end: 11 }),
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
// Walk Right
|
||||
this.anims.create({
|
||||
key: 'player_walk_right',
|
||||
frames: this.anims.generateFrameNumbers('player_complete', { start: 16, end: 19 }),
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
// Walk Up
|
||||
this.anims.create({
|
||||
key: 'player_walk_up',
|
||||
frames: this.anims.generateFrameNumbers('player_complete', { start: 24, end: 27 }),
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Priority 2: Player Actions** (64x64)
|
||||
```javascript
|
||||
// player_actions.png: 256x128 (4 cols × 2 rows)
|
||||
Row 0: Idle animation (4 frames)
|
||||
Row 1: Attack animation (4 frames)
|
||||
|
||||
// Total: 8 frames
|
||||
```
|
||||
|
||||
**Animations:**
|
||||
```javascript
|
||||
// Idle
|
||||
this.anims.create({
|
||||
key: 'player_idle',
|
||||
frames: this.anims.generateFrameNumbers('player_actions', { start: 0, end: 3 }),
|
||||
frameRate: 6,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
// Attack
|
||||
this.anims.create({
|
||||
key: 'player_attack',
|
||||
frames: this.anims.generateFrameNumbers('player_actions', { start: 4, end: 7 }),
|
||||
frameRate: 15,
|
||||
repeat: 0 // Play once
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Priority 3: Zombie Complete** (64x64)
|
||||
```javascript
|
||||
// zombie_complete.png: 512x128 (8 cols × 2 rows)
|
||||
Row 0: Walk cycle (8 frames)
|
||||
Row 1: Attack cycle (8 frames)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Priority 4: Items Atlas** (32x32)
|
||||
```javascript
|
||||
// items_atlas.png: 512x512 (16 cols × 16 rows = 256 items)
|
||||
|
||||
Items organized by category:
|
||||
Rows 0-2: Tools (axe, pickaxe, hoe, sword, etc.)
|
||||
Rows 3-5: Resources (wood, stone, iron_ore, gold, etc.)
|
||||
Rows 6-8: Seeds & Crops (wheat, carrot, potato, etc.)
|
||||
Rows 9-11: Food (bread, stew, apple, etc.)
|
||||
Rows 12-14: Buildings (fence, chest, furnace, etc.)
|
||||
Rows 15: Special items (key, blueprint, etc.)
|
||||
```
|
||||
|
||||
**Loading:**
|
||||
```javascript
|
||||
this.load.spritesheet('items_atlas', 'assets/sprites/items_atlas.png', {
|
||||
frameWidth: 32,
|
||||
frameHeight: 32
|
||||
});
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```javascript
|
||||
// Get specific item frame
|
||||
const axeFrame = scene.textures.getFrame('items_atlas', 0); // First item
|
||||
const pickaxeFrame = scene.textures.getFrame('items_atlas', 1); // Second item
|
||||
|
||||
// Create sprite from atlas
|
||||
const axeIcon = scene.add.sprite(x, y, 'items_atlas', 0);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ **Creating Sprite Sheets**
|
||||
|
||||
### **Method 1: Piskel (Free Online)**
|
||||
1. Go to [piskelapp.com](https://www.piskelapp.com/)
|
||||
2. Create 64x64 canvas
|
||||
3. Draw each frame
|
||||
4. Export as sprite sheet
|
||||
5. Configure:
|
||||
- Format: PNG
|
||||
- Layout: Horizontal strip or Grid
|
||||
- Spritesheet: Yes
|
||||
- Frame size: 64x64
|
||||
|
||||
---
|
||||
|
||||
### **Method 2: Aseprite (Paid, Best)**
|
||||
1. File → New → Sprite
|
||||
- Width: 64px
|
||||
- Height: 64px
|
||||
2. Draw animation frames
|
||||
3. File → Export Sprite Sheet
|
||||
- Sheet Type: By rows/columns
|
||||
- Columns: 8 (or as needed)
|
||||
- Padding: 0
|
||||
- Format: PNG
|
||||
|
||||
---
|
||||
|
||||
### **Method 3: Python Script (Automated)**
|
||||
```python
|
||||
# combine_sprites.py
|
||||
from PIL import Image
|
||||
import os
|
||||
|
||||
def create_sprite_sheet(images_folder, output_file, frame_width=64, frame_height=64, columns=8):
|
||||
"""Combine individual PNG files into sprite sheet"""
|
||||
|
||||
# Get all PNG files
|
||||
files = sorted([f for f in os.listdir(images_folder) if f.endswith('.png')])
|
||||
|
||||
if not files:
|
||||
print("No PNG files found!")
|
||||
return
|
||||
|
||||
# Calculate sheet dimensions
|
||||
num_frames = len(files)
|
||||
rows = (num_frames + columns - 1) // columns
|
||||
|
||||
sheet_width = columns * frame_width
|
||||
sheet_height = rows * frame_height
|
||||
|
||||
# Create blank sheet
|
||||
sheet = Image.new('RGBA', (sheet_width, sheet_height), (0, 0, 0, 0))
|
||||
|
||||
# Paste each frame
|
||||
for idx, filename in enumerate(files):
|
||||
img = Image.open(os.path.join(images_folder, filename))
|
||||
img = img.resize((frame_width, frame_height), Image.NEAREST) # Pixel art scaling
|
||||
|
||||
col = idx % columns
|
||||
row = idx // columns
|
||||
|
||||
x = col * frame_width
|
||||
y = row * frame_height
|
||||
|
||||
sheet.paste(img, (x, y))
|
||||
print(f"Added {filename} at ({x}, {y})")
|
||||
|
||||
sheet.save(output_file)
|
||||
print(f"✅ Sprite sheet saved: {output_file} ({sheet_width}x{sheet_height})")
|
||||
print(f" Frames: {num_frames}, Columns: {columns}, Rows: {rows}")
|
||||
|
||||
# Usage
|
||||
create_sprite_sheet('frames/player_walk/', 'player_walk_strip.png', columns=6)
|
||||
create_sprite_sheet('frames/zombie_walk/', 'zombie_walk_strip.png', columns=6)
|
||||
create_sprite_sheet('frames/items/', 'items_atlas.png', frame_width=32, frame_height=32, columns=16)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📐 **Sprite Sheet Calculator**
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Calculate sprite sheet dimensions
|
||||
*/
|
||||
function calculateSheetSize(numFrames, frameSize, columns) {
|
||||
const rows = Math.ceil(numFrames / columns);
|
||||
const width = columns * frameSize;
|
||||
const height = rows * frameSize;
|
||||
|
||||
console.log(`Frames: ${numFrames}`);
|
||||
console.log(`Layout: ${columns} cols × ${rows} rows`);
|
||||
console.log(`Size: ${width}×${height}px`);
|
||||
console.log(`File: ~${Math.ceil(width * height * 4 / 1024)}KB`);
|
||||
|
||||
return { width, height, rows };
|
||||
}
|
||||
|
||||
// Examples:
|
||||
calculateSheetSize(6, 64, 6); // Player walk: 384×64
|
||||
calculateSheetSize(32, 64, 8); // Player full: 512×256
|
||||
calculateSheetSize(256, 32, 16); // Items atlas: 512×512
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 **Frame Naming Convention**
|
||||
|
||||
### **For Individual Frames (Before Combining):**
|
||||
```
|
||||
player_walk_down_0.png
|
||||
player_walk_down_1.png
|
||||
player_walk_down_2.png
|
||||
player_walk_down_3.png
|
||||
player_walk_left_0.png
|
||||
...
|
||||
```
|
||||
|
||||
### **For Sprite Sheets:**
|
||||
```
|
||||
player_complete.png (all animations)
|
||||
player_walk.png (only walk cycle)
|
||||
player_actions.png (idle, attack, etc.)
|
||||
zombie_complete.png (all zombie animations)
|
||||
items_atlas.png (all items)
|
||||
terrain_tiles.png (all terrain)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 **Animation State Machine**
|
||||
|
||||
```javascript
|
||||
class AnimationController {
|
||||
constructor(sprite) {
|
||||
this.sprite = sprite;
|
||||
this.currentAnim = null;
|
||||
}
|
||||
|
||||
playWalk(direction) {
|
||||
const animKey = `player_walk_${direction}`;
|
||||
if (this.currentAnim !== animKey) {
|
||||
this.sprite.anims.play(animKey);
|
||||
this.currentAnim = animKey;
|
||||
}
|
||||
}
|
||||
|
||||
playIdle() {
|
||||
if (this.currentAnim !== 'player_idle') {
|
||||
this.sprite.anims.play('player_idle');
|
||||
this.currentAnim = 'player_idle';
|
||||
}
|
||||
}
|
||||
|
||||
playAttack() {
|
||||
this.sprite.anims.play('player_attack');
|
||||
this.currentAnim = 'player_attack';
|
||||
|
||||
// Return to idle when attack finishes
|
||||
this.sprite.once('animationcomplete', () => {
|
||||
this.playIdle();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 **TODO: Sprite Sheets Needed**
|
||||
|
||||
### **🔴 High Priority:**
|
||||
- [ ] `player_complete.png` (512×256, 32 frames) - 4 directions
|
||||
- [ ] `zombie_complete.png` (512×128, 16 frames) - walk + attack
|
||||
- [ ] `items_atlas.png` (512×512, 256 items) - all game items
|
||||
|
||||
### **🟡 Medium Priority:**
|
||||
- [ ] `npc_merchant.png` (256×64, 4 frames) - idle animation
|
||||
- [ ] `animals.png` (512×128, 16 frames) - cow, chicken animations
|
||||
- [ ] `effects.png` (256×256, 64 frames) - hit, explosion, sparkles
|
||||
|
||||
### **🟢 Low Priority:**
|
||||
- [ ] `ui_elements.png` (512×512) - buttons, icons, borders
|
||||
- [ ] `terrain_tiles.png` (512×512) - all terrain variations
|
||||
- [ ] `crops.png` (256×128) - growth stages for all crops
|
||||
|
||||
---
|
||||
|
||||
## 💾 **File Size Optimization**
|
||||
|
||||
### **PNG Optimization Tools:**
|
||||
```bash
|
||||
# NPM package
|
||||
npm install -g pngquant
|
||||
|
||||
# Optimize sprite sheet
|
||||
pngquant --quality=65-80 player_complete.png -o player_complete_opt.png
|
||||
|
||||
# Batch optimization
|
||||
pngquant --quality=65-80 assets/sprites/*.png --ext .png --force
|
||||
```
|
||||
|
||||
### **Expected Sizes:**
|
||||
- **64x64 × 32 frames** (Player): ~80-120 KB
|
||||
- **32x32 × 256 items** (Items): ~200-300 KB
|
||||
- **Total assets**: ~1-2 MB (very reasonable!)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **Next Steps**
|
||||
|
||||
1. **Create Priority Sheets:**
|
||||
- Use Piskel/Aseprite to create `player_complete.png`
|
||||
- Generate `items_atlas.png` with Python script
|
||||
|
||||
2. **Update PreloadScene:**
|
||||
- Add new sprite sheet loads
|
||||
- Create animations for all directions
|
||||
|
||||
3. **Update Player.js:**
|
||||
- Use AnimationController
|
||||
- Switch animations based on movement
|
||||
|
||||
4. **Performance Test:**
|
||||
- Measure FPS with full animations
|
||||
- Optimize if needed
|
||||
|
||||
---
|
||||
|
||||
**Status:** 📋 **Planning Complete - Ready for Implementation**
|
||||
**Tools:** Piskel (free) or Aseprite ($20)
|
||||
**Python Script:** Ready for batch processing
|
||||
Reference in New Issue
Block a user