====== 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'' and ''sourceDensity''). Only if this stage passes, you go next.
* define target - what plant you should put on filtered area (''targetPlant'' and ''targetDensity'').
* 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;
///
/// 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
///
public float sourceDensity;
///
/// [0,1] or KeepSourceDensity
///
public float targetDensity;
public FunctionPointer customFunctionPointer;
public bool customSourceCheck;
public void SetInputMask(NativeArray 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'' and ''sourceDensity'') 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 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);
}