Working with Unity objects
Most objects you interact with in Mycopunk are Unity GameObject instances with one or more components attached. Game classes such as UpgradePopup, Player, and Jukebox inherit from MonoBehaviour, either directly or through another class.
GameObjects, components, and transforms
A GameObject is a container. Components provide its behavior, and its Transform stores its position, rotation, scale, and parent.
Every component provides access to both:
GameObject owner = component.gameObject;
Transform transform = component.transform;Positions can be in world space or local space:
transform.position = new Vector3(0f, 2f, 0f);
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;position is relative to the world. localPosition is relative to the object's parent.
Finding components
If you already have a GameObject, ask it for the component you need:
if (gameObject.TryGetComponent<Renderer>(out var renderer))
{
Logger.LogInfo($"Found renderer: {renderer.name}");
}Other common searches are:
Collider collider = gameObject.GetComponent<Collider>();
AudioSource audio = gameObject.GetComponentInChildren<AudioSource>();
Renderer parentRenderer = gameObject.GetComponentInParent<Renderer>();These methods return null when no matching component exists. TryGetComponent is convenient when the component is optional.
Avoid broad searches such as FindObjectOfType inside Update. If you must search for an object, do it once and store the result. A direct reference from a Harmony argument or an existing component is usually safer than searching the whole scene.
Inspecting the running game with UnityExplorer
UnityExplorer is an in-game inspector for Unity objects. It shows the scenes, GameObject instances, components, and values that exist while Mycopunk is running. This makes it useful for finding an object when you only know what it looks like or what it does in-game.
WARNING
Use the linked AtlyssModding fork for Mycopunk. The game uses Unity 6, which the original UnityExplorer release does not support correctly. The fork contains a fix for Unity 6 scene handles.
Install the Unity 6 fork
The compatible build is available as Atlyss UnityExplorer on Thunderstore. The package is listed under the ATLYSS community, so install it manually into Mycopunk:
- Select Manual Download on the Thunderstore page.
- Extract the downloaded archive.
- Copy its
plugins/sinai-dev-UnityExplorerdirectory into theBepInEx/pluginsdirectory used by Mycopunk or your active mod-manager profile. - Launch the game with BepInEx and press
F7to show or hide UnityExplorer.
The installed files should look like this:
BepInEx/plugins/sinai-dev-UnityExplorer/
├── UnityExplorer.BIE5.Mono.dll
└── UniverseLib.Mono.dllUnityExplorer stores its settings in:
BepInEx/config/com.sinai.unityexplorer.cfgThe toggle key and startup behavior can be changed there.
Find an object at runtime
UnityExplorer provides several ways to locate game objects:
- Scene Explorer displays the hierarchy of loaded scenes, including objects under
DontDestroyOnLoad. - Object Search searches for
GameObjectinstances, components, static classes, and common singletons. - Mouse Inspect selects a world or UI object under the pointer.
- Inspector displays an object's transform, components, fields, properties, and methods.
A useful workflow is:
- Open the screen or trigger the object you want to study.
- Find it with Scene Explorer, Object Search, or Mouse Inspect.
- Inspect its components and note their type names.
- Expand the relevant component and inspect its fields and properties.
- Change a harmless value to confirm which object controls the behavior.
- Record the object path, component type, and member names you need for the mod.
For example, after an item pickup appears, search for an active UpgradePopup. Its inspector shows the popup's Transform, attached components, and references such as nameText, rarityText, and icon.
Treat runtime edits as temporary
Values changed through UnityExplorer usually reset when the object is recreated, the scene changes, or the game closes. Editing the wrong field or invoking a method manually can also leave the current session in a broken state.
Use it in a private test session, especially when inspecting networking or gameplay objects. Do not assume that a local value changed through the inspector is synchronized with other players or accepted by the server. UnityExplorer is a development tool and does not need to be included with your released mod.
Creating objects and components
Create a GameObject, give it a parent, and attach one of your own components with AddComponent:
private GameObject _modRoot;
private void CreateModObjects()
{
_modRoot = new GameObject("MyMod");
_modRoot.transform.SetParent(transform, worldPositionStays: false);
_modRoot.AddComponent<MyModBehaviour>();
}A custom component must inherit from MonoBehaviour:
internal sealed class MyModBehaviour : MonoBehaviour
{
private void Start()
{
Plugin.Logger.LogInfo("MyModBehaviour started");
}
}Do not create a MonoBehaviour with new. Unity needs to construct and attach it through AddComponent.
Use Instantiate when you have an existing object or prefab to copy:
GameObject copy = Instantiate(prefab, parent);
copy.name = "MyModCopy";Only instantiate a prefab after the game has created it and while its reference is valid.
Lifecycle methods
Unity calls component methods at different points in its lifetime:
| Method | Typical use |
|---|---|
Awake | Initialize fields and code that does not depend on other scene objects. |
OnEnable | Subscribe to events or enable behavior. May run more than once. |
Start | Use other objects that finished their Awake methods. |
Update | Read state or update visuals once per rendered frame. |
FixedUpdate | Work with physics at the fixed simulation interval. |
LateUpdate | Follow objects after their normal Update work is finished. |
OnDisable | Pause behavior and remove subscriptions made in OnEnable. |
OnDestroy | Release resources and remove remaining subscriptions. |
A BepInEx plugin is also a MonoBehaviour, but its Awake can run before the player and other game objects exist. Do not assume that a singleton such as Player.LocalPlayer is ready during plugin startup.
Waiting with a coroutine
Coroutines split work across frames without blocking the game. A plugin can start them because BaseUnityPlugin inherits from MonoBehaviour:
using System.Collections;
private Coroutine _waitForPlayerRoutine;
private void Awake()
{
_waitForPlayerRoutine = StartCoroutine(WaitForPlayer());
}
private IEnumerator WaitForPlayer()
{
while (Player.LocalPlayer == null)
yield return null;
Logger.LogInfo("The local player is ready");
_waitForPlayerRoutine = null;
}yield return null resumes the coroutine on the next frame. For a delay measured in game time, use WaitForSeconds:
yield return new WaitForSeconds(1f);Stop a long-running coroutine when it is no longer needed:
if (_waitForPlayerRoutine != null)
{
StopCoroutine(_waitForPlayerRoutine);
_waitForPlayerRoutine = null;
}Do not use Thread.Sleep for delays. It blocks the game thread and freezes the game.
Reacting to scene changes
Objects from a level are normally destroyed when its scene unloads. Subscribe to Unity's scene event if your mod needs to find or create objects after each scene load:
using UnityEngine.SceneManagement;
private void Awake()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
private static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
Logger.LogInfo($"Loaded scene: {scene.name}");
}
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}The scene event does not guarantee that every gameplay object is ready. If a required object is still missing, wait for it with a coroutine or patch the method that creates it.
Unity's null behavior
Unity objects have special null handling. An object can still have a managed C# reference after Unity destroys its native object, but it compares equal to null:
if (_target == null)
return;Check stored references before using them, especially after a scene change. A destroyed object can throw MissingReferenceException when you access it.
Per-frame work
Code in Update may run dozens or hundreds of times per second. Keep it small:
private void Update()
{
transform.Rotate(0f, 90f * Time.deltaTime, 0f);
}Multiplying by Time.deltaTime makes movement independent of the frame rate. Avoid repeated logging, file access, object searches, LINQ queries, and unnecessary allocations in per-frame methods.
Use FixedUpdate for physics work involving Rigidbody. Use LateUpdate when an object needs to follow another object after it has moved.
Clean up what the mod owns
Destroy objects created by the mod and release subscriptions when the plugin unloads:
private void OnDestroy()
{
if (_modRoot != null)
Destroy(_modRoot);
}Clean up event handlers, input actions, coroutines, and any objects the mod created. Do not destroy game-owned objects unless removing that object is the intended feature.
Unity APIs should normally be called from the main game thread. Background tasks can prepare ordinary data, but object creation, component access, and scene changes should return to the Unity thread first.
Continue with registering keybinds or exploring the game code when your mod needs to interact with a specific game system.