Devlog 1: Understanding and Creating Procedural Generation

Some ideas randomly filter in and out of my head. One of them that would not go away was the procedural generation of materials.

I wanted to create a way that a computer would consistently generate the same material, with a random name, toughness, sharpness, and some fantasy stats like magic conductivity.

The Beginning

Starting out I had to define all of the variables I wanted to use. These changed over time, but this is what I have currently:

using Random=UnityEngine.Random;
using System.Globalization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;



public class MaterialObject : MonoBehaviour
{
    public int tier;
    public int num;
    public int seed;
    public int hardness;
    public int sharpness;
    public int magicConductivity;
    public int value;
    public float redValue;
    public float blueValue;
    public float greenValue;
    public int loadThis;
    public string materialName;
    public string description;
    public string type;
    public GameObject prefab;
    public Material material;
    public List<Material> materialCrystalList = new List<Material>(); 
    public List<Material> materialOreList = new List<Material>(); 
    public List<GameObject> crystalList = new List<GameObject>();
    public List<GameObject> oreList = new List<GameObject>();

    private string[] prefix = {"bu", "un", "in", "ur", "a", "ab", "ad", "af", "ap", "ante", "anti", "be", "bi", "co", "de", "dia", "dis", "dif", "en", "ex", "fore", "hyper", "hypo", "mal", "mid", "mis", "ob", "non" ,"trans", "ultra", "up"};
    private string[] suffix = {"ium", "otum", "oum", "ity", "or", "ee", "acy", "en", "al", "ate", "ite", "in", "inium", "us"};
    private string[] names = {"ta", "ng", "ra", "an", "ba", "ka", "da", "la", "fe", "be", "te", "ne", "ge"};


    private string[] descriptionHardnessBad = {"is considered to be very soft"};
    private string[] descriptionHardnessOkay = {"is soft"};
    private string[] descriptionHardnessGood = {"is known to be tough"};
    private string[] descriptionHardnessPerfect = {"is considered one of the toughest"};

    private string[] descriptionSharpnessBad = {"is bad for knives"};
    private string[] descriptionSharpnessOkay = {"is known to be brittle"}; 
    private string[] descriptionSharpnessGood = {"can be used to create some great swords"};
    private string[] descriptionSharpnessPerfect = {"rarely if ever shatters"};

    private string[] descriptionMagicConductivityBad = {"is generally disregarded by mages"};
    private string[] descriptionMagicConductivityOkay = {"is commonly used for beginner wands"};
    private string[] descriptionMagicConductivityGood = {"is seeked commonly as magic instructors, as they commonly run out while creating graduation wands"};
    private string[] descriptionMagicConductivityPerfect = {"some of the best mages love using it"};

    private string[] descriptionValueBad = {"saddens people who find it expecting gold"};
    private string[] descriptionValueOkay = {"is commonly found in coal mines"};
    private string[] descriptionValueGood = {"is one of the more common materials used in trade"};
    private string[] descriptionValuePerfect = {"is seeked by people in hopes to become rich"};

    private string[] locations = {"in a cave", "during a lab test", "in a field of flowers", "atop a rare tree", "in the heavens"};

    private string[] people = {"a scientist", "a miner", "a lumberjack", "the prince", "an angel"};

    private string[] sentenceAdditives ={" and also ", " and ", ". This material ", ". This ", "", ""};
Code snippet of all defined variables. Created in Carbon.

I needed a lot of variables that are static. These are all of the lists from prefix to sentenceAddatives.
These allowed me to choose random phrases and mash them together for the description generation. Or mash syllables together for name generation.

From top to bottom, the variables do:

