Scripting API
All properties available in inspector can also be modified runtime from code via exposed getters and setters.
Except few things:
- Plant textures in layer definition cannot be assigned runtime.
- Each time you modify plant in layer definition, you will have to call
InvalidateBuffers()
function on definition object. - Each time you modify LOD properties in foliage surface, you will have to call
InvalidateLOD()
function on surface object.
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:
- define filter - what plant you should search for (
sourcePlant
andsourceDensity
). Only if this stage passes, you go next. - define target - what plant you should put on filtered area (
targetPlant
andtargetDensity
). - mode - filter and target may work differently according to used mode
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:
true
- filter (sourcePlant
andsourceDensity
) is used before passing data to your function.false
- filter is not used and every single point of area will be processed by your function.
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 FoliageLayerUpdateParams
struct 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); }