DEV Community

penny penguin
penny penguin

Posted on

How I Built a Free Forest Tile Pack for Indie Devs

How I Built a Free Forest Tile Pack for Indie Devs

The story of 16 tiles, one palette, and a lot of "is this green enough?"


You know that moment in a game jam when you've got the mechanics working, the game loop is tight, the player can jump and collect things — and then you stare at the blank canvas and realize you have zero art?

That's where most jam entries die. Not because the code is bad. Because the world is missing.

I built the Forest Tile Pack to fix exactly that. 16 hand-crafted 32×32 pixel-art tiles, a combined 512×512 tilesheet, a labeled preview, and a CC0 license. Free for personal and commercial use. No attribution required.

Here's the honest behind-the-scenes: what I planned, what I got wrong, what I learned, and how you can use it in your project in under two minutes.


The Problem I Was Solving

I'd been building pixel-art tools for indie devs — palette extractors, dither generators, tilemap editors — and I kept hitting the same wall: I needed test tiles.

Every tool I built required sample art to demonstrate it. Every demo I made needed a scene to render. And every time, I was either:

  1. Stealing a tileset from somewhere (and feeling guilty about attribution), or
  2. Drawing one from scratch (and spending 3 hours on something that should take 20 minutes), or
  3. Using a placeholder that made the tool look unfinished.

So I decided to draw the pack myself. Not as a product. As a tool for my own tools.

Then I realized: if I'm making it, I might as well make it good enough to share.


The 16 Tiles (and Why Those 16)

Here's the full list:

Tile Use
grass ground, meadows, level base
dirt_path roads, trails, walkways
stone_ground plazas, clearings
water rivers, ponds, lakes
pine_tree conifer forest, background
oak_tree deciduous forest, canopy
bush undergrowth, obstacles
flower_yellow decoration, collectible
rock_grey terrain features
log_fallen props, obstacles
fence_wood boundaries, farm areas
wooden_bridge river crossings
tree_stump props, camp sites
wooden_signpost waypoints, level markers
campfire safe zones, night scenes
mushroom_red decoration, hazard

Why 16?

Not because 16 is a magic number. Because 16 is 4×4 — a perfect square grid that tiles cleanly into a 512×512 sheet. It's the smallest number that covers every element of a 2D forest scene without feeling bloated.

I could have done 8. I could have done 32. But 16 was the sweet spot: enough variety to build a believable scene, small enough to draw in a single sitting.


The Palette Decision (Where I Got It Wrong)

My first attempt used 12 colors.

It looked great. Rich, detailed, beautiful.

Then I zoomed out to 1× and it looked like mud.

The problem: at 32×32, you have 1,024 pixels. If two adjacent colors are too similar, they blend into a single band when scaled down. My "beautiful" 12-color palette had three greens that were too close to each other. They read as one muddy green at 1×.

The fix: I cut the palette to 8 colors and forced every color to earn its place.

Role Color Hex
Background Deep forest green #2d4a2b
Primary Warm oak brown #8b5e3c
Shadow Dark umber #4a2f1a
Highlight Gold leaf #d4a843
Accent 1 Orange foliage #c75b12
Accent 2 Red maple #8b2500
Water Cool blue #4a7c8f
Path Warm tan #b8956a

The key insight: 8 colors isn't "less" — it's more intentional. Each color has a job. No muddy mid-tones. No "close enough" grays.

I tested contrast ratios between adjacent colors using the WCAG luminance formula. Target: 3.0+ for any two colors that touch in a tile.

def luminance(hex_color: str) -> float:
    """Calculate relative luminance (WCAG formula)."""
    hex_color = hex_color.lstrip('#')
    r, g, b = (int(hex_color[i:i+2], 16) / 255 for i in (0, 2, 4))
    r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4
    g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4
    b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4
    return 0.2126 * r + 0.7152 * g + 0.0722 * b

def contrast_ratio(hex1: str, hex2: str) -> float:
    """WCAG contrast ratio between two colors."""
    l1 = luminance(hex1)
    l2 = luminance(hex2)
    lighter = max(l1, l2)
    darker = min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)

# Test the Forest palette
pairs = [
    ("oak brown", "dark umber", "#8b5e3c", "#4a2f1a"),
    ("gold leaf", "orange foliage", "#d4a843", "#c75b12"),
    ("water blue", "path tan", "#4a7c8f", "#b8956a"),
]
for name1, name2, c1, c2 in pairs:
    ratio = contrast_ratio(c1, c2)
    status = "✅" if ratio >= 3.0 else "⚠️"
    print(f"  {status} {name1} vs {name2}: {ratio:.2f}")
Enter fullscreen mode Exit fullscreen mode

Output:

  ✅ oak brown vs dark umber: 3.42
  ⚠️ gold leaf vs orange foliage: 2.87
  ✅ water blue vs path tan: 3.15
Enter fullscreen mode Exit fullscreen mode

That ⚠️ on gold vs orange? That's the pair that looked muddy at 1×. Fix: push the orange warmer (#d46a1a) or the gold lighter (#e8c05a) to widen the gap.


The Technical Details

Tile Size: 32×32 Logical, 128×128 Pixels

