Dev Blog #30 | Foundry Fridays: Everything in Moderation
Author: cheeriodude,
published 1 year ago,
[h1]A First look at the Foundry Mod Kit[/h1]
Today I wanted to talk about one of the ways that you’ll be able to create mods in Foundry. This talk will be mostly focused on how you can script new components and systems to enhance existing buildings with new functionality.
More specifically, we’ll be looking at taking one of my favorites: The Logistic Container
[img]https://clan.cloudflare.steamstatic.com/images//38913947/edfc49e992c82e1044879e7efea9fc1784469602.png[/img]
And turning it into: The Planter Box
[img]https://clan.cloudflare.steamstatic.com/images//38913947/e7c17e231cec6ad31faf92c77450dbce25d58353.png[/img]
[h1]Modest Beginnings[/h1]
To make this new planter building, I don’t want to start from scratch. So the first thing I’m going to do is open the Foundry Asset Library and create a clone of the Logistic Container I:
[img]https://clan.cloudflare.steamstatic.com/images//38913947/92aebef3080c13f4eac31d39a586358fcd03bc6c.png[/img]
This sets me up with a copy of all the files I need and after buying a cool set of plants from the asset store, I can quickly setup my new planter box prefab:
[img]https://clan.cloudflare.steamstatic.com/images//38913947/eaaae9751ed0ef0f1b35fe810effd6c817a2b714.png[/img]
It’s probably worth mentioning at this point that I’m a programmer by trade so if you’re looking at this planter box and thinking “this doesn’t look too good”, you would be correct. That’s ok though because I wanted to make sure I went through all the steps of creating a mod myself, similar to how awesome people like you will be doing it at home.
Now on to the scripting!
[h1]One Component at a Time[/h1]
The first thing I want to do is create some components to store all the data I’m going to need. If you’re familiar with Unity/MonoBehaviours, these are quite similar:
[expand type=details]
[code]
// A component that represents a planter
// box which can have a plant growing in it
public class PlanterBox : IModComponent
{
// The plant growing in this planter box
[Save] public ulong plantId;
}
// A component that represents a plant that
// grows in a planter box
public class Plant : IModComponent
{
// How many sim ticks till the plant is fully grown
public int totalGrowthTimeInTicks = 100;
// What item template spawns this plant
public string itemTemplateId;
// How long has this plant been growing for
[Save] public int currentGrowthInTicks;
}
[/code]
[/expand]
Once you've created the script for your components, we can go ahead and add them to our Planter Box prefab:
[img]https://clan.cloudflare.steamstatic.com/images//38913947/f2ac985ce0c5a531e5ccb20c7e001cf014149e08.png[/img]
Now on to the systems!
[h1] Systematic Behaviour [/h1]
Systems are how you can add new behaviour to Foundry. First let's start with our PlantSimSystem. The only thing this system does is slowly grow our plants over time:
[expand type=details]
[code]
// A system which handles the growth of plants
//
// [AddSystemToGameSimulation] automatically starts
// this system when a game is started
[AddSystemToGameSimulation]
public class PlantSimSystem : SystemManager.System
{
// [LockstepTickRate(60)] means that this system is updated every
// 60 simulation ticks. It's an easy way to reduce the cost of your
// update.
[LockstepTickRate(60)]
public override void pstLockstep_tickUpdate(ulong tickId, DesyncHasher hasher)
{
// Here we iterate over all the Plant components
foreach(var kvp in Mods.Components.getAll())
{
// First we get the plant component
var plant = kvp.Value;
// Then we check to see if the plant is fully grown,
// and if so, then we skip the rest of the update.
bool isPlantFullyGrown = plant.currentGrowthInTicks >= plant.totalGrowthTimeInTicks;
if (isPlantFullyGrown) continue;
// And if we finally get to this point,
// then we increase how many ticks this
// plant has been growing for.
plant.currentGrowthInTicks++;
}
}
}
[/code]
[/expand]
Next let's handle the PlanterBoxSimSystem. For this there are two things we need to take care of. We need to gather a mapping of item templates to plants so that we know what plant to spawn when we place an item into the logistic container and then we have to do the fun bit which is actually spawning a plant once we've found a valid seed in store:
[expand type=details]
[code]
// A system which handles the spawning of plants
// when the appropriate item has been placed in
// storage.
[AddSystemToGameSimulation]
public class PlanterBoxSimSystem : SystemManager.System
{
// onAddedToManager() is called once when the system is first created
// and added to the SystemManager
public override void onAddedToManager()
{
// On startup we create a map of ItemTemplates->Plant Prototypes
createItemTemplateToPlantPrototypeMap();
}
// create the ItemTemplate->Plant Prototype Map
void createItemTemplateToPlantPrototypeMap()
{
// Iterate all plant prototypes.
//
// A prototype is similar to a Unity GameObject.
// It has a list of components and you can use it
// to spawn entities in the simulation.
foreach (var plantPrototype in Mods.Prototypes.getAllPrototypesWithComponent().Values)
{
// get the Plant component
var plant = plantPrototype.getComponent();
// get the associated ItemTemplate
var itemTemplate = AssetManager.getAsset(plant.itemTemplateId);
// Map the item template to the specified plant prototype
plantPrototypes[itemTemplate] = plantPrototype;
}
}
// Update the simulation
//
// [LockstepTickRate(60)] tells the system manager to only update this system
// every 60 ticks.
[LockstepTickRate(60)]
public override void pstLockstep_tickUpdate(ulong tickId, DesyncHasher hasher)
{
// Iterate all PlanterBox components
foreach(var kvp in Mods.Components.getAll())
{
// Get the planter box component
var planterBox = kvp.Value;
// If the planter box already has a valid plant, then
// skip the rest of the update
if (EntityManager.isValid(planterBox.plantId)) continue;
// Get the planter box id
var planterBoxId = kvp.Key;
// Get a handle to the planter box inventory.
//
// Handles are used to interface with the native Foundry
// entities that exist in the native C++ plugin.
var planterBoxInventory = new StorageEntityHandle(planterBoxId).inventory;
// Iterate all items in the planter box inventory
foreach (var item in planterBoxInventory.items)
{
// If the item is not a plant, then skip it
if (!plantPrototypes.TryGetValue(item.template, out var plantPrototype)) continue;
// Remove the item from the inventory
planterBoxInventory.tryRemoveItem(item.template.id);
// Spawn a new plant from it's prototype
var plantId = Mods.Entities.spawn(plantPrototype.id);
// Have the planter box keep track of it's newly spawned plant
planterBox.plantId = plantId;
// Trigger an event that will be handled by the PlanterBoxRenderSystem
// or any other mods who want to know when a plant has been planted.
trigger(new OnEntityPlanted
{
plantId = plantId,
planterBoxId = planterBoxId
});
// If we've found a valid plant then we
// can exit out of this loop
break;
}
}
}
Dictionary plantPrototypes = new Dictionary();
}
[/code]
[/expand]
Finally there's one last thing we're going to do which is to make a PlanterBoxRenderSystem which will spawn the plants once they get planted. For this we need to handle when the planter box is streamed in/out and when a new plant is planted. We're also going to scale up the plants as they grow.
I know this has already been quite technical up till this point and though I can't promise you that this is going to be any less technical, I can promise you that this is the last piece of code.
Promise.
[expand type=details]
[code]
// A system which handles the rendering of plants when they're planted in a planter box
// [AddSystemToGameClient] automatically adds the system to the game except when
// the game is running in dedicated server mode.
[AddSystemToGameClient]
public class PlanterBoxRenderSystem : SystemManager.System
{
// Handle when the planter box GameObject is streamed in
[EventHandler]
public void handle(OnEntityStreamedIn evt)
{
// Keep track of the newly streamed in GameObject
streamedInPlanterBoxes[evt.entityId] = evt.cmp;
// Check to see if there's a plant we should be
// visualizing
trySpawnPlantVisualizer(evt.entityId);
}
// Handle when the planter box gets streamed out
[EventHandler]
public void handle(OnEntityStreamedOut evt)
{
// Remove the streamed out planter box.
streamedInPlanterBoxes.Remove(evt.entityId);
// Get the planter box component
var planterBox = Mods.Components.get(evt.entityId);
// Check to see if there's a plant GameObject that needs to be destroyed
if(plantVisualizers.TryGetValue(planterBox.plantId, out var plantVisualizer))
{
// If there is, then destroy it
GameObject.Destroy(plantVisualizer.gameObject);
// And remove the reference we had to it
plantVisualizers.Remove(evt.entityId);
}
}
// Handle the case where we've planted a new plant
[EventHandler]
public void handle(OnEntityPlanted evt)
{
// Spawn the plant visualizer if necessary
trySpawnPlantVisualizer(evt.planterBoxId);
}
// This handles spawning the plant GameObject.
// It will only be spawned if there's a valid plant
// and if the planter box is streamed in.
void trySpawnPlantVisualizer(ulong planterBoxId)
{
// If the planter box isn't streamed in, we can just skip the rest of this function
if (!streamedInPlanterBoxes.TryGetValue(planterBoxId, out var planterBoxGO)) return;
// Get the planter box component
var planterBox = Mods.Components.get(planterBoxId);
// If it does not have a plant then just skip the rest.
if (!EntityManager.isValid(planterBox.plantId)) return;
// Get the plant prefab to instantiate
var plantPrefab = Mods.Entities.getEntityPrototype(planterBox.plantId).moddedGameObject;
// Instantiate the prefab
var plantGO = GameObject.Instantiate(plantPrefab);
// Place the prefab at the plantLocator position of the planter box
plantGO.transform.position = planterBoxGO.plantLocator.transform.position;
// Keep track of our newly spawned plant.
plantVisualizers.Add(planterBox.plantId, plantGO.gameObject);
}
// Handle the updating of the plant scales
//
// [RenderTickRate(1.0f)] means that this function will only be updated once
// a second
[RenderTickRate(1.0f)]
public override void LateUpdate()
{
// Iterate all plant visualizers
foreach(var kvp in plantVisualizers)
{
// Get the plant id
var plantId = kvp.Key;
// Get the plant component
var plant = Mods.Components.get(plantId);
// Calculate the new plant scale
float plantScale = 0.1f + 0.9f * (float)plant.currentGrowthInTicks / (float)plant.totalGrowthTimeInTicks;
// Adjust the plant's scale
kvp.Value.transform.localScale = new Vector3(plantScale, plantScale, plantScale);
}
}
// PlanterBox id to GameObject
Dictionary streamedInPlanterBoxes = new Dictionary();
// Plant id to GameObject
Dictionary plantVisualizers = new Dictionary();
}
[/code]
[/expand]
And that's it! That's basically all the code necessary to have a plant, place it in a planter box and have it grow over time, all done inside of a mod and as a thank you for sticking around this far, here's a look at the PlanterBox mod working along side a Sprinkler mod that was made from one of the in game Pumps:
[previewyoutube=RIAgVtB_cqk;full][/previewyoutube]
[h1]Future Work[/h1]
This is was just one of the ways that we're looking at making Foundry moddable. We're also working on Harmony support for those of you who want to hook into any C# functions we have. And for those of you who who are just looking to add/tweak/create crafting recipes/research/buildings, we're working on making it so you can do that by modifying data files, no programming necessary!
That's all I have for this Foundry Friday. Once again...
Thanks for listening!