13 KiB
b# 💧 Water Tile Animation Tutorial
Project: NovaFarma
Date: 11. December 2025
Author: Development Team
📚 Table of Contents:
- Overview
- How It Works
- Implementation Steps
- Code Breakdown
- Customization
- Troubleshooting
- Advanced Techniques
🌊 Overview:
Water tiles in NovaFarma use a 4-frame animation system to create realistic water movement with:
- Isometric perspective (diamond-shaped tiles)
- 3D depth effect (visible side faces)
- Animated waves (sine wave pattern)
- Sparkle effects (light reflections)
- Smooth transitions (60 FPS animation)
Result: Living, breathing water that feels organic!
🔧 How It Works:
System Architecture:
TerrainSystem.js
├── createWaterFrames() // Generates 4 animation frames
├── generate() // Creates water tiles on map
└── update() // Cycles through frames (60 FPS)
Animation Flow:
Frame 0 → Frame 1 → Frame 2 → Frame 3 → Loop back to Frame 0
↓ ↓ ↓ ↓
Wave Wave Wave Wave
Offset Offset Offset Offset
0 +3 +6 +9
Frame Duration: ~200ms each
Total Loop Time: ~800ms
FPS: 60 (smooth transitions)
🚀 Implementation Steps:
Step 1: Generate Water Frames
Location: src/systems/TerrainSystem.js
createWaterFrames() {
const tileWidth = 48;
const tileHeight = 48;
const P = 2; // Padding for anti-aliasing
// Generate 4 frames
for (let frame = 0; frame < 4; frame++) {
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
// ... drawing code ...
graphics.generateTexture(`water_frame_${frame}`, tileWidth + P * 2, tileHeight + P * 2);
graphics.destroy();
}
}
Call this in constructor:
constructor(scene) {
// ...
this.createWaterFrames();
}
Step 2: Create Isometric Diamond Shape
// Define isometric coordinates
const xs = P; // Left edge
const xe = 48 + P; // Right edge
const midX = 24 + P; // Center X
const topY = P; // Top point
const midY = 12 + P; // Middle Y
const bottomY = 24 + P; // Bottom point
const depth = 14; // 3D depth for sides
// Draw diamond top surface
graphics.fillStyle(0x33ccff); // Light cyan
graphics.beginPath();
graphics.moveTo(xs, midY); // Left point
graphics.lineTo(midX, topY); // Top point
graphics.lineTo(xe, midY); // Right point
graphics.lineTo(midX, bottomY); // Bottom point
graphics.closePath();
graphics.fill();
Step 3: Add 3D Side Faces
// LEFT FACE - Dark blue
const cLeft = 0x0066aa;
graphics.fillStyle(cLeft);
graphics.beginPath();
graphics.moveTo(midX, bottomY);
graphics.lineTo(midX, bottomY + depth);
graphics.lineTo(xs, midY + depth);
graphics.lineTo(xs, midY);
graphics.closePath();
graphics.fill();
// RIGHT FACE - Darker blue
const cRight = 0x004488;
graphics.fillStyle(cRight);
graphics.beginPath();
graphics.moveTo(xe, midY);
graphics.lineTo(xe, midY + depth);
graphics.lineTo(midX, bottomY + depth);
graphics.lineTo(midX, bottomY);
graphics.closePath();
graphics.fill();
Step 4: Add Wave Animation
// Offset changes per frame (0, 3, 6, 9)
const offset = frame * 3;
// Draw 3 wave lines
graphics.lineStyle(1, 0x66ddff, 0.3); // Semi-transparent white
for (let i = 0; i < 3; i++) {
graphics.beginPath();
const baseY = topY + 6 + i * 5; // Vertical spacing
for (let px = xs; px <= xe; px += 2) {
const relativeX = px - xs;
const waveOffset = Math.sin((relativeX + offset + i * 10) * 0.15) * 1.5;
const py = baseY + waveOffset;
if (px === xs) graphics.moveTo(px, py);
else graphics.lineTo(px, py);
}
graphics.strokePath();
}
Wave Formula Explained:
Math.sin(
(relativeX + offset + i * 10) // Position + animation + wave offset
* 0.15 // Frequency (lower = wider waves)
) * 1.5 // Amplitude (height of waves)
Step 5: Add Sparkle Effects
graphics.fillStyle(0xffffff); // White sparkles
const sparkles = [
{ x: midX - 10 + (frame * 2) % 20, y: midY + 3 },
{ x: midX + 8 - (frame * 3) % 16, y: midY + 8 },
{ x: midX - 4 + Math.floor(frame * 1.5) % 8, y: midY + 13 }
];
sparkles.forEach(s => {
// Draw cross pattern (5 pixels)
graphics.fillRect(s.x, s.y, 1, 1); // Center
graphics.fillRect(s.x - 2, s.y, 1, 1); // Left
graphics.fillRect(s.x + 2, s.y, 1, 1); // Right
graphics.fillRect(s.x, s.y - 2, 1, 1); // Top
graphics.fillRect(s.x, s.y + 2, 1, 1); // Bottom
});
Sparkle Animation:
- Each sparkle moves across the tile per frame
- Uses modulo (
%) to loop position - Creates realistic light reflection effect
Step 6: Animate Water Tiles
Location: src/systems/TerrainSystem.js - update() method
update(time, delta) {
// Update every 200ms
if (!this.lastWaterUpdate) this.lastWaterUpdate = 0;
if (time - this.lastWaterUpdate > 200) {
this.lastWaterUpdate = time;
this.currentWaterFrame = (this.currentWaterFrame + 1) % 4;
// Update all water tiles
for (let key in this.tiles) {
const tile = this.tiles[key];
if (tile.type === 'WATER_DEEP') {
tile.sprite.setTexture(`water_frame_${this.currentWaterFrame}`);
}
}
}
}
Performance Note: Only updates texture reference, not recreating sprites!
🎨 Customization:
Change Water Color:
// Original (light cyan)
const waterColor = 0x33ccff;
// Variations:
const waterColor = 0x0099ff; // Deeper blue
const waterColor = 0x55eeff; // Bright cyan
const waterColor = 0x2266aa; // Dark ocean
const waterColor = 0x88ff88; // Toxic green (swamp)
Adjust Wave Speed:
// Faster animation (100ms per frame)
if (time - this.lastWaterUpdate > 100) { ... }
// Slower animation (500ms per frame)
if (time - this.lastWaterUpdate > 500) { ... }
Change Wave Pattern:
// More waves (5 instead of 3)
for (let i = 0; i < 5; i++) {
const baseY = topY + 4 + i * 3; // Closer spacing
// ...
}
// Bigger waves (higher amplitude)
const waveOffset = Math.sin(...) * 3.0; // Was 1.5
// Faster waves (higher frequency)
const waveOffset = Math.sin((relativeX + offset + i * 10) * 0.3) * 1.5; // Was 0.15
More Sparkles:
const sparkles = [
{ x: midX - 15 + (frame * 2) % 30, y: midY + 2 },
{ x: midX - 5 + (frame * 3) % 10, y: midY + 6 },
{ x: midX + 5 - (frame * 2) % 10, y: midY + 10 },
{ x: midX + 10 + (frame * 4) % 20, y: midY + 14 },
{ x: midX - 8 + Math.floor(frame * 1.5) % 16, y: midY + 18 }
];
🐛 Troubleshooting:
Problem: Water not animating
Solution 1: Check if update() is called
// In GameScene.js update()
if (this.terrainSystem && this.terrainSystem.update) {
this.terrainSystem.update(Date.now(), delta);
}
Solution 2: Verify frames exist
// In browser console:
game.textures.list
// Should show: water_frame_0, water_frame_1, water_frame_2, water_frame_3
Problem: Water tiles are black/missing
Solution: Ensure createWaterFrames() is called before generate()
constructor(scene) {
// ...
this.createWaterFrames(); // MUST come first
this.generate(); // Then generate tiles
}
Problem: Animation is choppy
Solution: Reduce update frequency
// Too fast (every frame = choppy)
if (time - this.lastWaterUpdate > 16) { ... }
// Better (200ms = smooth)
if (time - this.lastWaterUpdate > 200) { ... }
Problem: Water looks pixelated
Solution: Add padding (anti-aliasing)
const P = 2; // Padding MUST be at least 2
graphics.generateTexture(`water_frame_${frame}`, tileWidth + P * 2, tileHeight + P * 2);
🚀 Advanced Techniques:
1. Shore Transitions:
Create special water tiles for edges:
createShoreWaterFrame(frame, edgeType) {
// edgeType: 'north', 'south', 'east', 'west'
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
// Draw water diamond
// ... (same as before)
// Add sand/grass edge overlay
if (edgeType === 'north') {
graphics.fillStyle(0xddaa77); // Sand color
graphics.beginPath();
graphics.moveTo(midX - 10, topY);
graphics.lineTo(midX + 10, topY);
graphics.lineTo(midX, topY + 5);
graphics.closePath();
graphics.fill();
}
graphics.generateTexture(`water_shore_${edgeType}_${frame}`, 52, 52);
graphics.destroy();
}
2. Depth Variations:
Different tile heights for shallow/deep water:
createWaterFrames() {
// Shallow water (lighter, less depth)
this.createWaterVariant('shallow', {
topColor: 0x66ddff,
sideColor: 0x33aadd,
depth: 8
});
// Deep water (darker, more depth)
this.createWaterVariant('deep', {
topColor: 0x0066aa,
sideColor: 0x003366,
depth: 20
});
}
3. Particle Effects:
Add water droplets on click:
// In GameScene.js
this.input.on('pointerdown', (pointer) => {
const tile = this.terrainSystem.getTileAt(pointer.x, pointer.y);
if (tile && tile.type === 'WATER_DEEP') {
this.createWaterSplash(pointer.x, pointer.y);
}
});
createWaterSplash(x, y) {
for (let i = 0; i < 10; i++) {
const particle = this.add.circle(x, y, 2, 0x66ddff);
this.tweens.add({
targets: particle,
x: x + (Math.random() - 0.5) * 30,
y: y - Math.random() * 20,
alpha: 0,
duration: 500,
onComplete: () => particle.destroy()
});
}
}
4. Reflection Effect:
Mirror sprites above water:
createReflection(sprite, gridX, gridY) {
const waterTile = this.tiles[`${gridX},${gridY}`];
if (waterTile && waterTile.type === 'WATER_DEEP') {
const reflection = this.scene.add.sprite(
sprite.x,
sprite.y + 20, // Offset below sprite
sprite.texture.key
);
reflection.setOrigin(0.5, 0);
reflection.setAlpha(0.3);
reflection.setFlipY(true); // Mirror vertically
reflection.setDepth(waterTile.sprite.depth - 1);
return reflection;
}
}
📖 Best Practices:
Performance:
- ✅ Generate frames ONCE in constructor
- ✅ Use texture swapping (not sprite recreation)
- ✅ Update at 200ms intervals (not every frame)
- ✅ Use object pooling for particles
- ❌ Don't recreate graphics every update
Visual Quality:
- ✅ Use padding (P = 2) for smooth edges
- ✅ Add sparkle effects for realism
- ✅ Use isometric perspective for depth
- ✅ Vary side face colors (left darker than right)
- ❌ Don't make waves too fast (looks jittery)
Code Organization:
- ✅ Separate frame generation from animation
- ✅ Use constants for colors/sizes
- ✅ Comment complex math formulas
- ✅ Log creation success (
console.log) - ❌ Don't hardcode magic numbers
💡 Pro Tips:
- Debug mode: Press F12 and type
game.textures.listto see all frames - Performance: Monitor FPS with
game.loop.actualFps - Testing: Change
this.currentWaterFramemanually in console - Variations: Create multiple water types (ocean, river, swamp)
- Polish: Add sound effects (water splash, waves)
🎓 Summary:
What you learned:
- ✅ How to create animated isometric tiles
- ✅ Sine wave animation technique
- ✅ Texture generation in Phaser
- ✅ Frame-based animation system
- ✅ 3D depth effect with side faces
Next steps:
- Experiment with colors and speeds
- Add shore transitions
- Create particle effects
- Implement reflection system
📚 Related Files:
c:\novafarma\src\systems\TerrainSystem.js 👈 Main implementation
c:\novafarma\docs\WATER_ANIMATION.md 👈 This file
Reference implementation:
Lines 237-324 in TerrainSystem.js
Happy animating! 💧🌊
Last updated: 11.12.2025 - 20:12