- Prerequisites
- Unity Project
2.1. Project Setup
2.2. Creating your first Prefab
2.3. Icon Creation
2.4. Creating an Asset Bundle - Visual Studio Project
3.1. Preparing the Base Class
3.2. Abstraction and You (Creating the Item Base Class)
3.3. Creating the Equipment Base Class
3.4. Implementing Our First Item
-
Read through this tutorial to set up your first project, and get a feel for the environment. Link: https://github.com/risk-of-thunder/R2Wiki/wiki/%5BIn-depth%5D-First-mod
-
Download and install Unity. Current version is: https://download.unity3d.com/download_unity/e6e9ca02b32a/Windows64EditorInstaller/UnitySetup64-2018.4.16f1.exe
-
After installing Unity, start it up.
-
Create new project.
-
Fill the
Project Name
field with your mod name, Template Type is3D
. -
Hit
Create
. -
On the bottom left of the window, you will see the file explorer for Unity. Delete the
Scenes
folder. -
Clear the scene hierarchy at the top left.
-
We'll create a folder structure to keep our assets organized, create the following folder structure:
Assets
Models
Prefabs
Item
First Item
Equipment
Meshes
Shaders
Textures
Icons
Item
Equipment
Buff
Materials
Item
First Item
Equipment
-
In the above structure, meshes are your raw models that you made in your 3D program of choice. Prefabs are the GameObject/Model we'll be making out of them. Go ahead and make a model or find one and place it in the Meshes folder.
-
Drag and drop that model to the scene hierarchy at the top left of the Unity window.
-
You'll notice it is in the scene, but it might be untextured (if you did not bake textures into the model). If this is the case, proceed. If it is not the case, click the model in the scene hierarchy at the top left and drag it to your
Models -> Prefabs -> Item -> First Item
folder and skip toStep 9
. -
Click into
Materials -> Item -> First Item
. -
Right Click anywhere in this folder and
Create -> Material
. -
A new material will be created and ask you for a name. The general practice for naming materials is to name them after what you're applying it to. So, if you're making a material for a handle of a mesh then
ItemNameHandle
. -
Click the material, and at the top right you should see all kinds of properties. For now, just modify the
Color
,Smoothness
, andEmission
to your liking. -
Once done, to apply it you simply drag and drop it either on the part of the model, or the part in the scene hierarchy you want to apply it to.
-
Once done setting up materials and applying them, drag the Prefab (in the scene hierarchy it has a box next to it) to your
Models -> Prefabs -> Item -> First Item
folder. -
When asked if you want to make an
Original Prefab
or aVariant Prefab
, chooseOriginal
. To get the base for an icon, proceed with the following section, otherwise skip over it.
-
Assuming the model is still in the scene from the previous steps, continue. Otherwise drag the prefab into the scene hierarchy, and continue.
-
In the scene itself, turn off the
Skybox
. -
In the scene itself, on the top right of it go to
Gizmos -> Uncheck Show Grid
. -
Right click the compass, and pick a facing you'd like to have your icon be. Alternatively, just rotate the prefab via the values at the top right till you get the alignment you like, but make sure to set the values back after.
-
Using a screen capture tool (like Lightshot, Greenshot, Printscr, etc), capture the area encompassing the model. Copy it to clipboard.
-
Open your image editor of choice, though for this tutorial I'll cover how to utilize the
Hopoo Outline Template
. That said, openPhotoshop
. -
Create a new image, hit
OK
. -
Paste in your image that you captured previously.
-
Select the
Magic Wand
tool, lower the tolerance down to around0
. -
Select the gray areas that were from the background in Unity.
-
Delete the selection.
-
Invert the selection, you should now have a selection around your item icon.
-
At the top,
Image -> Crop
. The image should now only contain the area directly containing the icon. -
Try to get the image size as close as you can to
460x460
. It doesn't have to be exact here, just one of the dimensions should be close to this. -
Increase
Canvas Size
to512x512
. There should be ample space around your icon right now. -
Filters -> Blur -> Blur More
-
Download the Hopoo Outline Templates: https://cdn.discordapp.com/attachments/567827235013132291/769053836077432872/RoR2_Public_Templates.rar
-
After extracting it, open the
Item Icon Template.psd
-
Expand the Layer Groups on the bottom right and delete any of the icons they had in there previously. Hide any other layers/sublayers aside from the one you want to place your item in.
-
In your icon image,
Select All
andCopy
. -
In the Item Icon Template, click the appropriate layer group for the Tier of your item.
-
CRTL+V to paste it in.
-
If everything looks alright, with the layer selected go to
File -> Export -> Export As
. -
Change the
Export Dimensions
to128x128
and save. -
Drag the resultant file to your Unity Instance's
Textures -> Icons -> Item
folder. -
Click it, and on the top right you'll see
Alpha is Transparency
. Click it. -
Increase Anisotropy Level to max, or just leave it alone.
-
At the bottom set compression to
None
. -
Hit
Apply
. -
At the top of that same area, where it says
Texture Type
, click it and set it toSprite (2D and UI)
. -
Hit
Apply
, and you're done with the icon. Congrats!
For asset bundles, think of them as if they're a central zip/pak/etc that stores all your assets in a project (usually excluding sound assets in ROR2). To get started, do the following:
-
On the top of the unity editor, click
Window -> Package Manager
, findAsset Bundle Browser
, and clickinstall
. -
Click any of the materials/prefabs/icons we made thus far and on the bottom right where it previews it, you'll see AssetBundle with a dropdown menu next to it. Click the dropdown menu.
-
In that menu, hit
New..
and name your asset bundle. For example,mycoolmod_assets
. -
In that same dropdown menu, if it is not already assigned, assign it to your new assetbundle by clicking the name.
-
Mixed results here, but you can either click through all your assets in the project and assign them to the asset bundle and skip over the next step, or continue.
-
On the top of the unity editor, Click
Window -> AssetBundle Browser
. It will pull up a window that should show your assetbundle name in the first tab. Click that, and then one of the assets on the list to the right. If a ton show up that you're sure you haven't assigned but want to, click one and do CRTL+A. Generally you'll know this if the list contains folders. Finally, on the bottom right of the unity editor, assign it to the assetbundle and continue to the next step. -
With this same window up, click the tab at the top of it that says
Build
. -
Assuming you went through the tutorial to set up a Visual Studio project, in the assetbundle browser window under
Output Path
click browse and navigate to your visual studio project folder. You can find these by default inYourUserFolder/repos/
. You want to select the directory that contains your CSProj file. -
Don't click any of the checkboxes in the Build tab, just hit
Build
. -
Congrats, you've just built your first asset bundle.
-
Name your base class anything like
Main.cs
orMyModName.cs
to keep track of it. -
Fill it out with the following:
using BepInEx; using R2API; using System.Reflection; using UnityEngine; namespace MyModCsProjDirectoryName { [BepInDependency(R2API.R2API.PluginGUID, R2API.R2API.PluginVersion)] [NetworkCompatibility(CompatibilityLevel.EveryoneMustHaveMod, VersionStrictness.EveryoneNeedSameModVersion)] [BepInPlugin("com.MyUsername.MyModName", "My Mod's Title and if we see this exact name on Thunderstore we will deprecate your mod", "1.0.0")] public class MyModName : BaseUnityPlugin { public void Awake() { } } }
-
Change the namespace to the name of the folder you store the csproj in, for example:
namespace IfMyNamespaceRemainsThisItMeansIDidn'tRead {
-
Change the relevant fields in the BepinPlugin attribute to suit your mod. For this tutorial we're going to extract these out into fields we'll define in our main class. For example:
[BepinPlugin(ModGuid, ModName, ModVer)]
-
Change the class name to match the class name file's name. For example, if you named your class Main.cs, then:
public class Main : BaseUnityPlugin {
-
We'll create the fields for our plugin attribute, the thing I mentioned in step 4. You should generally update the ModVer field any time you plan to make an update to your mod.
public class Main : BaseUnityPlugin { public const string ModGuid = "com.MyUserName.MyModName"; //Our Package Name public const string ModName = "MyModName"; public const string ModVer = "0.0.1"; public void Awake() { } }
-
How do I know what number to change on my mod version? Simple really, the format for this is called the Major.Minor.Patch Versioning Standard and they generally follow this guideline:
MAJOR version when you either complete your development milestones, or make extreme changes to your mod. MINOR version when you are adding minor features (like another item for instance) PATCH version increment when you're patching bugs, doing small tweaks, polishing assets, etc.
-
Assuming you followed the Asset Bundle creation portion of this tutorial, the asset bundle should be showing up in your Solution Explorer to the right as well as its Manifest file. Click the asset bundle file.
-
On the bottom right of Visual Studio's window, you'll see the properties for the Asset Bundle file. Set
Build Action
toEmbedded Resource
. -
Set
Copy to Output Directory
toDo Not Copy
-
In your Main.cs, add the following into your
Awake()
methodpublic class Main : BaseUnityPlugin { public const string ModGuid = "com.MyUserName.MyModName"; //Our Package Name public const string ModName = "MyModName"; public const string ModVer = "0.0.1"; public static AssetBundle MainAssets; public void Awake() { using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MyCoolModsNamespaceHere.mycoolmod_assets")) { MainAssets = AssetBundle.LoadFromStream(stream); } } }
-
Your code should look like this by now:
using BepInEx; using R2API; using System.Reflection; using UnityEngine; namespace MyModCSProjDirectoryName { [BepInDependency(R2API.R2API.PluginGUID, R2API.R2API.PluginVersion)] [NetworkCompatibility(CompatibilityLevel.EveryoneMustHaveMod, VersionStrictness.EveryoneNeedSameModVersion)] [BepinPlugin(ModGuid, ModName, ModVer)] public class Main : BaseUnityPlugin { public const string ModGuid = "com.MyUserName.MyModName"; //Our Package Name public const string ModName = "MyModName"; public const string ModVer = "0.0.1"; public static AssetBundle MainAssets; public void Awake() { using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MyCoolModsNamespaceHere.mycoolmod_assets")) { MainAssets = AssetBundle.LoadFromStream(stream); } } } }
-
As we utilize more of R2API, we'll need to add and use various parts of R2API's submodules. To do so, we'll use the
R2APISubmoduleDependency
attribute, and add onto said attribute the further we go along. Doing so just requires you to alter it like so with whatever API you need to load in. In this example we'll load in ItemAPI:[R2APISubmoduleDependency(nameof(ItemAPI))]
-
Easy right? To add more onto it, put a comma after
nameof(ItemAPI)
and add another. Let's move on to the harder bits now. -
You're probably noticing a few errors pop up in various places here. We'll need to add a reference to a Unity engine dll. In your Risk of Rain 2 folder, look for the
Managed
folder. -
If you've followed a competent tutorial, it will have recommended you to make a libs folder in your Visual Studio project directory (the one with your csproj file). If you haven't yet, make one.
-
If you're referencing the
BepinEx
andR2API
dlls from anywhere but the libs folder, go ahead and remove those references now from your dependencies. Then, copy the dlls to the libs folder we made in the previous step, and add them back in as references now. -
Back in the
Managed
folder, look for theUnityEngine
dll, andUnityEngine.AssetBundleModule
dll. Copy these to your libs folder, and add them as references in your visual studio project. -
You should have noticed the errors go away now.
If you're not too experienced with formal programming you're probably wondering, what is abstraction? In the context of this tutorial, we'll be creating abstract "ItemBase" and "EquipmentBase" classes. Imagine them to be a skeleton that all our items and all our equipment share. One that we add the muscle and skin to in our individual items and equipment. Let's get started.
-
In the solution explorer, right click on the CSProj file (the green rectangle with a C# in it) and
Add -> New Folder
. Name itItems
. -
Right click the
Items
folder, andAdd -> Class
. -
Name the class
ItemBase
. -
At the top of the class, with the other using, we need to add the
R2API
andRoR2
name space usings. It should look similar to the following:using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text;
-
Add a
public
access modifier to the front of your class definition, and anabstract
after it. For example:using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { } }
-
The
abstract
keyword lets us create a class in which all things that inherit from it must implement its fields/methods/etc that are abstract. Best part of this is that when we make an item inherit from it, we can use a quick action to quickly generate in those fields for us to fill out! First things first, we should think of what all our items need and what things they will all have in common. To this end we'll need:-
A
Name
field - This is the name you give to your item. Like"Omnipotent Egg"
for example. -
A
Name Language Token
field - This is what the code will identify this item by. Like"OMNIPOTENT_EGG"
for example. -
A
Pickup Description
field - This is the short description of our item. -
A
Full Item Description
field - This is the long description of our item. It should be detailed. -
A
Lore Entry
field - This is our lore snippet for the item, they're optional but users like to read them sometimes. -
An
Item Tier
field - This is what tier our item will appear in.ItemTier.Lunar
for example. -
An
Item Model
field - This is the direct reference to our model in our asset bundle. We need to load it in when we set this, and we can do it with our MainAsset file. LikeMainAssets.LoadAsset<type>("nameoffile.extension");
-
An
Item Icon
field - This is the direct reference to our item icon in our asset bundle. We need to load it in when we set this, and we can do it with our MainAsset file. LikeMainAssets.LoadAsset<type>("nameoffile.extension");
-
An
Initialization
method - Necessary in ordering your code execution flow in your items/item base (or what executes in what order). Also in the context of this tutorial, it will allow you to easily pass through the config file provided to you automatically by BepinEx to allow easy config entries. More on this later. -
An
Item Display Rule Setup
method - Necessary for an easy time setting up the actual ingame display for your item models. TheItemDisplayRuleDict
we return from this method will allow us to attach our item display rules to our item definition. -
A
Hooks
method - Necessary to keep track of what events/methods we subscribe to that we use to give our item its functionality. -
Well add more fields later as we need them, but for now we have our required abstract properties/fields/methods here.
-
-
Let's start adding these fields/properties in to the abstract class. We'll use the same access and keyword before our type so things inheriting the Item Base must implement them to use the interface. We'll do so for
Name
,Name Language Token
,Pickup Description
,Full Item Description
, andLore Entry
firstly. -
Fill out your item base to look like the following:
using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } } }
-
Let's add in the rest of the properties on our needs list. Like so:
using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } } }
-
Now we'll need to create our abstract methods, these are slightly different to define than the properties above. For
Initialization
, we'll do something special. We'll add a parameter to pass in aConfigFile
. Why you ask? It is so we can use the BepinEx configuration that our main plugin class inherits to provide easy config options to our items, we do this by forwarding it to a Config method on our item classes, more on that later. We'll also need to add a using for BepinEx.Configuration. So to define this method, we do:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public abstract void Init(ConfigFile config); } }
-
Adding in our final needs on the checklist we made should be simple at this point. It's similar in definition to the above, but with no parameters and a different return type. We'll add it like so:
using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public abstract void Init(ConfigFile config); public abstract ItemDisplayRuleDict CreateItemDisplayRules(); public abstract void Hooks(); } }
-
If you've been following along, we're actually almost to the point we can use our ItemBase now. Amazing, right? For the next step, we can create a method to set up the language tokens inside of our ItemBase. This will give this functionality with no requirement to implement it on the inheritors, it simply relies on being initialized and the assumption we've implemented our strings (the first 5 properties of our itembase).
-
To create the method we will use to set up our language tokens, we will be creating a method with the
protected
access modifier and avoid
return type.Protected
means only itself and subtypes of itself can use the method. To do so we do the following:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public abstract string ItemModelPath { get; } public abstract string ItemIconPath { get; } public abstract void Init(ConfigFile config); protected void CreateLang() { } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); public abstract void Hooks(); } }
-
Now we need to fill this method out so we register our language tokens on each item that inherits this abstract class. To do this, we'll need to use LanguageAPI to add our language tokens. For this step we'll use the overload of their addition method that requires
key
andvalue
. Ourkey
is our name identifier for the token (or the ID for it) like in the case of our item base, it'd be like"ITEM_OMNIPOTENT_EGG_NAME"
. Thevalue
is the actual values of our fields, like"Omnipotent Egg"
. To fill this out almost automatically, we'll be using the properties we defined earlier.using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_NAME", ItemName); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_PICKUP", ItemPickupDesc); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_DESCRIPTION", ItemFullDescription); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_LORE", ItemLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); public abstract void Hooks(); } }
-
Now back in our main class, we'll need to add the LanguageAPI submodule dependency. Like so:
using BepInEx; using R2API; using System.Reflection; using UnityEngine; namespace MyModCSProjDirectoryName { [BepInDependency(R2API.R2API.PluginGUID, R2API.R2API.PluginVersion)] [NetworkCompatibility(CompatibilityLevel.EveryoneMustHaveMod, VersionStrictness.EveryoneNeedSameModVersion)] [BepinPlugin(ModGuid, ModName, ModVer)] [R2APISubmoduleDependency(nameof(ItemAPI), nameof(LanguageAPI))] public class Main : BaseUnityPlugin { public const string ModGuid = "com.MyUserName.MyModName"; //Our Package Name public const string ModName = "MyModName"; public const string ModVer = "0.0.1"; public static AssetBundle MainAssets; public void Awake() { using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MyCoolModsNamespaceHere.mycoolmod_assets")) { MainAssets = AssetBundle.LoadFromStream(stream); } } } }
-
Next, we'll make another method in our ItemBase to create our item definition out of our properties and language tokens,
CreateItem()
similar to theCreateLang()
method we did above.using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_NAME", ItemName); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_PICKUP", ItemPickupDesc); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_DESCRIPTION", ItemFullDescription); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_LORE", ItemLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateItem() { } public abstract void Hooks(); } }
-
What we'll want to populate this method with is a way to create an item definition. To do this, we will be creating a new
ItemDef
. To do so, let us think about all the things anItemDef
shares, and what they do so we can plan out how to lay it out. AnItemDef
contains the following major things:-
name
- The identifier of the item. For exampleITEM_OMNIPOTENT_EGG
. -
nameToken
- The string corresponding to the language token for the name of the item. We defined this above. For exampleITEM_OMNIPOTENT_EGG_NAME
. -
pickupToken
- The string corresponding to the language token for the pickup description of the item. For example,ITEM_OMNIPOTENT_EGG_PICKUP
. -
descriptionToken
- The string corresponding to the language token for the detailed description of the item. For example,ITEM_OMNIPOTENT_EGG_DESCRIPTION
. -
loreToken
- The string corresponding to the language token for the lorebook entry of the item. For example,ITEM_OMNIPOTENT_EGG_LORE
. -
pickupModelPrefab
- The direct reference to a prefab from our assetbundle. This requires a GameObject, we provide one in ourItemModel
field which we'll use to set this. -
pickupIconSprite
- The direct reference to a prefab from our assetbundle. This requires a Sprite, we provide one in ourItemIcon
field which we'll use to set this.. -
hidden
- A boolean that determines whether or not the item in question will be displayed upon the others in the lore screen, and item selections. That said, not every item requires this. -
tags
- An enum array that represents the category of your item, and gives your item unique features from the base game. An example isItemTag.AIBlacklist
which will prevent the AI from obtaining or using your item. Another isItemTag.Utility
which will make the item appear inUtility
chests. That said, not every item requires this. -
canRemove
- A boolean that determines whether or not we can drop the item, or remove it from our inventory with things like the Bazaar cauldrons. That said, not every item requires this. -
tier
- An enum that determines what Tier the item will appear. Not only does this containItemTier.Tier1
,ItemTier.Tier2
, andItemTier.Tier3
but also contains unique tiers likeItemTier.Boss
,ItemTier.Lunar
, andItemTier.None
-
-
In the above, we identified all the needed major components of the
ItemDef
we'll be writing, but we also identified a few optional parts. For these, we don't want to force inheritors of theItemBase
to implement them, but we want to provide the option to do so and still have a value we can use if they don't. For this, we'll utilize thevirtual
keyword to do so on a few properties and give it a default value for those who don't want to implement it. -
Let's implement these properties now. The way to do this is similar in method to how we did our
abstract
properties, but with one key difference, we'll give one them a default value as well. Like so:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public virtual ItemTag[] ItemTags { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public virtual bool CanRemove { get; } = true; public virtual bool Hidden { get; } = false; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_NAME", ItemName); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_PICKUP", ItemPickupDesc); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_DESCRIPTION", ItemFullDescription); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_LORE", ItemLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateItem() { } public abstract void Hooks(); } }
-
Now we create our
ItemDef
. First, we create a field to save ourItemDef
that we are about to create. Each class that inherits this abstract base will have its own definedItemDef
here. Now we create theItemDef
itself. It is made up of all the properties we've defined thus far in a manner of speaking.using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public virtual ItemTag[] ItemTags { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public virtual bool CanRemove { get; } = true; public virtual bool Hidden { get; } = false; public ItemDef ItemDef; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_NAME", ItemName); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_PICKUP", ItemPickupDesc); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_DESCRIPTION", ItemFullDescription); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_LORE", ItemLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateItem() { ItemDef = ScriptableObject.CreateInstance<ItemDef>(); ItemDef.name = "ITEM_" + ItemLangTokenName; ItemDef.nameToken = "ITEM_" + ItemLangTokenName + "_NAME"; ItemDef.pickupToken = "ITEM_" + ItemLangTokenName + "_PICKUP"; ItemDef.descriptionToken = "ITEM_" + ItemLangTokenName + "_DESCRIPTION"; ItemDef.loreToken = "ITEM_" + ItemLangTokenName + "_LORE"; ItemDef.pickupModelPrefab = ItemModel; ItemDef.pickupIconSprite = ItemIcon; ItemDef.hidden = false; ItemDef.canRemove = CanRemove; ItemDef.tier = Tier; ItemDef.tags = ItemTags; } public abstract void Hooks(); } }
-
To register the item, we'll need to use
ItemAPI
.ItemAPI.Add()
requires one argument, a newCustomItem
.CustomItem
requires theItemDef
we just created, and anItemDisplayRuleDict
which we have created a method for earlier. First things first, let's create a variable to store ourItemDisplayRuleDict
and we'll do it by calling that method. Like so:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public virtual ItemTag[] ItemTags { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public virtual bool CanRemove { get; } = true; public virtual bool Hidden { get; } = false; public ItemDef ItemDef; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_NAME", ItemName); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_PICKUP", ItemPickupDesc); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_DESCRIPTION", ItemFullDescription); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_LORE", ItemLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateItem() { ItemDef = ScriptableObject.CreateInstance<ItemDef>(); ItemDef.name = "ITEM_" + ItemLangTokenName; ItemDef.nameToken = "ITEM_" + ItemLangTokenName + "_NAME"; ItemDef.pickupToken = "ITEM_" + ItemLangTokenName + "_PICKUP"; ItemDef.descriptionToken = "ITEM_" + ItemLangTokenName + "_DESCRIPTION"; ItemDef.loreToken = "ITEM_" + ItemLangTokenName + "_LORE"; ItemDef.pickupModelPrefab = ItemModel; ItemDef.pickupIconSprite = ItemIcon; ItemDef.hidden = false; ItemDef.canRemove = CanRemove; ItemDef.tier = Tier; ItemDef.tags = ItemTags; var itemDisplayRuleDict = CreateItemDisplayRules(); } public abstract void Hooks(); } }
-
Now that we have the
ItemDisplayRuleDict
, we can register the item with R2API'sItemAPI
. As a recap thatItemDef
we created can be used later in a multitude of ways, one of which allows us to easily create an Inventory Count method to track how many of our item we have. It is the direct reference to our item. By now your class should be looking like this:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public virtual ItemTag[] ItemTags { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public virtual bool CanRemove { get; } = true; public virtual bool Hidden { get; } = false; public ItemDef ItemDef; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_NAME", ItemName); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_PICKUP", ItemPickupDesc); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_DESCRIPTION", ItemFullDescription); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_LORE", ItemLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateItem() { ItemDef = ScriptableObject.CreateInstance<ItemDef>(); ItemDef.name = "ITEM_" + ItemLangTokenName; ItemDef.nameToken = "ITEM_" + ItemLangTokenName + "_NAME"; ItemDef.pickupToken = "ITEM_" + ItemLangTokenName + "_PICKUP"; ItemDef.descriptionToken = "ITEM_" + ItemLangTokenName + "_DESCRIPTION"; ItemDef.loreToken = "ITEM_" + ItemLangTokenName + "_LORE"; ItemDef.pickupModelPrefab = ItemModel; ItemDef.pickupIconSprite = ItemIcon; ItemDef.hidden = false; ItemDef.canRemove = CanRemove; ItemDef.tier = Tier; ItemDef.tags = ItemTags; var itemDisplayRuleDict = CreateItemDisplayRules(); } public abstract void Hooks(); } }
-
Now the moment you've been waiting for, where we register our item. We just need to feed in our
ItemDef
and ouritemDisplayRuleDict
toItemAPI.Add()
.using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public virtual ItemTag[] ItemTags { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public virtual bool CanRemove { get; } = true; public virtual bool Hidden { get; } = false; public ItemDef ItemDef; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_NAME", ItemName); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_PICKUP", ItemPickupDesc); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_DESCRIPTION", ItemFullDescription); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_LORE", ItemLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateItem() { ItemDef = ScriptableObject.CreateInstance<ItemDef>(); ItemDef.name = "ITEM_" + ItemLangTokenName; ItemDef.nameToken = "ITEM_" + ItemLangTokenName + "_NAME"; ItemDef.pickupToken = "ITEM_" + ItemLangTokenName + "_PICKUP"; ItemDef.descriptionToken = "ITEM_" + ItemLangTokenName + "_DESCRIPTION"; ItemDef.loreToken = "ITEM_" + ItemLangTokenName + "_LORE"; ItemDef.pickupModelPrefab = ItemModel; ItemDef.pickupIconSprite = ItemIcon; ItemDef.hidden = false; ItemDef.canRemove = CanRemove; ItemDef.tier = Tier; ItemDef.tags = ItemTags; var itemDisplayRuleDict = CreateItemDisplayRules(); ItemAPI.Add(new CustomItem(ItemDef, itemDisplayRuleDict)); } public abstract void Hooks(); } }
-
Once again, we'll need to return to our main class and check our
R2APISubmoduleDependency
. It should look like this so far:using BepInEx; using R2API; using System.Reflection; using UnityEngine; namespace MyModCSProjDirectoryName { [BepInDependency(R2API.R2API.PluginGUID, R2API.R2API.PluginVersion)] [NetworkCompatibility(CompatibilityLevel.EveryoneMustHaveMod, VersionStrictness.EveryoneNeedSameModVersion)] [BepinPlugin(ModGuid, ModName, ModVer)] [R2APISubmoduleDependency(nameof(ItemAPI), nameof(LanguageAPI))] public class Main : BaseUnityPlugin { public const string ModGuid = "com.MyUserName.MyModName"; //Our Package Name public const string ModName = "MyModName"; public const string ModVer = "0.0.1"; public static AssetBundle MainAssets; public void Awake() { using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MyCoolModsNamespaceHere.mycoolmod_assets")) { MainAssets = AssetBundle.LoadFromStream(stream); } } } }
-
Before we wrap this section up, let's add a few helper method to our
ItemBase
class that will allow us to easily get the count of items that inherit ourItemBase
. We'll call these methodsGetCount
. A key thing to note about inventory is that it's not just stored on theCharacterBody
component, but also theCharacterMaster
component. So we'll define aGetCount
method for both.using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public virtual ItemTag[] ItemTags { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public virtual bool CanRemove { get; } = true; public virtual bool Hidden { get; } = false; public ItemDef ItemDef; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_NAME", ItemName); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_PICKUP", ItemPickupDesc); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_DESCRIPTION", ItemFullDescription); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_LORE", ItemLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateItem() { ItemDef = ScriptableObject.CreateInstance<ItemDef>(); ItemDef.name = "ITEM_" + ItemLangTokenName; ItemDef.nameToken = "ITEM_" + ItemLangTokenName + "_NAME"; ItemDef.pickupToken = "ITEM_" + ItemLangTokenName + "_PICKUP"; ItemDef.descriptionToken = "ITEM_" + ItemLangTokenName + "_DESCRIPTION"; ItemDef.loreToken = "ITEM_" + ItemLangTokenName + "_LORE"; ItemDef.pickupModelPrefab = ItemModel; ItemDef.pickupIconSprite = ItemIcon; ItemDef.hidden = false; ItemDef.canRemove = CanRemove; ItemDef.tier = Tier; ItemDef.tags = ItemTags; var itemDisplayRuleDict = CreateItemDisplayRules(); ItemAPI.Add(new CustomItem(ItemDef, itemDisplayRuleDict)); } public abstract void Hooks(); public int GetCount(CharacterBody body) { } public int GetCount(CharacterMaster master) { } } }
-
The process of populating these two methods is going to be extremely similar between the two of them. First, we'll do a condition with an early return to null check the parameter and passing that the parameter's inventory. If our parameter or its inventory is null, we will early return a value of
0
, as in0
items. If our parameter or its inventory is not null, we'll get the count of our items in that inventory usingparameter.inventory.GetItemCount
which takes our field we defined earlier,ItemDef
. Let's go ahead and populate those methods like so:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public abstract class ItemBase { public abstract string ItemName { get; } public abstract string ItemLangTokenName { get; } public abstract string ItemPickupDesc { get; } public abstract string ItemFullDescription { get; } public abstract string ItemLore { get; } public abstract ItemTier Tier { get; } public virtual ItemTag[] ItemTags { get; } public abstract GameObject ItemModel { get; } public abstract Sprite ItemIcon { get; } public virtual bool CanRemove { get; } = true; public virtual bool Hidden { get; } = false; public ItemDef ItemDef; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_NAME", ItemName); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_PICKUP", ItemPickupDesc); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_DESCRIPTION", ItemFullDescription); LanguageAPI.Add("ITEM_" + ItemLangTokenName + "_LORE", ItemLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateItem() { ItemDef = ScriptableObject.CreateInstance<ItemDef>(); ItemDef.name = "ITEM_" + ItemLangTokenName; ItemDef.nameToken = "ITEM_" + ItemLangTokenName + "_NAME"; ItemDef.pickupToken = "ITEM_" + ItemLangTokenName + "_PICKUP"; ItemDef.descriptionToken = "ITEM_" + ItemLangTokenName + "_DESCRIPTION"; ItemDef.loreToken = "ITEM_" + ItemLangTokenName + "_LORE"; ItemDef.pickupModelPrefab = ItemModel; ItemDef.pickupIconSprite = ItemIcon; ItemDef.hidden = false; ItemDef.canRemove = CanRemove; ItemDef.tier = Tier; ItemDef.tags = ItemTags; var itemDisplayRuleDict = CreateItemDisplayRules(); ItemAPI.Add(new CustomItem(ItemDef, itemDisplayRuleDict)); } public abstract void Hooks(); public int GetCount(CharacterBody body) { if (!body || !body.inventory) { return 0; } return body.inventory.GetItemCount(ItemDef); } public int GetCount(CharacterMaster master) { if (!master || !master.inventory) { return 0; } return master.inventory.GetItemCount(ItemDef); } } }
With that, we've defined an ItemBase
that we can use for our items. We'll improve it later when we get around to adding configuration options to our items.
-
With the creation of the
ItemBase
class, the creation of theEquipmentBase
class will be a much quicker task. First off, right click your CSPROJ and create a new folderEquipment
. Then, right click that folderAdd -> New Class
and name itEquipmentBase
.EquipmentBase
will share all of our language token properties and path properties, so let's just implement those. This time we'll call themEquipmentName
,EquipmentLangTokenName
, and etc. Like so:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } } }
-
We will use the same
Initialization
method that we do in ourItemBase
, as well as theCreateLang
method,CreateItemDisplayRules
method, and theHooks
method. Let's go ahead and add those in.using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public abstract void Init(ConfigFile config); protected void CreateLang() { } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); public abstract void Hooks(); } }
-
Populate the
CreateLang
method similar to how you did inItemBase
using our properties. Like so:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_NAME", EquipmentName); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP", EquipmentPickupDesc); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION", EquipmentFullDescription); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_LORE", EquipmentLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); public abstract void Hooks(); } }
-
We'll need a
CreateEquipment
method similar to how we have aCreateItem
method in ourItemBase
. Let's add that in:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_NAME", EquipmentName); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP", EquipmentPickupDesc); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION", EquipmentFullDescription); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_LORE", EquipmentLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateEquipment() { } public abstract void Hooks(); } }
-
Just like before, we need a definition for our equipment. This isn't
ItemDef
this time though it'sEquipmentDef
and they have a slightly different setup. To that end, let's brainstorm what major components anEquipmentDef
has so we can plan out our class. An EquipmentDef has:-
name
- Just like before, this is the identifier.EQUIPMENT_BOLT_LAUNCHER
for instance. -
nameToken
- Just like before, this corresponds to our Language Token.EQUIPMENT_BOLT_LAUNCHER_NAME
. -
pickupToken
- Just like before, this corresponds to our language token.EQUIPMENT_BOLT_LAUNCHER_PICKUP
. -
descriptionToken
- Just like before, this corresponds to our language token.EQUIPMENT_BOLT_LAUNCHER_DESCRIPTION
. -
loreToken
- Just like before, this corresponds to our language token.EQUIPMENT_BOLT_LAUNCHER_LORE
. -
pickupModelPrefab
- Just like before, this corresponds to a direct reference to our equipment model. -
pickupIconSprite
- Just like before, this corresponds to a direct reference to our equipment icon. -
appearsInSinglePlayer
- A boolean that determines whether or not you want this equipment to appear in Singleplayer. This doesn't have to be changed from the default value we'll assign it, it's optional. -
appearsInMultiplayer
- A boolean that determines whether or not you want this equipment to appear in Multiplayer. This doesn't have to be changed from the default value we'll assign it, it's optional. -
canDrop
- A boolean that determines whether or not we can remove this equipment from our inventory. This doesn't have to be changed from the default value we'll assign it, it's optional. -
cooldown
- A float that determines how long we want our equipment's cooldown duration to be after use. This doesn't have to be changed from the default value we'll assign it, it's optional. -
enigmaCompatible
- A boolean that determines whether or not our equipment can be randomly selected by the Artifact of Enigma. This doesn't have to be changed from the default value we'll assign it, it's optional. -
isBoss
- A boolean that determines if this equipment drops from bosses. This doesn't have to be changed from the default value we'll assign it, it's optional. -
isLunar
- A boolean that determines if this is a normal equipment that can be found in equipment barrels, or can be found in lunar pods. This doesn't have to be changed from the default value we'll assign it, it's optional.
-
-
You'll notice that in the above we have a ton of optional things, and we've already added properties for our required parts of the
EquipmentDef
. Just like in the previous section, we'll createvirtual
properties for these and default values so our inheritors don't have to implement them if they are not needed on an equipment. Like so:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public virtual bool AppearsInSinglePlayer { get; } = true; public virtual bool AppearsInMultiPlayer { get; } = true; public virtual bool CanDrop { get; } = true; public virtual float Cooldown { get; } = 60f; public virtual bool EnigmaCompatible { get; } = true; public virtual bool IsBoss { get; } = false; public virtual bool IsLunar { get; } = false; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_NAME", EquipmentName); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP", EquipmentPickupDesc); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION", EquipmentFullDescription); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_LORE", EquipmentLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateEquipment() { } public abstract void Hooks(); } }
-
Now let's create our
EquipmentDef
with our properties just like we did inItemBase
, theitemDisplayRules
, and then register it all in one go. Like so:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public EquipmentDef EquipmentDef; public virtual bool AppearsInSinglePlayer { get; } = true; public virtual bool AppearsInMultiPlayer { get; } = true; public virtual bool CanDrop { get; } = true; public virtual float Cooldown { get; } = 60f; public virtual bool EnigmaCompatible { get; } = true; public virtual bool IsBoss { get; } = false; public virtual bool IsLunar { get; } = false; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_NAME", EquipmentName); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP", EquipmentPickupDesc); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION", EquipmentFullDescription); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_LORE", EquipmentLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateEquipment() { EquipmentDef = ScriptableObject.CreateInstance<EquipmentDef>(); EquipmentDef.name = "EQUIPMENT_" + EquipmentLangTokenName; EquipmentDef.nameToken = "EQUIPMENT_" + EquipmentLangTokenName + "_NAME"; EquipmentDef.pickupToken = "EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP"; EquipmentDef.descriptionToken = "EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION"; EquipmentDef.loreToken = "EQUIPMENT_" + EquipmentLangTokenName + "_LORE"; EquipmentDef.pickupModelPrefab = EquipmentModel; EquipmentDef.pickupIconSprite = EquipmentIcon; EquipmentDef.appearsInSinglePlayer = AppearsInSinglePlayer; EquipmentDef.appearsInMultiPlayer = AppearsInMultiPlayer; EquipmentDef.canDrop = CanDrop; EquipmentDef.cooldown = Cooldown; EquipmentDef.enigmaCompatible = EnigmaCompatible; EquipmentDef.isBoss = IsBoss; EquipmentDef.isLunar = IsLunar; ItemAPI.Add(new CustomEquipment(EquipmentDef, CreateItemDisplayRules())); } public abstract void Hooks(); } }
-
By now your class should look like this:
using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public EquipmentDef EquipmentDef; public virtual bool AppearsInSinglePlayer { get; } = true; public virtual bool AppearsInMultiPlayer { get; } = true; public virtual bool CanDrop { get; } = true; public virtual float Cooldown { get; } = 60f; public virtual bool EnigmaCompatible { get; } = true; public virtual bool IsBoss { get; } = false; public virtual bool IsLunar { get; } = false; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_NAME", EquipmentName); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP", EquipmentPickupDesc); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION", EquipmentFullDescription); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_LORE", EquipmentLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateEquipment() { EquipmentDef = ScriptableObject.CreateInstance<EquipmentDef>(); EquipmentDef.name = "EQUIPMENT_" + EquipmentLangTokenName; EquipmentDef.nameToken = "EQUIPMENT_" + EquipmentLangTokenName + "_NAME"; EquipmentDef.pickupToken = "EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP"; EquipmentDef.descriptionToken = "EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION"; EquipmentDef.loreToken = "EQUIPMENT_" + EquipmentLangTokenName + "_LORE"; EquipmentDef.pickupModelPrefab = EquipmentModel; EquipmentDef.pickupIconSprite = EquipmentIcon; EquipmentDef.appearsInSinglePlayer = AppearsInSinglePlayer; EquipmentDef.appearsInMultiPlayer = AppearsInMultiPlayer; EquipmentDef.canDrop = CanDrop; EquipmentDef.cooldown = Cooldown; EquipmentDef.enigmaCompatible = EnigmaCompatible; EquipmentDef.isBoss = IsBoss; EquipmentDef.isLunar = IsLunar; ItemAPI.Add(new CustomEquipment(EquipmentDef, CreateItemDisplayRules())); } public abstract void Hooks(); } }
-
This next part is important. We're going to subscribe to an event for the first time. Think of the process as if we had a conveyor belt that leads to two (or more) paths. On one path, we allow the items on the belt to pass as they are. On the others, we modify the items to do specific things if they match certain conditions. At the end (or sometimes in the beginning) we return these modified items back into the belt same as the others. Following me? The point of this in context to this section of the tutorial is that we're going to subscribe to an event and we're going to call a method if the data in that event matches our equipment's
ItemDef
. We'll be using the eventOn.RoR2.EquipmentSlot.PerformEquipmentAction
. Like so:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public EquipmentDef EquipmentDef; public virtual bool AppearsInSinglePlayer { get; } = true; public virtual bool AppearsInMultiPlayer { get; } = true; public virtual bool CanDrop { get; } = true; public virtual float Cooldown { get; } = 60f; public virtual bool EnigmaCompatible { get; } = true; public virtual bool IsBoss { get; } = false; public virtual bool IsLunar { get; } = false; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_NAME", EquipmentName); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP", EquipmentPickupDesc); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION", EquipmentFullDescription); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_LORE", EquipmentLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateEquipment() { EquipmentDef = ScriptableObject.CreateInstance<EquipmentDef>(); EquipmentDef.name = "EQUIPMENT_" + EquipmentLangTokenName; EquipmentDef.nameToken = "EQUIPMENT_" + EquipmentLangTokenName + "_NAME"; EquipmentDef.pickupToken = "EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP"; EquipmentDef.descriptionToken = "EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION"; EquipmentDef.loreToken = "EQUIPMENT_" + EquipmentLangTokenName + "_LORE"; EquipmentDef.pickupModelPrefab = EquipmentModel; EquipmentDef.pickupIconSprite = EquipmentIcon; EquipmentDef.appearsInSinglePlayer = AppearsInSinglePlayer; EquipmentDef.appearsInMultiPlayer = AppearsInMultiPlayer; EquipmentDef.canDrop = CanDrop; EquipmentDef.cooldown = Cooldown; EquipmentDef.enigmaCompatible = EnigmaCompatible; EquipmentDef.isBoss = IsBoss; EquipmentDef.isLunar = IsLunar; ItemAPI.Add(new CustomEquipment(EquipmentDef, CreateItemDisplayRules())); On.RoR2.EquipmentSlot.PerformEquipmentAction += } public abstract void Hooks(); } }
-
Next to the
+=
we're going to input the name we'd like to call our method/delegate. In this case, we'll just use the namePerformEquipmentAction
. Fill that out with that name, thenRight Click the Name -> Quick Actions -> Generate Method EquipmentBase.PerformEquipmentAction
. Make sure you selectGenerate Method EquipmentBase.PerformEquipmentAction
and notGenerate Abstract Method EquipmentBase.PerformEquipmentAction
. Your code should look like this now:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public EquipmentDef EquipmentDef; public virtual bool AppearsInSinglePlayer { get; } = true; public virtual bool AppearsInMultiPlayer { get; } = true; public virtual bool CanDrop { get; } = true; public virtual float Cooldown { get; } = 60f; public virtual bool EnigmaCompatible { get; } = true; public virtual bool IsBoss { get; } = false; public virtual bool IsLunar { get; } = false; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_NAME", EquipmentName); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP", EquipmentPickupDesc); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION", EquipmentFullDescription); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_LORE", EquipmentLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateEquipment() { EquipmentDef = ScriptableObject.CreateInstance<EquipmentDef>(); EquipmentDef.name = "EQUIPMENT_" + EquipmentLangTokenName; EquipmentDef.nameToken = "EQUIPMENT_" + EquipmentLangTokenName + "_NAME"; EquipmentDef.pickupToken = "EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP"; EquipmentDef.descriptionToken = "EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION"; EquipmentDef.loreToken = "EQUIPMENT_" + EquipmentLangTokenName + "_LORE"; EquipmentDef.pickupModelPrefab = EquipmentModel; EquipmentDef.pickupIconSprite = EquipmentIcon; EquipmentDef.appearsInSinglePlayer = AppearsInSinglePlayer; EquipmentDef.appearsInMultiPlayer = AppearsInMultiPlayer; EquipmentDef.canDrop = CanDrop; EquipmentDef.cooldown = Cooldown; EquipmentDef.enigmaCompatible = EnigmaCompatible; EquipmentDef.isBoss = IsBoss; EquipmentDef.isLunar = IsLunar; ItemAPI.Add(new CustomEquipment(EquipmentDef, CreateItemDisplayRules())); On.RoR2.EquipmentSlot.PerformEquipmentAction += PerformEquipmentAction; } private bool PerformEquipmentAction(On.RoR2.EquipmentSlot.orig_PerformEquipmentAction orig, RoR2.EquipmentSlot self, EquipmentDef equipmentDef) { } public abstract void Hooks(); } }
-
The general rule with these kinds of methods is that if we're not planning to disrupt the original input of the method, we return the original input. What we want to do in this method is check if the input data of the method
equipmentDef
is our equipment'sEquipmentDef
. If it is, we will call a method we'll create in a second to allow us an easier time creating equipment use actions. If it isn't, we'll return the original input by passing it back into the parameterorig
. Let's set up the logic for that:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public EquipmentDef EquipmentDef; public virtual bool AppearsInSinglePlayer { get; } = true; public virtual bool AppearsInMultiPlayer { get; } = true; public virtual bool CanDrop { get; } = true; public virtual float Cooldown { get; } = 60f; public virtual bool EnigmaCompatible { get; } = true; public virtual bool IsBoss { get; } = false; public virtual bool IsLunar { get; } = false; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_NAME", EquipmentName); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP", EquipmentPickupDesc); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION", EquipmentFullDescription); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_LORE", EquipmentLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateEquipment() { EquipmentDef = ScriptableObject.CreateInstance<EquipmentDef>(); EquipmentDef.name = "EQUIPMENT_" + EquipmentLangTokenName; EquipmentDef.nameToken = "EQUIPMENT_" + EquipmentLangTokenName + "_NAME"; EquipmentDef.pickupToken = "EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP"; EquipmentDef.descriptionToken = "EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION"; EquipmentDef.loreToken = "EQUIPMENT_" + EquipmentLangTokenName + "_LORE"; EquipmentDef.pickupModelPrefab = EquipmentModel; EquipmentDef.pickupIconSprite = EquipmentIcon; EquipmentDef.appearsInSinglePlayer = AppearsInSinglePlayer; EquipmentDef.appearsInMultiPlayer = AppearsInMultiPlayer; EquipmentDef.canDrop = CanDrop; EquipmentDef.cooldown = Cooldown; EquipmentDef.enigmaCompatible = EnigmaCompatible; EquipmentDef.isBoss = IsBoss; EquipmentDef.isLunar = IsLunar; ItemAPI.Add(new CustomEquipment(EquipmentDef, CreateItemDisplayRules())); On.RoR2.EquipmentSlot.PerformEquipmentAction += PerformEquipmentAction; } private bool PerformEquipmentAction(On.RoR2.EquipmentSlot.orig_PerformEquipmentAction orig, RoR2.EquipmentSlot self, EquipmentDef equipmentDef) { if (equipmentDef == EquipmentDef) { } else { return orig(self, equipmentDef); } } public abstract void Hooks(); } }
-
In our condition
equipmentDef == EquipmentDef
we're going to be returning the result of a method that will check if we've activated our equipment and whatever behaviors we've defined on it. We'll be doing something similar to a proxy method of typebool
here, by having our method have anEquipmentSlot
parameter. We'll call itActivateEquipment
. It's easier done than said, so let's start by creating the method:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public EquipmentDef EquipmentDef; public virtual bool AppearsInSinglePlayer { get; } = true; public virtual bool AppearsInMultiPlayer { get; } = true; public virtual bool CanDrop { get; } = true; public virtual float Cooldown { get; } = 60f; public virtual bool EnigmaCompatible { get; } = true; public virtual bool IsBoss { get; } = false; public virtual bool IsLunar { get; } = false; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_NAME", EquipmentName); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP", EquipmentPickupDesc); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION", EquipmentFullDescription); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_LORE", EquipmentLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateEquipment() { EquipmentDef = ScriptableObject.CreateInstance<EquipmentDef>(); EquipmentDef.name = "EQUIPMENT_" + EquipmentLangTokenName; EquipmentDef.nameToken = "EQUIPMENT_" + EquipmentLangTokenName + "_NAME"; EquipmentDef.pickupToken = "EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP"; EquipmentDef.descriptionToken = "EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION"; EquipmentDef.loreToken = "EQUIPMENT_" + EquipmentLangTokenName + "_LORE"; EquipmentDef.pickupModelPrefab = EquipmentModel; EquipmentDef.pickupIconSprite = EquipmentIcon; EquipmentDef.appearsInSinglePlayer = AppearsInSinglePlayer; EquipmentDef.appearsInMultiPlayer = AppearsInMultiPlayer; EquipmentDef.canDrop = CanDrop; EquipmentDef.cooldown = Cooldown; EquipmentDef.enigmaCompatible = EnigmaCompatible; EquipmentDef.isBoss = IsBoss; EquipmentDef.isLunar = IsLunar; ItemAPI.Add(new CustomEquipment(EquipmentDef, CreateItemDisplayRules())); On.RoR2.EquipmentSlot.PerformEquipmentAction += PerformEquipmentAction; } private bool PerformEquipmentAction(On.RoR2.EquipmentSlot.orig_PerformEquipmentAction orig, RoR2.EquipmentSlot self, EquipmentDef equipmentDef) { if (equipmentDef == EquipmentDef) { } else { return orig(self, equipmentDef); } } protected abstract bool ActivateEquipment(EquipmentSlot slot); public abstract void Hooks(); } }
-
All that is left to do now is return our method inside of our condition we defined just a few moments back, and pass in the
self
parameter to it which is theEquipmentSlot
we need. Like so:using BepInEx.Configuration; using ROR2; using R2API; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Equipment { public abstract class EquipmentBase { public abstract string EquipmentName { get; } public abstract string EquipmentLangTokenName { get; } public abstract string EquipmentPickupDesc { get; } public abstract string EquipmentFullDescription { get; } public abstract string EquipmentLore { get; } public abstract GameObject EquipmentModel { get; } public abstract Sprite EquipmentIcon { get; } public EquipmentDef EquipmentDef; public virtual bool AppearsInSinglePlayer { get; } = true; public virtual bool AppearsInMultiPlayer { get; } = true; public virtual bool CanDrop { get; } = true; public virtual float Cooldown { get; } = 60f; public virtual bool EnigmaCompatible { get; } = true; public virtual bool IsBoss { get; } = false; public virtual bool IsLunar { get; } = false; public abstract void Init(ConfigFile config); protected void CreateLang() { LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_NAME", EquipmentName); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP", EquipmentPickupDesc); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION", EquipmentFullDescription); LanguageAPI.Add("EQUIPMENT_" + EquipmentLangTokenName + "_LORE", EquipmentLore); } public abstract ItemDisplayRuleDict CreateItemDisplayRules(); protected void CreateEquipment() { EquipmentDef = ScriptableObject.CreateInstance<EquipmentDef>(); EquipmentDef.name = "EQUIPMENT_" + EquipmentLangTokenName; EquipmentDef.nameToken = "EQUIPMENT_" + EquipmentLangTokenName + "_NAME"; EquipmentDef.pickupToken = "EQUIPMENT_" + EquipmentLangTokenName + "_PICKUP"; EquipmentDef.descriptionToken = "EQUIPMENT_" + EquipmentLangTokenName + "_DESCRIPTION"; EquipmentDef.loreToken = "EQUIPMENT_" + EquipmentLangTokenName + "_LORE"; EquipmentDef.pickupModelPrefab = EquipmentModel; EquipmentDef.pickupIconSprite = EquipmentIcon; EquipmentDef.appearsInSinglePlayer = AppearsInSinglePlayer; EquipmentDef.appearsInMultiPlayer = AppearsInMultiPlayer; EquipmentDef.canDrop = CanDrop; EquipmentDef.cooldown = Cooldown; EquipmentDef.enigmaCompatible = EnigmaCompatible; EquipmentDef.isBoss = IsBoss; EquipmentDef.isLunar = IsLunar; ItemAPI.Add(new CustomEquipment(EquipmentDef, CreateItemDisplayRules())); On.RoR2.EquipmentSlot.PerformEquipmentAction += PerformEquipmentAction; } private bool PerformEquipmentAction(On.RoR2.EquipmentSlot.orig_PerformEquipmentAction orig, RoR2.EquipmentSlot self, EquipmentDef equipmentDef) { if (equipmentDef == EquipmentDef) { return ActivateEquipment(self); } else { return orig(self, equipmentDef); } } protected abstract bool ActivateEquipment(EquipmentSlot slot); public abstract void Hooks(); } }
With that, we now have our ItemBase
and EquipmentBase
classes all set up! It's time to get down to creating our first item and equipment!
Now that we've defined both of our abstract classes that will serve as the template for our items/equipment, we can begin to use them. In the solution explorer, right click our Items
folder, Add -> Class
, and name it OmnipotentEgg
for example. Now let's get started.
-
In our newly generated class, add a
public
access modifier before the class name. It should look like:using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public class OmnipotentEgg { } }
-
Now, let's inherit from our ItemBase. In this example, we'll do this by adding a colon after the class name, followed by the abstract class name.
using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public class OmnipotentEgg : ItemBase { } }
-
At this point you'll notice the console outputting a ton of errors about us not implementing the abstract class' fields/properties.
Right click the class name OmnipotentEgg -> Quick Actions -> Implement Abstract Class
. -
Woah! Our class just filled up with a bunch of properties and fields! It also created usings at the top of our class! This is the result of our work in the last two sections. We can easily generate these just by inheriting the
ItemBase
andEquipmentBase
. First order of business, let's put these in order since it likely jumbled these around when we implemented it in the last step. It should look like this:using BepInEx.Configuration; using R2API; using RoR2; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public class OmnipotentEgg : ItemBase { public override string ItemName => throw new NotImplementedException(); public override string ItemLangTokenName => throw new NotImplementedException(); public override string ItemPickupDesc => throw new NotImplementedException(); public override string ItemFullDescription => throw new NotImplementedException(); public override string ItemLore => throw new NotImplementedException(); public override ItemTier Tier => throw new NotImplementedException(); public override GameObject ItemModel => throw new NotImplementedException(); public override Sprite ItemIcon => throw new NotImplementedException(); public override void Init(ConfigFile config) { throw new NotImplementedException(); } public override ItemDisplayRuleDict CreateItemDisplayRules() { throw new NotImplementedException(); } public override void Hooks() { throw new NotImplementedException(); } } }
-
Now let us think of what we want this item to do. For this tutorial, we'll make the egg fire out a projectile when we're damaged, but we need to think more in depth about that.
- How much damage should our projectile deal?
- Does it scale with our damage?
- Does it have a set amount of damage it does per stack of the item?
-
Once we've got these considerations and more all planned out, let's create some config entries for these. These will by default provide our own balance considerations for an item, but allow a user to configure it to their liking as well. After all of our properties, we'll define the fields that will store the values from our config entries. We'll do one for the damage of the projectile, and one for additional damage per stack. Both of these will be of type
Float
. Like so:using BepInEx.Configuration; using R2API; using RoR2; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public class OmnipotentEgg : ItemBase { public override string ItemName => throw new NotImplementedException(); public override string ItemLangTokenName => throw new NotImplementedException(); public override string ItemPickupDesc => throw new NotImplementedException(); public override string ItemFullDescription => throw new NotImplementedException(); public override string ItemLore => throw new NotImplementedException(); public override ItemTier Tier => throw new NotImplementedException(); public override GameObject ItemModel => throw new NotImplementedException(); public override Sprite ItemIcon => throw new NotImplementedException(); public float DamageOfMainProjectile; public float AdditionalDamageOfMainProjectilePerStack; public override void Init(ConfigFile config) { throw new NotImplementedException(); } public override ItemDisplayRuleDict CreateItemDisplayRules() { throw new NotImplementedException(); } public override void Hooks() { throw new NotImplementedException(); } } }
-
With these in place, we'll want a method to not only keep these organized, but also allow us to control when we bind the values from our config file to the fields we just made. For this, create a method
CreateConfig
and have one parameterConfigFile config
, then inInit
call that method with the parameterconfig
. TheInit
method itself will be getting aConfigFile
input into it later when we begin to modify the main class to initialize our items. Back on topic, our code should now look like:using BepInEx.Configuration; using R2API; using RoR2; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public class OmnipotentEgg : ItemBase { public override string ItemName => throw new NotImplementedException(); public override string ItemLangTokenName => throw new NotImplementedException(); public override string ItemPickupDesc => throw new NotImplementedException(); public override string ItemFullDescription => throw new NotImplementedException(); public override string ItemLore => throw new NotImplementedException(); public override ItemTier Tier => throw new NotImplementedException(); public override string ItemModelPath => throw new NotImplementedException(); public override string ItemIconPath => throw new NotImplementedException(); public float DamageOfMainProjectile; public float AdditionalDamageOfMainProjectilePerStack; public override void Init(ConfigFile config) { CreateConfig(config); } public void CreateConfig(ConfigFile config) { } public override ItemDisplayRuleDict CreateItemDisplayRules() { throw new NotImplementedException(); } public override void Hooks() { throw new NotImplementedException(); } } }
-
Next we'll be creating a
Config
binding usingconfig.Bind<Type>
and then grab its fieldValue
in one go.config.Bind<Type>
acts as both the writer and reader of a config file, and will update itself when new values are input into its generated config field, or if the default has changed. The overload we're going to use will consist of the following parts:-
section
- The category that this config option will appear under. I generally use either"Item: " + ItemName
or"Equipment:" + EquipmentName
which will place all of our config options for this in the same category and allow jumping to that category when using mod managers likeR2ModMan
. -
key
- This is the name of this individual config option. It can be a short description too, like"Damage of the Main Projectile"
. -
defaultValue
- This should be self-explanatory, but this is the value you want this option to have by default. For a config option of typefloat
this could be100.58f
. -
description
- This is the long description of what this config entry does. Generally, you ask a question in detail here. For example,"What should the base damage of the main projectile be?"
.
-
-
Having all this in mind, let's create the config bindings for our item, and assign them to our fields we just created.
using BepInEx.Configuration; using R2API; using RoR2; using System; using System.Collections.Generic; using System.Text; namespace MyModsNameSpace.Items { public class OmnipotentEgg : ItemBase { public override string ItemName => throw new NotImplementedException(); public override string ItemLangTokenName => throw new NotImplementedException(); public override string ItemPickupDesc => throw new NotImplementedException(); public override string ItemFullDescription => throw new NotImplementedException(); public override string ItemLore => throw new NotImplementedException(); public override ItemTier Tier => throw new NotImplementedException(); public override GameObject ItemModel => throw new NotImplementedException(); public override Sprite ItemIcon => throw new NotImplementedException(); public float DamageOfMainProjectile; public float AdditionalDamageOfMainProjectilePerStack; public override void Init(ConfigFile config) { CreateConfig(config); } public void CreateConfig(ConfigFile config) { DamageOfMainProjectile = config.Bind<float>("Item: " + ItemName, "Damage of the Main Projectile", 150f, "How much base damage should the projectile deal?").Value; AdditionalDamageOfMainProjectilePerStack = config.Bind<float>("Item: " + ItemName, "Additional Damage of Projectile per Stack", 100f, "How much more damage should the projectile deal per additional stack?").Value; } public override ItemDisplayRuleDict CreateItemDisplayRules() { throw new NotImplementedException(); } public override void Hooks() { throw new NotImplementedException(); } } }
-
The main reason we're doing the config entries first is so that we can use them in our description. By doing so, we can automatically update the description string should we or the user change the values around. Now let's fill out the language string fields. We'll do these one at a time, and focus specifically on the fields in question due to a little bit of text shenaniganery we're about to do. First things first, let's set the name of our Item. Do this by replacing the
throw new NotImplementedException();
with a string containing the name of the item, in this example"Omnipotent Egg"
.public override string ItemName => "Omnipotent Egg";
-
Next up, fill out the
ItemLangTokenName
similar to how you did the one above.public override string ItemLangTokenName => "OMNIPOTENT_EGG";
-
Next,
ItemPickupDesc
will be a short description of what the item does.public override string ItemPickupDesc => "Shoot a projectile out when you are damaged";
-
ItemFullDescription
which will be where we make our first use of theTextMeshPro
rich text style tags. Before that however, visit either link below to familiarize yourself with these: -
With that out of the way, let's proceed to fill out the full description. This is where we will go in detail with what the item does. Ours will list the condition in which our item activates, how much damage it does, and how much additional damage per stack it does. For this, we will make use of two of the style tags in the game.
<style=cIsDamage></style>
and<style=cStack></style>
. Stack information generally will be put into parenthesis after main info. Secondly, we'll be using the shorthand for String.Format before our description string$
which will allow us to use variables in the string via {field/variable/etc} (like the config fields we did earlier). Let's fill out that description now like so:public override string ItemFullDescription => $"When you are damaged, shoot out a projectile for <style=cIsDamage>{DamageOfMainProjectile}</style> <style=cStack>(+{AdditionalDamageOfMainProjectilePerStack}).";
-
Finally, lore is completely optional for an item, but people tend to like the extra polish that goes into creations. We'll just fill it out like so:
public override string ItemLore => "Since the dawn of man, one egg has always stood above the rest. This egg.";
-
Now we decide what tier we want this item to appear in. The effect is pretty good (identical to razorwire even), so green. That is to say,
ItemTier.Tier2
.public override ItemTier Tier => ItemTier.Tier2;
-
Load in the model and icon. To do so, first at the very top of the file add in a static using to our main class. This will allow us to use the static field for our asset bundle as if we were in the main class. Then to load things in, you can reference the asset bundle from the main class by using
MainAssets.LoadAsset<type>("filename.extension");
using BepInEx.Configuration; using R2API; using RoR2; using System; using System.Collections.Generic; using System.Text; using static MyModsNameSpace.MyMainClass;
public override GameObject ItemModel => MainAssets.LoadAsset<GameObject>("OmnipotentEgg.prefab"); public override Sprite ItemIcon => MainAssets.LoadAsset<Sprite>("OmnipotentEgg.png");
-
Now in our
Init
method, simply add aCreateLang();
underCreateConfig(config);
. This will initialize our Language Token registry after we bind the config values to the fields, resulting in a description that changes if the user changes their values. -
At this point your code should look similar to the following:
using BepInEx.Configuration; using R2API; using RoR2; using System; using System.Collections.Generic; using System.Text; using static MyModsNameSpace.MyMainClass; namespace MyModsNameSpace.Items { public class OmnipotentEgg : ItemBase { public override string ItemName => "Omnipotent Egg"; public override string ItemLangTokenName => "OMNIPOTENT_EGG"; public override string ItemPickupDesc => "Shoot a projectile out when you are damaged"; public override string ItemFullDescription => $"When you are damaged, shoot out a projectile for <style=cIsDamage>{DamageOfMainProjectile}</style> <style=cStack>(+{AdditionalDamageOfMainProjectilePerStack})."; public override string ItemLore => "Since the dawn of man, one egg has always stood above the rest. This egg."; public override ItemTier Tier => ItemTier.Tier2; public override GameObject ItemModel => MainAssets.LoadAsset<GameObject>("OmnipotentEgg.prefab"); public override Sprite ItemIcon => MainAssets.LoadAsset<Sprite>("OmnipotentEgg.png"); public float DamageOfMainProjectile; public float AdditionalDamageOfMainProjectilePerStack; public override void Init(ConfigFile config) { CreateConfig(config); CreateLang(); } public void CreateConfig(ConfigFile config) { DamageOfMainProjectile = config.Bind<float>("Item: " + ItemName, "Damage of the Main Projectile", 150f, "How much base damage should the projectile deal?").Value; AdditionalDamageOfMainProjectilePerStack = config.Bind<float>("Item: " + ItemName, "Additional Damage of Projectile per Stack", 100f, "How much more damage should the projectile deal per additional stack?").Value; } public override ItemDisplayRuleDict CreateItemDisplayRules() { throw new NotImplementedException(); } public override void Hooks() { throw new NotImplementedException(); } } }
-
We'll need to create a projectile for our item to fire, so let's create a method
CreateProjectile
with no return type. It will serve to keep our initilizations nice and tidy. Place this method underneathCreateConfig
.using BepInEx.Configuration; using R2API; using RoR2; using System; using System.Collections.Generic; using System.Text; using static MyModsNameSpace.MyMainClass; namespace MyModsNameSpace.Items { public class OmnipotentEgg : ItemBase { public override string ItemName => "Omnipotent Egg"; public override string ItemLangTokenName => "OMNIPOTENT_EGG"; public override string ItemPickupDesc => "Shoot a projectile out when you are damaged"; public override string ItemFullDescription => $"When you are damaged, shoot out a projectile for <style=cIsDamage>{DamageOfMainProjectile}</style> <style=cStack>(+{AdditionalDamageOfMainProjectilePerStack})."; public override string ItemLore => "Since the dawn of man, one egg has always stood above the rest. This egg."; public override ItemTier Tier => ItemTier.Tier2; public override GameObject ItemModel => MainAssets.LoadAsset<GameObject>("OmnipotentEgg.prefab"); public override Sprite ItemIcon => MainAssets.LoadAsset<Sprite>("OmnipotentEgg.png"); public float DamageOfMainProjectile; public float AdditionalDamageOfMainProjectilePerStack; public override void Init(ConfigFile config) { CreateConfig(config); CreateLang(); } public void CreateConfig(ConfigFile config) { DamageOfMainProjectile = config.Bind<float>("Item: " + ItemName, "Damage of the Main Projectile", 150f, "How much base damage should the projectile deal?").Value; AdditionalDamageOfMainProjectilePerStack = config.Bind<float>("Item: " + ItemName, "Additional Damage of Projectile per Stack", 100f, "How much more damage should the projectile deal per additional stack?").Value; } public void CreateProjectile() { } public override ItemDisplayRuleDict CreateItemDisplayRules() { throw new NotImplementedException(); } public override void Hooks() { throw new NotImplementedException(); } } }
-
When creating a projectile, there are a few paths you can go:
- You can create your own projectile from scratch, which takes considerable effort and experience.
- You can use a base game projectile, such as Commando's
FMJ
. - You can clone a base game projectile, and edit the properties of it safely. This is what we're going to do.
-
Below our fields and properties, put a new
public
static
field of typeGameObject
. Name itEggProjectile
. Like so:using BepInEx.Configuration; using R2API; using RoR2; using System; using System.Collections.Generic; using System.Text; using static MyModsNameSpace.MyMainClass; namespace MyModsNameSpace.Items { public class OmnipotentEgg : ItemBase { public override string ItemName => "Omnipotent Egg"; public override string ItemLangTokenName => "OMNIPOTENT_EGG"; public override string ItemPickupDesc => "Shoot a projectile out when you are damaged"; public override string ItemFullDescription => $"When you are damaged, shoot out a projectile for <style=cIsDamage>{DamageOfMainProjectile}</style> <style=cStack>(+{AdditionalDamageOfMainProjectilePerStack})."; public override string ItemLore => "Since the dawn of man, one egg has always stood above the rest. This egg."; public override ItemTier Tier => ItemTier.Tier2; public override GameObject ItemModel => MainAssets.LoadAsset<GameObject>("OmnipotentEgg.prefab"); public override Sprite ItemIcon => MainAssets.LoadAsset<Sprite>("OmnipotentEgg.png"); public float DamageOfMainProjectile; public float AdditionalDamageOfMainProjectilePerStack; public static GameObject EggProjectile; public override void Init(ConfigFile config) { CreateConfig(config); CreateLang(); } public void CreateConfig(ConfigFile config) { DamageOfMainProjectile = config.Bind<float>("Item: " + ItemName, "Damage of the Main Projectile", 150f, "How much base damage should the projectile deal?").Value; AdditionalDamageOfMainProjectilePerStack = config.Bind<float>("Item: " + ItemName, "Additional Damage of Projectile per Stack", 100f, "How much more damage should the projectile deal per additional stack?").Value; } public void CreateProjectile() { } public override ItemDisplayRuleDict CreateItemDisplayRules() { throw new NotImplementedException(); } public override void Hooks() { throw new NotImplementedException(); } } }
-
In
CreateProjectile
we'll use PrefabAPI'sInstantiateClone
clone a base game projectile without modifying the original via referencing it with an extensionless path. It requires two things:- A
GameObject
- In our example, this will be the projectile'sPrefab
we want to clone. - A
name
- What we want to call our new projectile code wise.
That
FMJ
referenced earlier will be the one we'll use for theGameObject
, and to use it we'll need to callResources.Load<Type>()
with the path leading there to it. For the name, we'll simply use"EggProjectile"
. - A
-
Done correctly, your
CreateProjectile
method should look like this now:public void CreateProjectile() { EggProjectile = PrefabAPI.InstantiateClone(Resources.Load<GameObject>("Prefabs/Projectiles/FMJ"); }
-
Let's change the damage type of our projectile now so that it slows whatever enemy it hits. We'll do this by calling
GameObject.GetComponent<Type>
on ourEggProjectile
and the type will beProjectile.ProjectileDamage
. Save this to a variabledamage
. -
Next, assign the
damageType
field ondamage
to a new damage type. In our case, we'll doDamageType.SlowOnHit
. YourCreateProjectile
should look similar to this now:public void CreateProjectile() { EggProjectile = PrefabAPI.InstantiateClone(Resources.Load<GameObject>("Prefabs/Projectiles/FMJ"); var damage = EggProjectile.GetComponent<Projectile.ProjectileDamage>(); damage = DamageType.SlowOnHit; }
-
Now that we've got the properties of our projectile set up, all we have to do now is register it and then add it to the projectile catalog. The first one is done with
PrefabAPI
'sRegisterNetworkPrefab
which accepts aGameObject
. The second one will be us adding to the list of projectiles using the newProjectileAPI
. Let's do so now:public void CreateProjectile() { EggProjectile = PrefabAPI.InstantiateClone(Resources.Load<GameObject>("Prefabs/Projectiles/FMJ"); var damage = EggProjectile.GetComponent<Projectile.ProjectileDamage>(); damage = DamageType.SlowOnHit; if (EggProjectile) PrefabAPI.RegisterNetworkPrefab(EggProjectile); ProjectileAPI.Add(EggProjectile); }
-
Back in the main class, add
PrefabAPI
andProjectileAPI
into yourR2APISubmoduleDependency
attribute as we've done multiple times now when we use one ofR2API
's submodules.[R2APISubmoduleDependency(nameof(LanguageAPI), nameof(ItemAPI), nameof(PrefabAPI)), nameof(ProjectileAPI)]
-
Now for the hellish bit,
ItemDisplayRules
. You can skip this over completely and just return an emptyItemDisplayRuleDict
in the methodCreateItemDisplayRules()
, or you can sit through this to have your items appear on characters. First things first, give yourCreateItemDisplayRules
method a body. Like so:public override ItemDisplayRuleDict CreateItemDisplayRules() { }
-
Create another
GameObject
field under your properties at the start of the class with thepublic
access modifier andstatic
keyword. Name it something likeItemBodyModelPrefab
. This field will be used to set up yourItemDisplay
, as well as theRendererInfos
for it. As an added bonus in our public class, this allows mod authors to easily add in yourItemDisplay
to their models by referencing it. -
We'll assign the field in our
CreateItemDisplayRules
method by using ourItemModel
we set up earlier. At this point your code should look similar to the following:using BepInEx.Configuration; using R2API; using RoR2; using System; using System.Collections.Generic; using System.Text; using static MyModsNameSpace.MyMainClass; namespace MyModsNameSpace.Items { public class OmnipotentEgg : ItemBase { public override string ItemName => "Omnipotent Egg"; public override string ItemLangTokenName => "OMNIPOTENT_EGG"; public override string ItemPickupDesc => "Shoot a projectile out when you are damaged"; public override string ItemFullDescription => $"When you are damaged, shoot out a projectile for <style=cIsDamage>{DamageOfMainProjectile}</style> <style=cStack>(+{AdditionalDamageOfMainProjectilePerStack})."; public override string ItemLore => "Since the dawn of man, one egg has always stood above the rest. This egg."; public override ItemTier Tier => ItemTier.Tier2; public override GameObject ItemModel => MainAssets.LoadAsset<GameObject>("OmnipotentEgg.prefab"); public override Sprite ItemIcon => MainAssets.LoadAsset<Sprite>("OmnipotentEgg.png"); public float DamageOfMainProjectile; public float AdditionalDamageOfMainProjectilePerStack; public static GameObject EggProjectile; public static GameObject ItemBodyModelPrefab; public override void Init(ConfigFile config) { CreateConfig(config); CreateLang(); } public void CreateConfig(ConfigFile config) { DamageOfMainProjectile = config.Bind<float>("Item: " + ItemName, "Damage of the Main Projectile", 150f, "How much base damage should the projectile deal?").Value; AdditionalDamageOfMainProjectilePerStack = config.Bind<float>("Item: " + ItemName, "Additional Damage of Projectile per Stack", 100f, "How much more damage should the projectile deal per additional stack?").Value; } public void CreateProjectile() { EggProjectile = PrefabAPI.InstantiateClone(Resources.Load<GameObject>("Prefabs/Projectiles/FMJ"); var damage = EggProjectile.GetComponent<Projectile.ProjectileDamage>(); damage = DamageType.SlowOnHit; if (EggProjectile) PrefabAPI.RegisterNetworkPrefab(EggProjectile); ProjectileAPI.Add(EggProjectile); } public override ItemDisplayRuleDict CreateItemDisplayRules() { ItemBodyModelPrefab = ItemModel; } public override void Hooks() { throw new NotImplementedException(); } } }
-
We'll need to add a component
ItemDisplay
to our ItemBodyModelPrefab so we can set upRenderInfos
for it to work properly with the overlay system in the game. Since that field is aGameObject
field, we can callAddComponent<Type>
on it to add ourItemDisplay
component. We'll also save the result in a variable.public override ItemDisplayRuleDict CreateItemDisplayRules() { ItemBodyModelPrefab = ItemModel; var itemDisplay = ItemBodyModelPrefab.AddComponent<ItemDisplay>(); }
-
Our next move will require us to generate
RendererInfos
for the meshes on ourItemBodyModelPrefab
. We'll be doing this per mesh but don't worry, it's an easy task I'll provide a helper method for free of charge. First things first, right click the CSProj file in your solution explorer (the green rectangle with a C# in it) and thenAdd -> Folder
. Name itUtils
. -
Right click the folder,
Add -> Class
. Name itItemHelpers
. -
Add the
public
access modifier to the front of the class. -
Add usings for
RoR2
andUnityEngine
at the top of the class. -
Finally, add in the following snippet to the class:
public static CharacterModel.RendererInfo[] ItemDisplaySetup(GameObject obj) { MeshRenderer[] meshes = obj.GetComponentsInChildren<MeshRenderer>(); CharacterModel.RendererInfo[] renderInfos = new CharacterModel.RendererInfo[meshes.Length]; for (int i = 0; i < meshes.Length; i++) { renderInfos[i] = new CharacterModel.RendererInfo { defaultMaterial = meshes[i].material, renderer = meshes[i], defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On, ignoreOverlays = false //We allow the mesh to be affected by overlays like OnFire or PredatoryInstinctsCritOverlay. }; } return renderInfos; }
The quick rundown of what this helper does is that when we call it, we pass in a
GameObject obj
. From it, we grab all of theMeshRenderer
inside of it and put them into aMeshRenderer
array. We then create aRendererInfo
array we'll iterate through of a length equal to how manyMeshRenderer
we have in theMeshRenderer
array. We then iterate through theMeshRenderer
array and create a newRendererInfo
for each iteration based on the mesh and its material we currently have the indexi
of. After we're done, we're returning theRendererInfo
array from the method, and it's set up from that point on. -
Back in your new item's class, add a static using for
MyModsNameSpace.Utils.ItemHelpers
. To do so, just add astatic
keyword after theusing
. -
Now in our
CreateItemDisplayRules
method, we'll use our newly created helper method by referencing the property on ouritemDisplay
variable and assigning it with our util method call. Like so:public override ItemDisplayRuleDict CreateItemDisplayRules() { ItemBodyModelPrefab = ItemModel; var itemDisplay = ItemBodyModelPrefab.AddComponent<ItemDisplay>(); itemDisplay.rendererInfos = ItemDisplaySetup(ItemBodyModelPrefab); }
-
Now we will create the
ItemDisplayRuleDict
that will store ourItemDisplayRule
arrays. We'll call itrules
. It will need a newItemDisplayRule
array, like so:public override ItemDisplayRuleDict CreateItemDisplayRules() { ItemBodyModelPrefab = ItemModel; var itemDisplay = ItemBodyModelPrefab.AddComponent<ItemDisplay>(); itemDisplay.rendererInfos = ItemDisplaySetup(ItemBodyModelPrefab); ItemDisplayRuleDict rules = new ItemDisplayRuleDict(new RoR2.ItemDisplayRule[] { }); }
-
As usual, we need to think about what an
ItemDisplayRule
's major components are that we need. This however varies between the two rule types ofItemDisplayRule
, which areItemDisplayRuleType.ParentedPrefab
andItemDisplayRuleType.LimbMask
. We will be usingItemDisplayRuleType.ParentedPrefab
and it has the following components:-
ruleType
- As established above, this is eitherItemDisplayRuleType.LimbMask
orItemDisplayRuleType.ParentedPrefab
. -
followerPrefab
- The model we seek to place onto the characters. This doesn't have to be your pickup model as we are doing in this tutorial, it can be any prefab. It however needs to have anItemDisplay
component andRendererInfos
set up for it. -
childName
- This is astring
referencing theChildLocator
transform pair we wish to attach ourfollowerPrefab
to. -
localPos
- This is the positional offset of our item from the origin of the child we have parented ourfollowerPrefab
to. -
localAngles
- This is the euler angle offset of our item from the base rotation of the child we have parented ourfollowerPrefab
to. (0-359 clamped, higher or lower values wrap around) -
localScale
- This is the size offset of our item from the base scale of the child we have parented ourfollowerPrefab
to.
-
-
Once the above considerations have been taken into account, decide what the
childName
you're parenting yourfollowerPrefab
to. These points can be found in any character'sCharacterModel
, under theChildLocator
component. For the purpose of the tutorial, we'll just useChest
as it is a commonly sharedchildName.
Create a zeroed outItemDisplayRule
inside of our emptyItemDisplayRule
array. Like so:public override ItemDisplayRuleDict CreateItemDisplayRules() { ItemBodyModelPrefab = ItemModel; var itemDisplay = ItemBodyModelPrefab.AddComponent<ItemDisplay>(); itemDisplay.rendererInfos = ItemDisplaySetup(ItemBodyModelPrefab); ItemDisplayRuleDict rules = new ItemDisplayRuleDict(new RoR2.ItemDisplayRule[] { new RoR2.ItemDisplayRule { ruleType = ItemDisplayRuleType.ParentedPrefab, followerPrefab = ItemBodyModelPrefab, childName = "Chest", localPos = new Vector3(0, 0, 0), localAngles = new Vector3(0, 0, 0), localScale = new Vector3(1, 1, 1) } }); }
This is a default rule given that we provide no string leading to a CharacterModel, thus it means this will apply to all characters that have a
childName
Chest
and don't have aChest
rule directly written for them. Zeroing it out like this will place it directly at the origin of the attach point with the rotation of the attach point. -
Adjustments from this point are up to you to do, but there are tools to make this process loads easier. When using the one I will link one below, you can search for your Prefab's name which will show
PrefabName(Clone)
. Clicking on it will place a question mark above the model, and if it is your model you can open theTransform
component on it. From there, you can adjust thelocalPos
,localAngles (sometimes localEuler)
, andlocalScale
fields until you have it looking as you want it. Once you do, just simply copy the values of those fields to the matching fields in yourItemDisplayRule
code to apply them properly. That said, the link to the tool is below:https://thunderstore.io/package/Twiner/RuntimeInspector/ - RuntimeInspector by Twiner
-
The only difference for our specific
CharacterModel
string rules is that we're adding them to our existingItemDisplayRuleDict rules
the same way we would add to a normal C#Dictionary
. That is to say,key, value
. The internals are functionally the same as what we've done in step 42. Like so:public override ItemDisplayRuleDict CreateItemDisplayRules() { ItemBodyModelPrefab = ItemModel; var itemDisplay = ItemBodyModelPrefab.AddComponent<ItemDisplay>(); itemDisplay.rendererInfos = ItemDisplaySetup(ItemBodyModelPrefab); ItemDisplayRuleDict rules = new ItemDisplayRuleDict(new RoR2.ItemDisplayRule[] { new RoR2.ItemDisplayRule { ruleType = ItemDisplayRuleType.ParentedPrefab, followerPrefab = ItemBodyModelPrefab, childName = "Chest", localPos = new Vector3(0, 0, 0), localAngles = new Vector3(0, 0, 0), localScale = new Vector3(1, 1, 1) } }); rules.Add("mdlHuntress", new RoR2.ItemDisplayRule[] { new RoR2.ItemDisplayRule { ruleType = ItemDisplayRuleType.ParentedPrefab, followerPrefab = ItemBodyModelPrefab, childName = "Chest", localPos = new Vector3(0, 0, 0), localAngles = new Vector3(0, 0, 0), localScale = new Vector3(1, 1, 1) } }); }
Make sense? It's as simple as that for the specific rules. On a closing note here, you can add more than one
ItemDisplayRule
to theItemDisplayRule
array on each of these by simply adding a comma after the rule and making anotherItemDisplayRule
like the two above. -
For the last part of the
CreateItemDisplayRules
method, we simply need toreturn rules;
Like so:public override ItemDisplayRuleDict CreateItemDisplayRules() { ItemBodyModelPrefab = ItemModel; var itemDisplay = ItemBodyModelPrefab.AddComponent<ItemDisplay>(); itemDisplay.rendererInfos = ItemDisplaySetup(ItemBodyModelPrefab); ItemDisplayRuleDict rules = new ItemDisplayRuleDict(new RoR2.ItemDisplayRule[] { new RoR2.ItemDisplayRule { ruleType = ItemDisplayRuleType.ParentedPrefab, followerPrefab = ItemBodyModelPrefab, childName = "Chest", localPos = new Vector3(0, 0, 0), localAngles = new Vector3(0, 0, 0), localScale = new Vector3(1, 1, 1) } }); rules.Add("mdlHuntress", new RoR2.ItemDisplayRule[] { new RoR2.ItemDisplayRule { ruleType = ItemDisplayRuleType.ParentedPrefab, followerPrefab = ItemBodyModelPrefab, childName = "Chest", localPos = new Vector3(0, 0, 0), localAngles = new Vector3(0, 0, 0), localScale = new Vector3(1, 1, 1) } }); return rules; }