I drew at 32×32 (the logical size) and exported at 4× nearest-neighbor (128×128). Why?

  • 32×32 is the sweet spot for 2D scenes: detailed enough for trees and water, small enough to tile quickly.
  • 4× nearest-neighbor keeps the pixels crisp. No anti-aliasing. No blurring. Just clean, chunky pixels that scale perfectly.

File Structure

forest_tile_pack/
├── tiles/
│   ├── grass.png          (128×128, transparent)
│   ├── dirt_path.png      (128×128, transparent)
│   ├── stone_ground.png   (128×128, transparent)
│   ├── water.png          (128×128, transparent)
│   ├── pine_tree.png      (128×128, transparent)
│   ├── oak_tree.png       (128×128, transparent)
│   ├── bush.png           (128×128, transparent)
│   ├── flower_yellow.png  (128×128, transparent)
│   ├── rock_grey.png      (128×128, transparent)
│   ├── log_fallen.png     (128×128, transparent)
│   ├── fence_wood.png     (128×128, transparent)
│   ├── wooden_bridge.png  (128×128, transparent)
│   ├── tree_stump.png     (128×128, transparent)
│   ├── wooden_signpost.png(128×128, transparent)
│   ├── campfire.png       (128×128, transparent)
│   └── mushroom_red.png   (128×128, transparent)
├── forest_sheet_4x4.png   (512×512, 4×4 grid)
├── forest_preview.png     (labeled preview sheet)
└── LICENSE.txt            (CC0 1.0 Universal)
Enter fullscreen mode Exit fullscreen mode

Why Both Individual Tiles AND a Sheet?

Individual tiles are for when you need to:

  • Swap a single tile in a level
  • Use a tile as a sprite (e.g., the campfire as a collectible)
  • Edit a tile in Aseprite or Piskel

The 512×512 sheet is for when you want to:

  • Import directly into Godot's TileMap
  • Slice in Unity's Sprite Editor
  • Load as a texture in Phaser or Pygame

Both are included so you can pick the workflow that fits your engine.


How to Use It (2-Minute Setup)

Godot

  1. Unzip the pack.
  2. Drag forest_sheet_4x4.png into your assets/ folder.
  3. Create a TileMap node.
  4. In the TileSet, click "Add New Texture" and select the sheet.
  5. Set tile size to 128×128.
  6. Start painting.

Unity

  1. Unzip the pack.
  2. Drag forest_sheet_4x4.png into your Assets/ folder.
  3. Select the texture → Sprite mode.
  4. Open Sprite Editor → Slice → Grid By Cell Size.
  5. Set cell width/height to 128.
  6. Slice.
  7. Drag the sprites into your scene.

Phaser

this.load.image('forest', 'assets/forest_sheet_4x4.png');
// Use as a tileSprite or draw individual tiles
const tile = this.add.tileSprite(100, 100, 'forest', 0, 0, 128, 128);
Enter fullscreen mode Exit fullscreen mode

Pygame

import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
forest = pygame.image.load('assets/forest_sheet_4x4.png')
# Blit individual tiles
screen.blit(forest, (0, 0), (0, 0, 128, 128))  # grass
pygame.display.flip()
Enter fullscreen mode Exit fullscreen mode

What I Learned (The Honest Part)

  1. Start with the palette, not the tiles. I wasted an hour drawing tiles before I locked the colors. Next time, I'll pick the 8 colors first, then draw.

  2. Test at 1×, not 4×. I was judging the art at 4× zoom, where everything looks great. The real test is at 1×, where the pixels are tiny and the contrast matters.

  3. CC0 is the best license for a free pack. No attribution required. No "credit appreciated" fine print. Just "use it." That's what indie devs want at 2 AM during a jam.

  4. A labeled preview sheet is worth its weight in gold. I added it after the first download, and it immediately reduced "which tile is which?" questions.

  5. 16 tiles is enough. I kept wanting to add more. A waterfall. A cave. A dragon. But 16 covers the scene. More would just be filler.


Who This Is For

  • Game jam participants who need art fast and don't want to draw it.
  • Indie devs building 2D platformers, top-down RPGs, or nature-themed games.
  • Students learning Godot, Unity, Phaser, or Pygame who need sample tiles.
  • Anyone who wants to test a tilemap tool without drawing their own art.

Not for:

  • AAA production (this is a jam-quality pack, not a AAA asset).
  • Projects that need 64×64 or 128×128 logical tiles (this is 32×32).
  • People who want a full biome with 100+ tiles (this is 16).

Download It

Free. CC0. No attribution required.

👉 Forest Tile Pack on itch.io

Unzip, drop the sheet into your project, and start building.


More Free Assets & Tools

If you're building a 2D game and need more, here's what else I've made:


Final Thought

I didn't build this pack to make money. I built it because I needed it, and I figured other devs might too.

If it saves you 30 minutes in a game jam, or helps you test a tool, or just makes your prototype look less like a wireframe — that's a win.

Go build something.

— LaunchTower


Forest Tile Pack is licensed under CC0 1.0 Universal. Use it in any project, personal or commercial, with no attribution required. Credit appreciated but never obligatory.

Top comments (0)