  • tier alters values to become bigger, basically if something a higher-tier material, it will be better
  • num is currently not in use due to its use being moved to another file (I need to grab the information from the other file)
  • seed is the seed used for generation (more on this later)
  • hardness is used to describe the hardness of the object
  • sharpness is used to describe the brittleness of the object
  • magicConductivity is used to describe the magic proficiency of the object
  • value is used to define the worth of the object
  • redValue, greenValue, and blueValue are the values from 0.0f to 1.0 on the RGB scale the material falls under
  • loadThis decides whether or not on load if the class should immediately generate a material
  • materialName is the name of the material generated with the prefix suffix and names lists
  • description is the description generated with the lists that start with the description and location
  • type decides whether or not the material is a crystal or metal
  • prefab is the generated prefab for the material
  • material is the generated material for the object
  • materialCrystalList and materialOreList are lists that define what materials (the one used for 3d objects) can be used for the respective type
  • crystalList and oreList are used to define what prefabs can be used for each type
  • people and sentence additives are used in the generation of the description

Start()

The start function is very short so I will go over it briefly.


    // Start is called before the first frame update
    void Start()
    {
      // if there is a material to load it will load it
      // otherwise it will generate a new one. 
       if (loadThis == -1)
       {
              generate();
       }
       else 
       {
       load();
       }

       //Button btn = generateButton.GetComponent<Button>();
       //btn.onClick.AddListener(generate);
    }
image created with carbon

The start function tries to use generate() if loadThis is equal to -1 else, it will use load.

There was another function on Start() and that was for testing. I had a button that would just let me generate materials as many times as I wanted.

Now you might be curious to see generate()

generate()

generate is the main function where everything goes down. This calls all other functions in the class, so I will go over them in the order that generate() calls them.

public void generate()
    {
      if(seed == -1)
      {
       seed = Random.Range(0,213123232);  
      }
       Random.InitState(seed);
       int randomNum;
       hardness = tier*Random.Range(1,20);
       sharpness = tier*Random.Range(1,5);
       value = tier*Random.Range(49,101);
       magicConductivity = tier*Random.Range(0,12);

       materialName = nameGenerator();
       sentenceAdditives[4] = $". {materialName} ";
       sentenceAdditives[5] = $", {materialName} ";
       description = descriptionGenerator();

       if(Random.Range(0,4)>1)
       {
              type = "crystal";
       }
       else
       {
              type = "metal";
       }

       if(type == "metal")
       {
            randomNum = Random.Range(1, oreList.Count+1);
            prefab = oreList[randomNum-1];
            randomNum = Random.Range(1, materialOreList.Count+1);
            material = materialOreList[randomNum-1];
       }
       else
       {
            randomNum = Random.Range(1, crystalList.Count+1);
            prefab = crystalList[randomNum-1];
            randomNum = Random.Range(1, materialCrystalList.Count+1);
            material = materialCrystalList[randomNum-1];
       }

       redValue = Random.Range(0.3f, 1f);
       Random.InitState(seed+1);
       blueValue = Random.Range(0.3f, 1f);
       Random.InitState(seed+2);
       greenValue = Random.Range(0.3f, 1f);
       Random.InitState(seed);
       loadThis = -2;
       
       save();

       load();

       
    }
created with carbon

As I mentioned before, generate() mostly calls other functions to do its work, but I will break down the things it does without the help of other functions.

First, if a seed isn’t given, generate creates a random seed.

What is a seed?

Seeds allow one to create seeded generation. Seeded generation will have the same results every time with the same seed. This means that someone can send someone else the seed to something they find cool, or developers can whitelist only seeds that produce good content.

Unity allows seeded generation by calling Random.InitState(seed); If you are generating multiple randoms (for example if you are generating two random numbers between 0 and 1) you need to consistently change the seed if you want to generate a different number.

generate() continued

After initializing the seed, generate initializes int randomNum, which is used in a few random generations in generate(); It then generates the hardness, sharpness, value, and magic conductivity.

Then generate calls up the name generator and description generator to help it create the name and description, both of these functions are void functions so no inputs are needed.

generate() then decides if the material is a crystal or a metal, this is important for the rest of the generation’s choices, so it does it now. After that, it does the material and prefab generation which is chosen from the list that the user has to input.

The RGB values are chosen in the way they are so that they do not produce the same number. If the only colors chosen are the same (0.4, 0.4, 0.4) they are all just going to be grey with different brightness.

After this loadThis is set to -2 so that the start() function never happens to run again (this is mostly a just in case or a save load check.) Then the generate() function runs the save() and load() functions.

nameGenerator()

The name generator uses the three lists defined at the top and smashes their syllables together. It then returns the combined string. Here is the simple code that does that:

