Table of Contents

Scripting API

All properties available in inspector can also be modified runtime from code via exposed getters and setters.
Except few things:


Foliage Job Area

Before doing modification on surface, you have to define area that you will work on by creating FoliageJobArea.
Remember that larger area means more expensive work, so try to always use smallest possible area.
You create area by calling appropriate function on FoliageSurface object:

/*
    All areas are in 2D space (XZ plane).
    Vector3 functions are helpers that allow using transform.position directly.
    They simply cut Vector3 into Vecto2 by using XZ values.
*/
 
// create circular area
public FoliageJobArea CreateJobArea(Vector2 center, float radius);
public FoliageJobArea CreateJobArea(Vector3 center, float radius);
 
// create rectangular area
public FoliageJobArea CreateJobArea(Vector2 position, Vector2 size);
public FoliageJobArea CreateJobArea(Vector3 position, Vector3 size);
 
// create any polygon area by defining 4 points in clockwise order
public FoliageJobArea CreateJobArea(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4);
public FoliageJobArea CreateJobArea(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4);
 
// return rectangular area that covers entire surface
public FoliageJobArea CreateJobAreaFull();

Once you create area, you can keep it and reuse between tasks - see below.


Surface Modification

Task Concept

All operations requested on surface return FoliageAreaTask object, that implements IDisposable interface and allows operation to be either syncrhonous or asyncrhonous.
The recommended way is to treat all tasks as asynchronous, even if some task is internally always syncrhonous at this moment - things may change in future.
It is also your responsibility to Dispose() task as soon as you finish working on it.

public abstract class FoliageAreaTask : IDisposable
{
    public FoliageSurface Surface { get; }
    public FoliageJobArea Area { get; }
    public bool Disposed { get; }
 
    public void Dispose();
 
    public abstract bool IsCompleted();
    public abstract void Update();
    public abstract void InstantComplete();
}
 
/////////////////////////////////
 
/*
    Method #1 - synchronous
    Just call InstantComplete()
*/
 
FoliageAreaTask task;
task.InstantComplete();
task.Dispose(); // remember to dispose
 
/////////////////////////////////
 
/*
    Method #2 - asynchronous
    Keep calling Update() until IsCompleted() return false
    Ideally, do it in coroutine with yield return null
*/
 
FoliageAreaTask task;
while (!task.IsCompleted())
{
    yield return null; // skip one frame, if in coroutine
    task.Update();
}
task.Dispose(); // remember to dispose

Heightmap Bake

You can rebake heightmap at any area in runtime.
Call appropriate function on FoliageSurface object.

public class FoliageSurface : MonoBehaviour
{
    public FoliageHeightmapUpdateTask BakeHeightmap(FoliageJobArea area);
}
FoliageSurface surface;
 
FoliageJobArea area = surface.CreateJobArea(...);
FoliageHeightmapUpdateTask task = surface.BakeHeightmap(area);
 
while (!task.IsCompleted())
    task.Update();
 
task.Dispose();

Foliage Layer Update

To actually modify foliage on surface (add or remove plants), together with area you also have to prepare FoliageLayerUpdateParams struct and then call appropriate function on FoliageSurface object.
By creating parameters struct, you basically configure few things:

See examples below:

public enum FoliageLayerUpdateMode
{
    AdditiveBlend,
    Override,
    Remove,
    Custom
}
 
public struct FoliageLayerUpdateParams
{
    public const int NullPlant;
    public const int AnyPlant;
    public const float KeepSourceDensity;
 
    public delegate void CustomUpdateDelegate(ref int plant, ref float density);
 
    public FoliageLayerUpdateMode mode;
 
    public int sourcePlant;
    public int targetPlant;
 
    /// <summary>
    /// 0 mean all density
    /// (0,1] mean only density that are equal or above absolute value of sourceDensity
    /// [-1,0) mean only density that are equal or below absolute value of sourceDensity
    /// </summary>
    public float sourceDensity;
 
    /// <summary>
    /// [0,1] or KeepSourceDensity
    /// </summary>
    public float targetDensity;
 
    public FunctionPointer<CustomUpdateDelegate> customFunctionPointer;
    public bool customSourceCheck;
 
    public void SetInputMask(NativeArray<float> mask);
    public void GenerateOutputMask();
 
    // validate
    public bool Validate(ref FoliageJobArea area, out string errorMessage);
 
    public static FoliageLayerUpdateParams CreateDefault();
}
 
public class FoliageSurface : MonoBehaviour
{
    public FoliageLayerUpdateTask ModifyFoliage(int layer, FoliageJobArea area, FoliageLayerUpdateParams parameters);
}
// create initial struct, filled with default values - important
FoliageLayerUpdateParams parameters = FoliageLayerUpdateParams.CreateDefault();
 
/////////////////////////////////
 
// example 1 - simple paint with plant X and density 0.5f
parameters.mode = FoliageLayerUpdateMode.Override
parameters.sourcePlant = FoliageLayerUpdateParams.AnyPlant; // variant 1 - use entire area, no matter what
parameters.sourcePlant = FoliageLayerUpdateParams.NullPlant; // variant 2 - paint only where there is no plant
parameters.sourceDensity = 0; // filter any density
parameters.targetPlant = ... // plant X index
parameters.targetDensity = 0.5f;
 
