Procedurally Generating a Animal Crossing World - Part 1

Published

Procedurally generating an environment is a very interesting problem and the process very much depends on what kind of result you want.

In this case I’d like to replicate the generation process for the Gamecube version of Animal Crossing, which means:

  • Grid-based map
  • High ground / low ground seperation
  • Water
  • Fully connected plots of lands
  • Constraints on placement of Points of Interest

In this article I’ll be looking at generating a mask for high ground / low ground for a grid, since it is the most important and the most defining feature for these maps.

Animal Crossing Village Map
Map for a Village from Animal Crossing

In AC on Gamecube, the player arrives at the train station which is located at A3. All maps show high grounds on the first 2 rows of the map (A and B), while subsequent rows are either low ground or high ground. It is important to note that a cell can only be high ground IF the cell above it is also high ground.

It seems then that we should start with setting the top most row or two as high ground, and then employ an iterative process that draws at random whether the cell is high ground or not.

The setup is pretty simple: we create an array of int (or bool), fill up the first row, then we go row by row, column by column to draw the type of ground. We’ll add a little bonus: we don’t only check the cell directly above the current cell, but also the one on top-right and top left. This allows us to get a smoother look, with less chances of having a single column being all high ground and seperating the map in two.

How we do this is simple: we track whether the tiles above the current one are high ground or not and increment a score (for example if we’re going to draw for B2 and A1 is high ground, then score += 1, else score stays the same).

This score is an integer that then serves as an index to an array of probability thresholds: if the current cell has a score of 3, then we draw a random number and compare it to the defined threshold for a value of 3 high ground neighbours. If the number is below the threshold, then it is considered to be high ground.

Here’s the result, visualized by instantiating cubes:

generated 5x5 high ground mask
Result for a 5x6 map like AC GC
generated 10x10 high ground mask
Trying a bigger map 10x10

Playing with the draw thresholds can vary the map shape. The code below enforces 1-padding for out of bounds cells and spawns cubes when calling Run. Editor code is below as well.

Mask Generator:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MapGen : MonoBehaviour
{
    public Transform mInstantiationParent;

    [Header("Prefabs")]
    public GameObject mCubePrefab;

    [Header("Map Settings")]
    public Vector2Int mSizeMap;
    public float[] mThresholds;
    public int[,] mMap;



    public void Run() {
        Clear();

        FillMapHeights();

        for (int i = 0; i < mSizeMap.x; i++) {
            for (int j = 0; j < mSizeMap.y; j++) {
                //Instantiate(mCubePrefab, new Vector3((float)i, (float)mMap[i, j], (float)j), Quaternion.identity, mInstantiationParent);
                Instantiate(mCubePrefab, mInstantiationParent, false).transform.localPosition = new Vector3((float)i, (float)mMap[i, j], (float)j);


            }
        }
    }

    private void FillMapHeights() {
        mMap = new int[mSizeMap.x, mSizeMap.y];

        System.Random rand = new System.Random();

        // fill top row with ones
        for (int i = 0; i < mSizeMap.x; i++) {
            mMap[i, 0] = 1;
        }

        for (int j = 1; j < mSizeMap.y; j++) {
            for (int i = 0; i < mSizeMap.x; i++) {
                int score = 0;
                int tempI = i - 1;
                if (tempI < 0) {
                    score += 1;
                } else {
                    score += mMap[tempI, j - 1];
                }

                tempI = i + 1;
                if (tempI >= mSizeMap.x) {
                    score += 1;
                } else {
                    score += mMap[tempI, j - 1];
                }

                if (mMap[i, j - 1] == 0) {
                    score = 0;
                } else {
                  score += 1;
                }

                float drawn = (float)rand.NextDouble();
                if (drawn < mThresholds[score]) {
                    mMap[i, j] = 1;
                }
            }
        }

    }

    public void Clear() {
        int nbChildren = mInstantiationParent.childCount;
        for (int idChild = nbChildren - 1; idChild >= 0; idChild--) {
            DestroyImmediate(mInstantiationParent.GetChild(idChild).gameObject);

        }

    }
}

Editor Code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using UnityEditor;

[CustomEditor(typeof(MapGen))]
public class MapGenEditor : Editor
{
    public override void OnInspectorGUI() {
        base.OnInspectorGUI();

        MapGen obj = (MapGen)target;

        if (GUILayout.Button("Run")) {
            obj.Run();
        }

        if (GUILayout.Button("Clear")) {
            obj.Clear();
        }
    }
}