Skip to content

Interactables

Step-by-Step Tutorial

  • 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);
    private GameObject beebleStatue = Addressables.LoadAssetAsync<GameObject>("RoR2/Base/Beetle/mdlBeetle.fbx").WaitForCompletion();
    private GameObject beebleStatue = PrefabAPI.InstantiateClone(Addressables.LoadAssetAsync<GameObject>("RoR2/Base/Beetle/mdlBeetle.fbx").WaitForCompletion(), "BeebleMemorialStatue");
    private Material beebleStatueMat = Addressables.LoadAssetAsync<Material>("RoR2/Base/MonstersOnShrineUse/matMonstersOnShrineUse.mat").WaitForCompletion();
    
  • 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 called
    beebleStatue.name = "BeebleMemorialStatue"; 
    
    // If the GameObject you're using doesn't have a NetworkIdentity, add one for multiplayer compat
    beebleStatue.AddComponent<NetworkIdentity>(); 
    
    // Scaling the model up 3x since we want a nice, large shrine
    beebleStatue.transform.localScale = new Vector3(3f, 3f, 3f); 
    
    // This applies the statue material to the mesh
    beebleStatue.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 BoxCollider
    beebleStatue.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 well
    PurchaseInteraction interaction = beebleStatue.AddComponent<PurchaseInteraction>(); 
    
    // What the shrine displays when near
    interaction.contextToken = "Beeble Memorial (E to pay respects)"; 
    
    // What the shrine displays on ping
    interaction.NetworkdisplayNameToken = "Beeble Memorial (E to pay respects)"; 
    
    // The renderer that will be highlighted by our Highlight component
    beebleStatue.GetComponent<Highlight>().targetRenderer = beebleStatue.GetComponentInChildren<SkinnedMeshRenderer>(); 
    
    // Create a new GameObject that'll act as the trigger
    GameObject trigger = Instantiate(new GameObject("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 highlight
    trigger.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
        public class BeebleMemorialManager : NetworkBehaviour
        {
          public PurchaseInteraction purchaseInteraction;
          private GameObject shrineUseEffect = Addressables.LoadAssetAsync<GameObject>("RoR2/Base/Common/VFX/ShrineUseEffect.prefab").WaitForCompletion();
    
          public void Start()
          {
            if (NetworkServer.active && Run.instance)
            {
              purchaseInteraction.SetAvailable(true);
            }
    
            purchaseInteraction.onPurchase.AddListener(OnPurchase);
          }
    
          [Server]
          public void OnPurchase(Interactor interactor)
          {
            if (!NetworkServer.active)
            {
              Debug.LogWarning("[Server] function 'BeebleMemorialManager::OnPurchase(RoR2.Interactor)' called on client");
              return;
            }
    
            EffectManager.SpawnEffect(shrineUseEffect, new EffectData()
            {
              origin = gameObject.transform.position,
              rotation = Quaternion.identity,
              scale = 3f,
              color = Color.cyan
            }, true);
            Chat.SendBroadcastChat(new Chat.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
    BeebleMemorialManager mgr = beebleStatue.AddComponent<BeebleMemorialManager>();
    PurchaseInteraction interaction = 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.
    InteractableSpawnCard interactableSpawnCard = ScriptableObject.CreateInstance<InteractableSpawnCard>();
    interactableSpawnCard.name = "iscBeebleMemorialStatue";
    interactableSpawnCard.prefab = beebleStatue;
    interactableSpawnCard.sendOverNetwork = true;
    // The size of the interactable, there's Human, Golem, and BeetleQueen
    interactableSpawnCard.hullSize = HullClassification.Golem; 
    // Which nodegraph should it spawn on, air or ground
    interactableSpawnCard.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 it
    interactableSpawnCard.forbiddenFlags = RoR2.Navigation.NodeFlags.NoShrineSpawn;
    // How much should it cost the director to spawn your interactable
    interactableSpawnCard.directorCreditCost = 0;
    interactableSpawnCard.occupyPosition = true;
    interactableSpawnCard.orientToFloor = false;
    interactableSpawnCard.skipSpawnWhenSacrificeArtifactEnabled = false;
    
  • Now we'll need to create the DirectorCard and the DirectorCardHolder
    DirectorCard directorCard = new DirectorCard
    {
      selectionWeight = 100, // The higher this number the more common it'll be, for reference a normal chest is about 230
      spawnCard = interactableSpawnCard,
    };
    
    DirectorAPI.DirectorCardHolder directorCardHolder = new DirectorAPI.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 stage
    DirectorAPI.Helpers.AddNewInteractable(directorCardHolder);
    // Or create your stage list and register it on each of those stages
    List<DirectorAPI.Stage> stageList = new List<DirectorAPI.Stage>();
    
    stageList.Add(DirectorAPI.Stage.DistantRoost);
    stageList.Add(DirectorAPI.Stage.AbyssalDepthsSimulacrum);
    
    foreach (DirectorAPI.Stage stage in stageList)
    {
      DirectorAPI.Helpers.AddNewInteractableToStage(directorCardHolder, stage);
    }
    
  • Congrats! You can now pay respect to the beetles who bravely gave their lives in the Great Lunar War beeble
Back to top