Developing Mods - Example Power Up



Table of Contents

About This Guide

Moddding Overview

Tools & Software

Reading Decompiled Code

Creating Custom Assets in Unity

C# Mod Development

Advanced / Useful Subjects

Example Project Source



About This Guide

I find it's much easier to learn by example. This document will largely follow a tutorial format. However it is not meant to be a tutorial, it's meant to be a reference for the overall work flow and how to get started. At the end of which you should be to create modifications for Jet Island.

This document was written with the expectation that you at least have a cursory knowledge of the following.

  • C# Programming Language
  • Unity 3D Game Engine

Knowing very little is okay, but the more you know the better!

YouTube is a great source for Unity Tutorials!

If you need help developing a mod, please join the Jet Island Discord and ask in #modding



Modding Overview

It helps to have an understanding of what is going on at a high level. Further digging into detail as we go along.

Modding a Unity game like Jet Island is very similar to Unity development in general. You just don't have access to the games Unity project in the editor.

IPA is used to inject code into Jet Island, from there you have access to the entire Unity engine, as well as direct access to much of the games own code as well.

To import custom assets (3D models, sounds, etc.) into Jet Island. We use the same version of Unity the game was built with. It is used to create Prefabs, which are then export as AssetBundles. Since it's possible to inject our own code with IPA. All you have to do is write code to load those asset bundles. Add logic to them, and do whatever is necessary for your own mod.

Most mods will be split into two separate projects. A Unity project that contains it's custom assets/prefabs, and a C# project that contains an IPA Plugin and code the mod requires to function.

Also make sure you've Setup IPA already with Jet Island from the How to Install page



Tools & Software

Software

  • Visual Studio 2017 Community Edition - Other versions should work but are untested

  • Unity 3D 2017.1.1 - Only required if you wish to deal with custom assets, which this example mod does. Other versions may not work and are untested.

  • dnSpy - To decompile the game's code and figure out what we need to modify

Useful Tools

  • uTinyRipper - Tool to extract various Unity game assets

  • 3D Modeling software if your mod requires custom models (Blender, Maya, 3ds Max)

  • Image / Texture editing software, if your mod requires custom images or textures (GIMP, Photoshop, Paint .net)

  • Audio Editing software, if your mod requires custom sounds. (Audacity)



Reading Decompiled Code

  • Open JetIsland_Data\Managed\Assembly-CSharp.dll with dnSpy

  • Look under Assembly-CSharp (0.0.0.0) > Assembly-CSharp.dll > - (just a single dash) > the game's code.

There really isn't much to explain in this step. In essence you just have browse through the code, become familiar with it. Figure out how to accomplish what it is you want your mod to do, etc...

Many of the classes aren't too important, in terms of modding.

A great place to start is the PlayerBody class. Much is handled there.

(More to come in this section?)



Creating Custom Assets in Unity

Setting Up the Project

  • Create a new Unity Project as you normally would. Make sure you're using 2017.1.1!

  • Adjust the Projects VR Settings

    • Go to Edit > Project Settings > Player. Then enable VR support as well as setting the Rendering Method to Single Pass


Asset Bundle Exporter

  • Create the following directory structure in your project Assets\StreamingAssets\AssetBundles\

  • Create a new folder in the Assets directory called Editor, in that folder create a new Script called ExportAssetBundle.cs with the following code

using System.IO;
using UnityEditor;
using UnityEngine;
public class ExportAssetBundle
{
    [MenuItem("Assets/Build AssetBundle")]
    static void ExportResource()
    {
        string folderName = "AssetBundles";
        string filePath = Path.Combine(Application.streamingAssetsPath, folderName);
        BuildPipeline.BuildAssetBundles(filePath, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);
        AssetDatabase.Refresh();
    }
}

Building An AssetBundle

  • Import any assets your mod requires into the project. In this case I have a custom mesh I'll be importing. It looks like a cheesy early-2000's video game power up that was made in 5 minutes : )

  • Create a new Prefab just like you would as if you were developing a Unity game.

  • Build up the Prefab in the Editor just as you would any other Prefab, here I have a MeshRenderer, some Materials, and a Particle Emitter. However do not add any Scripts we will do that in a C# project later

  • Update the Prefab by dragging it from the Hierarchy onto the Prefab Asset

  • Select the Prefab Asset and assign it to a new AssetBundle

  • Name the AssetBundle whatever-you-like.unity3d

  • Once your Prefab is built, and assigned to an AssetBundle, it's now time to export it. Go to Assets > Build AssetBundle

Note: any time you need to update your AssetBundle, make sure you also update the Prefab as well by dragging it from the Hierarchy again!

  • You now have your custom AssetBundle in Assets\StreamingAssets\AssetBundles\whatever-you-like.unity3d that can be copied to JetIsland_Data\StreamingAssets\ It's ready to be loaded in game!


C# Mod Development

Setting Up the Project

  • Create a new C# Class Library project

  • Add the references required to develop a IPA Plugin, as well as the Unity engine, and the games own Assembly

  • Add the references as shown in the image below, they all should be in JetIsland_Data\Managed\

Note: If you need to work with the UI, TextMeshPro, or Photon, add those as well!


