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:
- Stealing a tileset from somewhere (and feeling guilty about attribution), or
- Drawing one from scratch (and spending 3 hours on something that should take 20 minutes), or
- 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}")
Output:
✅ oak brown vs dark umber: 3.42
⚠️ gold leaf vs orange foliage: 2.87
✅ water blue vs path tan: 3.15
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)
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
- Unzip the pack.
- Drag
forest_sheet_4x4.pnginto yourassets/folder. - Create a
TileMapnode. - In the TileSet, click "Add New Texture" and select the sheet.
- Set tile size to 128×128.
- Start painting.
Unity
- Unzip the pack.
- Drag
forest_sheet_4x4.pnginto yourAssets/folder. - Select the texture → Sprite mode.
- Open Sprite Editor → Slice → Grid By Cell Size.
- Set cell width/height to 128.
- Slice.
- 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);
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()
What I Learned (The Honest Part)
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.
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.
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.
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.
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.
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:
Pixel Palette Extractor — extract a palette from any image in your browser
👉 https://pennypenguinapp-beep.github.io/launchtower-palette-extractor/Aseprite Palette to CSS — convert your Aseprite palette to CSS variables
👉 https://pennypenguinapp-beep.github.io/launchtower-palette-css/Dither Pattern Generator — generate dithering patterns for smooth gradients
👉 https://pennypenguinapp-beep.github.io/launchtower-dither-gen/Tilemap Editor Lite — edit tilemaps in your browser
👉 https://pennypenguinapp-beep.github.io/launchtower-tilemap-editor/Full catalog of 30+ free tools, games, and asset packs
👉 https://pennypenguinapp-beep.github.io/
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)