Have your mod project set up, check the First Mod template if you haven't already. We'll be making a Beetle Memorial statue as the example interactable, the repo with the complete code is here.
Load the required assets, if you're using an existing game asset, you'll need to use PrefabAPI to clone the object so you don't affect the original. The below code includes code for both, choose one "beebleStatue". Note: If you use your own model, you'll need to register it using PrefabAPI after setting it up, PrefabAPI.RegisterNetworkPrefab(InteractableModel);
Now that we have the assets, let's prep the beetle model to be turned into a shrine.
// Changing the name to what you'd like it to be calledbeebleStatue.name="BeebleMemorialStatue";// If the GameObject you're using doesn't have a NetworkIdentity, add one for multiplayer compatbeebleStatue.AddComponent<NetworkIdentity>();// Scaling the model up 3x since we want a nice, large shrinebeebleStatue.transform.localScale=newVector3(3f,3f,3f);// This applies the statue material to the meshbeebleStatue.transform.GetChild(1).GetComponent<SkinnedMeshRenderer>().sharedMaterial=beebleStatueMat;// Adding a collider so the statue is solid, though it uses a SkinnedMeshRenderer so it doesn't easily work with a MeshCollider, for simplicity sake we're using a simple BoxColliderbeebleStatue.transform.GetChild(1).gameObject.AddComponent<BoxCollider>();
After the asset has been prepped, now we'll need to turn it into an interactable
// Add main necessary component PurchaseInteraction, this will add a Highlight component as wellPurchaseInteractioninteraction=beebleStatue.AddComponent<PurchaseInteraction>();// What the shrine displays when nearinteraction.contextToken="Beeble Memorial (E to pay respects)";// What the shrine displays on pinginteraction.NetworkdisplayNameToken="Beeble Memorial (E to pay respects)";// The renderer that will be highlighted by our Highlight componentbeebleStatue.GetComponent<Highlight>().targetRenderer=beebleStatue.GetComponentInChildren<SkinnedMeshRenderer>();// Create a new GameObject that'll act as the triggerGameObjecttrigger=Instantiate(newGameObject("Trigger"),beebleStatue.transform);// Adding a BoxCollider and setting it to be a trigger so it's not solid trigger.AddComponent<BoxCollider>().isTrigger=true;// EntityLocator is necessary for the interactable highlighttrigger.AddComponent<EntityLocator>().entity=beebleStatue;
Great, we now have an interactable, but it does nothing at the moment, so let's create a component that adds functionality
publicclassBeebleMemorialManager:NetworkBehaviour{publicPurchaseInteractionpurchaseInteraction;privateGameObjectshrineUseEffect=Addressables.LoadAssetAsync<GameObject>("RoR2/Base/Common/VFX/ShrineUseEffect.prefab").WaitForCompletion();publicvoidStart(){if(NetworkServer.active&&Run.instance){purchaseInteraction.SetAvailable(true);}purchaseInteraction.onPurchase.AddListener(OnPurchase);} [Server]publicvoidOnPurchase(Interactorinteractor){if(!NetworkServer.active){Debug.LogWarning("[Server] function 'BeebleMemorialManager::OnPurchase(RoR2.Interactor)' called on client");return;}EffectManager.SpawnEffect(shrineUseEffect,newEffectData(){origin=gameObject.transform.position,rotation=Quaternion.identity,scale=3f,color=Color.cyan},true);Chat.SendBroadcastChat(newChat.SimpleChatMessage(){baseToken="<style=cEvent><color=#307FFF>o7 to the Beebles we lost in the great lunar war.</color></style>"});}}
Now we'll need to go back and add this component to the shrine and also add the purchaseinteraction to our new component
BeebleMemorialManagermgr=beebleStatue.AddComponent<BeebleMemorialManager>();PurchaseInteractioninteraction=beebleStatue.AddComponent<PurchaseInteraction>();interaction.contextToken="Beeble Memorial (E to pay respects)";interaction.NetworkdisplayNameToken="Beeble Memorial (E to pay respects)";mgr.purchaseInteraction=interaction;
So the interactable is now done, you can spawn it directly wherever you want if that's what you're going for, though if you want it to be a normal interactable selectable by the stage director, you'll need to register it using DirectorAPI. First we'll need to create an InteractableSpawnCard that'll contain information that the director will need to spawn it.
InteractableSpawnCardinteractableSpawnCard=ScriptableObject.CreateInstance<InteractableSpawnCard>();interactableSpawnCard.name="iscBeebleMemorialStatue";interactableSpawnCard.prefab=beebleStatue;interactableSpawnCard.sendOverNetwork=true;// The size of the interactable, there's Human, Golem, and BeetleQueeninteractableSpawnCard.hullSize=HullClassification.Golem;// Which nodegraph should it spawn on, air or groundinteractableSpawnCard.nodeGraphType=RoR2.Navigation.MapNodeGroup.GraphType.Ground;interactableSpawnCard.requiredFlags=RoR2.Navigation.NodeFlags.None;// Nodes have flags that help define what can be spawned on it, any node marked "NoShrineSpawn" shouldn't spawn our shrine on itinteractableSpawnCard.forbiddenFlags=RoR2.Navigation.NodeFlags.NoShrineSpawn;// How much should it cost the director to spawn your interactableinteractableSpawnCard.directorCreditCost=0;interactableSpawnCard.occupyPosition=true;interactableSpawnCard.orientToFloor=false;interactableSpawnCard.skipSpawnWhenSacrificeArtifactEnabled=false;
Now we'll need to create the DirectorCard and the DirectorCardHolder
DirectorCarddirectorCard=newDirectorCard{selectionWeight=100,// The higher this number the more common it'll be, for reference a normal chest is about 230spawnCard=interactableSpawnCard,};DirectorAPI.DirectorCardHolderdirectorCardHolder=newDirectorAPI.DirectorCardHolder{Card=directorCard,InteractableCategory=DirectorAPI.InteractableCategory.Shrines};
Final step, we need to register it with the DirectorAPI, you can register it with all stages or specific stages
// Registers the interactable on every stageDirectorAPI.Helpers.AddNewInteractable(directorCardHolder);// Or create your stage list and register it on each of those stagesList<DirectorAPI.Stage>stageList=newList<DirectorAPI.Stage>();stageList.Add(DirectorAPI.Stage.DistantRoost);stageList.Add(DirectorAPI.Stage.AbyssalDepthsSimulacrum);foreach(DirectorAPI.StagestageinstageList){DirectorAPI.Helpers.AddNewInteractableToStage(directorCardHolder,stage);}
Congrats! You can now pay respect to the beetles who bravely gave their lives in the Great Lunar War