    // generates names based off 3 lists. 
    string nameGenerator()
    {
        string nameTotal = ""; 
        int randomNum = Random.Range(0, 11);
        if(randomNum > 1)
        {
            Random.InitState(seed+1);
            randomNum = Random.Range(1, prefix.Length+1);
            nameTotal += prefix[randomNum-1];
        }

        Random.InitState(seed+2);
        randomNum = Random.Range(1, names.Length+1);
        nameTotal += names[randomNum-1];

        randomNum = Random.Range(0, 20);
        Random.InitState(seed+2);


        if(randomNum > 1)
        {
            Random.InitState(seed+3);
            randomNum = Random.Range(1, suffix.Length+1);
            nameTotal += suffix[randomNum-1];     
        }

        Random.InitState(seed);
        return nameTotal;
Created using Carbon

This code first decides whether or not it’s going to use a suffix or prefix, and if it does it generates it. It will always use a syllable from the name list.
It then returns the combination of the syllables it decided to generate.

descriptionGenerator()

The description generator is a lot more complex than the name generator. This is because the order of the phrases is random, and there needs to be a conjunction connecting them.

   string descriptionGenerator()
    {
        bool hardnessTest = false;
        bool sharpnessTest = false;
        bool magicTest = false;
        bool locationTest = false;
        bool valueTest = false;
        bool unTest = false;

        int randomNum = 0;

        string descriptionTotal = "";
        descriptionTotal += materialName;
        descriptionTotal += " ";
        while(((!hardnessTest || !sharpnessTest) || (((!magicTest || !locationTest)) || ((!valueTest || !unTest)))))
        {

            randomNum = Random.Range(0, 7);

            if(randomNum == 1 && !hardnessTest)
            {
                if(hardness/tier > 17)
                {
                     randomNum = Random.Range(1, descriptionHardnessPerfect.Length);
                     descriptionTotal += descriptionHardnessPerfect[randomNum-1];
                }
                else if(hardness/tier > 12)
                {
                     randomNum = Random.Range(1, descriptionHardnessGood.Length);
                     descriptionTotal += descriptionHardnessGood[randomNum-1];
                }
                else if(hardness/tier > 6)
                {
                     randomNum = Random.Range(1, descriptionHardnessOkay.Length);
                     descriptionTotal += descriptionHardnessOkay[randomNum-1];
                }
                else
                {
                     randomNum = Random.Range(1, descriptionHardnessBad.Length);
                     descriptionTotal += descriptionHardnessBad[randomNum-1];
                }
                hardnessTest = true;

                if (!((hardnessTest && sharpnessTest) && ((magicTest && locationTest) && (valueTest && unTest))))
                {
                randomNum = Random.Range(1, sentenceAdditives.Length+1);

                descriptionTotal += sentenceAdditives[randomNum-1];
                }

            }
            else if(randomNum == 2 && !sharpnessTest)
            {
                if(sharpness/tier > 4)
                {
                     randomNum = Random.Range(1, descriptionSharpnessPerfect.Length);
                     descriptionTotal += descriptionSharpnessPerfect[randomNum-1];
                }
                else if(sharpness/tier > 3)
                {
                     randomNum = Random.Range(1, descriptionSharpnessGood.Length);
                     descriptionTotal += descriptionSharpnessGood[randomNum-1];
                }
                else if(sharpness/tier > 2)
                {
                     randomNum = Random.Range(1, descriptionSharpnessOkay.Length);
                     descriptionTotal += descriptionSharpnessOkay[randomNum-1];
                }
                else
                {
                     randomNum = Random.Range(1, descriptionSharpnessBad.Length);
                     descriptionTotal += descriptionSharpnessBad[randomNum-1];
                }

                sharpnessTest = true;

                if (!((hardnessTest && sharpnessTest) && ((magicTest && locationTest) && (valueTest && unTest))))
                {
                randomNum = Random.Range(1, sentenceAdditives.Length+1);
                descriptionTotal += sentenceAdditives[randomNum-1];
                }

            }
            else if(randomNum == 3 && !magicTest)
            {
                if(magicConductivity/tier > 10)
                {
                     randomNum = Random.Range(1, descriptionMagicConductivityPerfect.Length);
                     descriptionTotal += descriptionMagicConductivityPerfect[randomNum-1];
                }
                else if(magicConductivity/tier > 7)
                {
                     randomNum = Random.Range(1, descriptionMagicConductivityGood.Length);
                     descriptionTotal += descriptionMagicConductivityGood[randomNum-1];
                }
                else if(magicConductivity/tier > 4)
                {
                     randomNum = Random.Range(1, descriptionMagicConductivityOkay.Length);
                     descriptionTotal += descriptionMagicConductivityOkay[randomNum-1];
                }
                else
                {
                     randomNum = Random.Range(1, descriptionMagicConductivityBad.Length);
                     descriptionTotal += descriptionMagicConductivityBad[randomNum-1];
                }
                magicTest = true;
                if (!((hardnessTest && sharpnessTest) && ((magicTest && locationTest) && (valueTest && unTest))))
                {
                randomNum = Random.Range(1, sentenceAdditives.Length+1);
                descriptionTotal += sentenceAdditives[randomNum-1];
                }

            }
            else if(randomNum == 4 && !locationTest)
            {
                string location = "is rumored to be originally found ";
                
                randomNum = Random.Range(1, locations.Length);
                location += locations[randomNum-1];
                
                location += " by ";

                randomNum = Random.Range(1, people.Length);
                location += people[randomNum-1];

                descriptionTotal += location;
                locationTest = true;
                if (!((hardnessTest && sharpnessTest) && ((magicTest && locationTest) && (valueTest && unTest))))
                {
                randomNum = Random.Range(1, sentenceAdditives.Length+1);
                descriptionTotal += sentenceAdditives[randomNum-1];
                }
            }
            else if(randomNum == 5 && !valueTest)
            {
                if(value/tier > 95)
                {
                     randomNum = Random.Range(1, descriptionValuePerfect.Length);
                     descriptionTotal += descriptionValuePerfect[randomNum-1];
                }
                else if(value/tier > 75)
                {
                     randomNum = Random.Range(1, descriptionValueGood.Length);
                     descriptionTotal += descriptionValueGood[randomNum-1];
                }
                else if(value/tier > 65)
                {
                     randomNum = Random.Range(1, descriptionValueOkay.Length);
                     descriptionTotal += descriptionValueOkay[randomNum-1];
                }
                else
                {
                     randomNum = Random.Range(1, descriptionValueBad.Length);
                     descriptionTotal += descriptionValueBad[randomNum-1];
                }
                valueTest = true;
                if (!((hardnessTest && sharpnessTest) && ((magicTest && locationTest) && (valueTest && unTest))))
                {
                randomNum = Random.Range(1, sentenceAdditives.Length+1);
                descriptionTotal += sentenceAdditives[randomNum-1];
                }
            }
            else if(randomNum == 6 && !unTest)
            {
                descriptionTotal += "bungus";
                unTest = true;
                if (!((hardnessTest && sharpnessTest) && ((magicTest && locationTest) && (valueTest && unTest))))
                {
                randomNum = Random.Range(1, sentenceAdditives.Length+1);
                descriptionTotal += sentenceAdditives[randomNum-1];
                }
            }
        }
        return descriptionTotal;


    }
Image created using Carbon

Let’s start from the top as this can get a bit confusing (without the comments that I forgot to add).

descriptionGenerator() has 6 variables it needs to keep a track of. These make sure that it never duplicates any part of the description and that it never misses any part.

Whenever any of the parts are successfully generated, its corresponding variable is set to true so that the code skips that generation next time.

Inside each generation a set of if statements decide whether or not the corresponding stat is good or bad for the given tier. It is done by dividing the generated value by the tier. This works because tiers scale linearly. It then compares with numbers arbitrarily chosen by me that are close to the minimum and maximum possible generation. Then if it is not the last thing to be generated in the description, it will add a sentence conjunction.

The location works a bit differently from the others by creating its own nonsense information by combining a location and person to create a possible origin for the material.


The unTest at the bottom is temporary for a possible 6th stat to keep track of.

save()

The save function took me a bit to figure out and is most likely to change in the future. It uses Unity’s built-in JSON manipulation functions to create JSON and then load it later.

    //Saves material information into a Json File
    void save()
    {
     
     string tempStringSave = ""+tier+num+"";
     BinaryFormatter formatter = new BinaryFormatter();
     string path = Application.persistentDataPath + $"/{tempStringSave}.lu";
     FileStream stream = new FileStream(path, FileMode.Create);


     string saveTrue = JsonUtility.ToJson(this, true);

     formatter.Serialize(stream, saveTrue);
     stream.Close();

    }
Created using Carbon

Here is where num is used, but it is currently set to NULL, so I am surprised it works.
I use the BinaryFormatter to take the JSON created with the JSON Utility and serialize it. This makes it so that no player can cheat by manipulating the material’s value.

load()

load() is slightly more complex, as it also creates the prefab in the world.

Created using Carbon

load() starts off by doing a similar thing to save, but it has to check if the file exists. If the file does not, an error will be thrown out into the logs. The important difference is the initiation of the prefab.

The prefab is instantiated after the information is loaded from FromJsonOverwrite (which takes the JSON information and overwrites the object’s information). It then takes the material and color information and writes that onto the prefab. Currently, other information is not added to the material.

 

What is update() doing??

After looking at my code, some people may be wondering what the heck update() is doing.

    // Update is called once per frame
    void Update()
    {
    /*
      if(materialName == "bungus")
         {
              Debug.Log(seed);
      }
       else
       {
             generate();
      }
      */
    }
Created using Carbon

The short answer is that update() does nothing. The comments were for a request by a friend for me to search out a specific name that my name generator could make.

Understanding my gameManager()

My gameManager is pretty simple. It just counts up until reaching a count of 5, then resetting back at 0, but with the tier one higher. It then runs generate() with the updated information.

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

public class gameManager : MonoBehaviour
{
    public int counter;
    public int tier;
    public GameObject Manager;
    public Button generateButton;

    // Consistantly counts up and changes the seed based off tier and count.
    void tierCounter()
    {
        if(counter == 0)
        {
            Manager.GetComponent<MaterialObject>().tier = tier;

            Manager.GetComponent<MaterialObject>().seed+=counter*tier;

            Manager.GetComponent<MaterialObject>().generate();
            
        }
        else
        {

        Manager.GetComponent<MaterialObject>().seed+=counter*tier;

        Manager.GetComponent<MaterialObject>().generate();

        }
        
        if(counter == 4)
           {
                  counter = 0;
                  tier++;
           }
        else
           {
                  counter++;
           }
    }

    void Start()
    {
        tier = 1;
        counter = 0;

       Button btn = generateButton.GetComponent<Button>();
       btn.onClick.AddListener(tierCounter);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Created with carbon.

This allows the generator to create 5 of the same tier and then move on, allowing the player to savor each tier a bit. I connected a button to tierCounter() for now, but it can easily be removed.

What to take away

Overall what I need to take away and do after this is add more comments. There is a few pieces of code that do nothing that I need to remove. I will make the num in MaterialObject.cs equal the counter in gameManager.cs

If you haven’t tried experimenting with procedural generation, it is fun and an interesting problem.


https://github.com/Thundertides/procedualGenerationUsingUnity
0 forks.
0 stars.
0 open issues.

Recent commits:

 

The code will be available below to check out, and you can always do whatever with it using GitHub’s features, including offering pull requests.


No Comments

Send Comment Edit Comment


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
Previous
Next