Code Overview

  • Mods must implement the IPA IPlugin interface, it's much easier than it sounds. Take a look at the Enable Rotation Mod Source it is about as simple as it gets!

  • Below are some snippets from this specific mod you may find interesting. The entire project is well commented and available on GitHub. These snippets are from Plugin.cs

  • How to load AssetBundles and Prefabs, many of the class fields dealing with the engine itself are of type GameObject

public void Init()
{
    //Load our asset bundle.
    AssetBundle assetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "rocketpower.unity3d"));

    //Load the prefab we created in Unity.
    _rocketPowerupPrefab = assetBundle.LoadAsset<GameObject>("RocketPowerPrefab");

    //Unload the asset bundle without destroying assets from it. Keeps them loaded in the engine for further use.
    assetBundle.Unload(false);

    //Spawn the power up.
    SpawnRocketPowerUp();
}
  • How to spawn and position Prefabs in the game world.
public void SpawnRocketPowerUp()
{
    //Check if our object already exists to prevent spawning it multiple times.
    if (_rocketPowerup != null)
        return;

    //Create and position an instance of the prefab we loaded earlier.
    _rocketPowerup = UnityEngine.Object.Instantiate(_rocketPowerupPrefab);

    //This position is right in front of the space ship in the tutorial area.
    _rocketPowerup.transform.position = new Vector3(-85.8f, 121.0f, 9561.8f);

    //Add our MonoBehaviour from PowerUp.cs to the spawned game object, this is basically the same as adding a script to it in the
    //Unity Editor. We have to do it this way because asset bundles can't contain code. 
    //(Not entirely true, but this way is easier in my opinion)
    _rocketPowerup.AddComponent<PowerUp>();
}
  • How to destroy objects we create & interact with the games own code
public void PickupPowerup()
{
    //Destroy our object and set its reference to null;
    UnityEngine.GameObject.DestroyImmediate(_rocketPowerup);
    _rocketPowerup = null;

    //PowerUp will last for 10 seconds.
    _powerupDuration = Time.time + 10f;

    //PowerUp will respawn after 15 seconds;
    _powerupRespawnTime = Time.time + 15f;

    //Save the original jet force so we can set it back later.
    _originalJetForce = PlayerBody.localPlayer.movement.jetForce;

    //Set our new jet force.
    PlayerBody.localPlayer.movement.jetForce = _powerupJetForce;

    //Display a message informing the powerup has been collected.
    PlayerBody.localPlayer.DisplayMessageInFrontOfPlayer("Rocket Power!");

    _powerupActive = true;
}

Attaching Code to GameObjects

In Unity you may be used to just adding a Script component, however with this approach you need to create your classes in the C# project and load them at run-time. You can inherit from MonoBehaviour just like you would in Unity itself.

  • Create a new C# Class

  • The following is the code for our PowerUp, this is just as if it was written for a Unity project. Notice how it inherits from MonoBehaviour. All it does is spin around and bob up and down, in a traditional power up style : )
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace JetIslandPowerupsPlugin
{
    //This class is just like any script you would attach to a game object in Unity. Notice how it inherits from MonoBehaviour.
    class PowerUp : MonoBehaviour
    {
        private float _yOffset = 0f;

        void Start()
        {
        }

        //Spin to win!
        void Update()
        {
            gameObject.transform.Rotate(new Vector3(0f, 90f * Time.deltaTime, 0f));

            _yOffset = Mathf.Sin(Time.time * 2f) * 0.5f * Time.deltaTime;
            gameObject.transform.position += new Vector3(0f, _yOffset, 0f);
        }
    }
}
  • To attach it to our GameObject, all we do is the following. Very simple.
_rocketPowerup.AddComponent<PowerUp>();


Advanced / Useful Subjects

Reloading Assets During Run-Time

It is possible to reload AssetBundles during run-time, this allows for quick iteration if your mod is asset-heavy, like a custom level for instance.

pubic void OnUpdate()
{
    if (Input.GetKeyDown(KeyCode.R))
    {
        if (myObject)
            UnityEngine.GameObject.DestroyImmediate(myObject);

        if(!myObject)
        {
            AssetBundle assetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "my-bundle.unity3d"));
            GameObject prefab = assetBundle.LoadAsset<GameObject>("MyPrefab");
            myObject = UnityEngine.Object.Instantiate(prefab);
            myObject.transform.position = new Vector3(0f, 0f, 0f);
            assetBundle.Unload(false);
        }
    }
}

Using Reflection

It is possible to edit protected & private fields as well as call methods in the games own code using reflection. A great class for this is ReflectionUtil.cs


Hooking Methods

Care must be taken hooking methods, if multiple mods hook the same method it will create a conflict that could lead to undetermined results

A very useful library for hooking methods is PlayHooky. However it's largest downside is that it can not call the original method.

To do that using this library.

  • You must hook the method
  • Do what you need to do pre-hook
  • Unhook the method
  • Call the original method
  • Do what you need to do post-hook
  • Then finally re-hook the method.

It isn't the best approach, but it works just fine.



Example Project Source

Here you will find links to the source code for everything in this example mod.