// example 2 - replace plant A with plant B, but don't change density
parameters.mode = FoliageLayerUpdateMode.Override
parameters.sourcePlant = ... // plant A index
parameters.sourceDensity = 0; // filter any density
parameters.targetPlant = ... // plant B index
parameters.targetDensity = FoliageLayerUpdateParams.KeepSourceDensity;
 
// example 3 - AdditiveBlend with plant X at density 0.5f
// this mode works in two steps:
// 1 - if source plant is different, then it will replace with target depending on density difference and some randomness
//     (e.g. if source is 1.0 and target 0.5, then approximately only half of area will be replace)
// 2 - set density to target density only if source density is lower
parameters.mode = FoliageLayerUpdateMode.AdditiveBlend
parameters.sourcePlant = FoliageLayerUpdateParams.AnyPlant;
parameters.sourceDensity = 0; // filter any density
parameters.targetPlant = ... // plant X index
parameters.targetDensity = 0.5f;
 
// example 4 - remove all plants where density is lower than 0.2
parameters.mode = FoliageLayerUpdateMode.Remove
parameters.sourcePlant = FoliageLayerUpdateParams.AnyPlant;
parameters.sourceDensity = -0.2f;
// targetPlant and targetDensity is not used in Remove mode
 
/////////////////////////////////
 
// final step - run task
FoliageSurface surface;
int layerIndex;
 
FoliageJobArea area = surface.CreateJobArea(...);
FoliageAreaTask task = surface.ModifyFoliage(layerIndex, area, surface);
 
while (!task.IsCompleted())
    task.Update();
 
task.Dispose();

Layer Update - Custom Mode

If you are not happy with built-in modes, you can create custom function that will process (and optionally - modify) plant and density at every area point.
For this, you will have to implement CustomUpdateDelegate delegate that can be compiled with Burst Compiler so you will be able to assign pointer to this function to customFunctionPointer field.

Visit official Burst Compiler documentation for details.

In Custom mode, you can also set customSourceCheck field:

Layer Update - Mask

Sometimes you may want to gather details about processed area e.g. to check if any plant was actually changed in order to increase player score.
For this you can use mask.
Mask contains information about amount of plant changes for every point.
Such mask can be also passed to FoliageLayerUpdateParams struct as another input filter - so you can chain update tasks so second task will run only on area actually processed by previous task.

To generate mask, call GenerateOutputMask() on FoliageLayerUpdateParams struct before executing task.
After you complete task, you can call GetOutputResult() on task object to get result mask with summary.
Such mask can be later used for secondary task, by calling SetInputMask(mask) on FoliageLayerUpdateParamsstruct before executing task.

NOTE 1: Output mask can be gathered and used ONLY if you haven't disposed task yet - if you want to use multiple tasks, nest them appropriately.
NOTE 2: When using output mask as input for next task - you have to use exactly same area.

public class FoliageLayerUpdateTask : FoliageAreaTask
{
    public class OutputResult
    {
        public readonly NativeArray<float> mask; // pass it to next task as input
        public readonly int affectedPixels;
        public readonly float totalAffection;
    }
 
    /*
        This function can only be called if you have enabled output mask generation
        and if task is not disposed yet.
        Otherwise exception will be thrown.
    */
    public OutputResult GetOutputResult();
}
// combine harvester example
 
int foliageWheat; // wheat plant id
int FoliageStubble; // stubble plant id
 
var area = foliage.CreateJobArea(...); // cutter area
 
var param = FoliageLayerUpdateParams.CreateDefault();
param.mode = FoliageLayerUpdateMode.Override;
param.sourcePlant = foliageWheat;
param.targetPlant = FoliageStubble;
 
// "using" statement will call Dispose() for you automatically
using (var task = foliage.ModifyFoliage(foliageLayer, area, param))
{
    task.InstantComplete();
 
    var result = task.GetOutputResult();
    if (result.affectedPixels > 0)
    {
        // wheat has been harvested
        // queue straw creation
 
        // ...
 
        var collectedWheat = result.totalAffection;
        // calculate collected grain and add it to combine tank
    }
}

Save/Load Foliage Storage

FoliageStorage class includes utility functions to serialize and deserialize storage, so you can include it in your savegame system.
Keep in mind that this process is slow and you should either do it during loading screen or rewrite it on your own to make it as asynchronous operation.

public class FoliageSurface : MonoBehaviour
{
    public FoliageStorage CurrentStorage { get; }
 
    /*
        If instantiate is true, surface will make local copy of given storage and also automatically destroy it.
        If false, then it will use given storage directly and it will be your responsibility to destroy it later.
    */
    public void LoadStorage(FoliageStorage storage, bool instantiate);
}
 
public class FoliageStorage : ScriptableObject
{
    public static string SerializeString(FoliageStorage storage);
    public static byte[] SerializeBytes(FoliageStorage storage);
 
    public static FoliageStorage Deserialize(string data);
    public static FoliageStorage Deserialize(byte[] data);
}