User Tools

Site Tools


besiege:modding:example-guides:wardrum

This is an old revision of the document!


War Drum (WIP)

This guide demonstrates how to create a mod from start to finish. The mod created is a reduced version TheGuysYouDespise's War Music mod for the old mod loader.

Our version of the mod will contain only the War Drum, not the War Horn also included in the original version.

Additionally, some of the complicated code relating to displaying&simulating the shock wave will be omitted or just glossed over, the focus of this guide is how to work with the mod loader.

This is not a tutorial for complete beginners, it assumes familiarity with C# and some knowledge about how modding works. The purpose is an intro to working with the mod loader, using the War Drum as an example.

Creating the basic structure

We're going to start by creating a WarDrum directory inside the Mods directory in Besiege_Data.

Inside that, the following Mod.xml skeleton file is needed:

<Mod>
    <Name>War Drum</Name>
    <Author>TGYD, adapted for the tutorial by spaar</Author>
    <Version>1.0.0</Version>
    <Description>The War Drum is a medieval music instrument that can even knock stuff over!</Description>
 
    <MultiplayerCompatible>false</MultiplayerCompatible>
</Mod>

```

This is a basic file that's enough to get the mod loader by the game. For more information on these elements, see Mod Manifest.

Of course when developing your own mod, you'd replace the appropriate values.

Notice how MultiplayerCompatible is set to false right now, we're going to change that later once we actually add in multiplayer support. Of course you can keep this on from the beginning if having an sp-only mod is annoying and you know the limitations.

Resources

Next, we're going to add the resources required for the drum. We won't discuss 3D modelling or texturing here, so we're just going to take these as a given.

TODO: Add the resources for following along?

The resources need to be placed in a Resources/ directory next to the Mod.xml file.

After placing the resources in the correct place, we're going to add them to the manifest:

<Mod>
    ...
 
    <Resources>
        <Mesh name="drum-mesh" path="drum.obj" />
        <Texture name="drum-texture" path="DrumTexture.png" />
        <Texture name="shock-bump" path="DrumShockBump.png" />
        <AudioClip name="drum-sound" path="WarDrumSound.ogg" />
    </Resources>
</Mod>

```

This enables us to reference these resources as mesh and texture for the block later, or, in the case of the sound and the bump texture, access them from code.

For more information on resources, see Resources.

Defining the block

Now to the interesting part: Defining the properties of our block.

First, add a reference to it to the manifest:

<Mod>
    ...
    <Blocks>
        <Block path="Drum.xml" />
    </Blocks>
...
</Mod>

Now add the basic structure to the Drum.xml file:

<Block>
    <ID>1</ID>
    <Name>War Drum</Name>
    <Mass>0.5</Mass>
    <CanFlip>false</CanFlip>
 
    <Health>3</Health>
 
    <Mesh name="drum-mesh">
        <!-- The game expects the mesh in a different orientation to what we exported it in,
             so just rotate it here. -->
        <Rotation x="90" y="0" z="0" />
    </Mesh>
 
    <Texture name="drum-texture" />
 
    <Icon>
        <Position x="0.1" y="-0.1" z="-0.05" />
        <Rotation x="-66" y="45" z="-14" />
        <Scale x="0.26" y="0.26" z="0.26" />
    </Icon>
 
    <Colliders>
 
    </Colliders>
 
    <AddingPoints>
 
    </AddingPoints>
</Block>

We're still missing our colliders and adding points, these are listed below to keep the above snippet readable.

Note that normally one wouldn't just know the transform values for the Icon element or the colliders and adding points of course. The exact values are usually determined through trial-and-error.

Take a look at Debug Mode for a technique that makes this trial-and-error process much less painful.

Now though, the colliders and adding points:

<BoxCollider>
    <Position x="0" y="0" z="0.9" />
    <Rotation x="0" y="0" z="0" />
    <Scale x="1" y="1" z="1" />
</BoxCollider>
 
<BoxCollider>
    <Position x="0.000" y="0.899" z="0.833" />
    <Rotation x="-8.5" y="0" z="0" />
    <Scale x="1.2" y="1" z="1" />
</BoxCollider>
<BoxCollider>
    <Position x="0.636" y="0.636" z="0.833" />
    <Rotation x="-6" y="6" z="-45" />
    <Scale x="1.2" y="1" z="1" />
</BoxCollider>
<BoxCollider>
    <Position x="0.899" y="0.000" z="0.833" />
    <Rotation x="0" y="8.5" z="-90" />
    <Scale x="1.2" y="1" z="1" />
</BoxCollider>
<BoxCollider>
    <Position x="0.636" y="-0.636" z="0.833" />
    <Rotation x="6" y="6" z="-135" />
    <Scale x="1.2" y="1" z="1" />
</BoxCollider>
<BoxCollider>
    <Position x="0.000" y="-0.899" z="0.833" />
    <Rotation x="8.5" y="0" z="-180" />
    <Scale x="1.2" y="1" z="1" />
</BoxCollider>
<BoxCollider>
    <Position x="-0.636" y="-0.636" z="0.833" />
    <Rotation x="6" y="-6" z="-225" />
    <Scale x="1.2" y="1" z="1" />
</BoxCollider>
<BoxCollider>
    <Position x="-0.899" y="0.000" z="0.833" />
    <Rotation x="0" y="-8.5" z="-270" />
    <Scale x="1.2" y="1" z="1" />
</BoxCollider>
<BoxCollider>
    <Position x="-0.636" y="0.636" z="0.833" />
    <Rotation x="-6" y="-6" z="-315" />
    <Scale x="1.2" y="1" z="1" />
</BoxCollider>
 
<BoxCollider>
    <Position x="0.0" y="0.75" z="0.55" />
    <Rotation x="-45" y="0" z="0" />
    <Scale x="1" y="1" z="0.5" />
</BoxCollider>
<BoxCollider>
    <Position x="0.75" y="0.0" z="0.55" />
    <Rotation x="0" y="45" z="-90" />
    <Scale x="1" y="1" z="0.5" />
</BoxCollider>
<BoxCollider>
    <Position x="0.0" y="-0.75" z="0.55" />
    <Rotation x="45" y="0" z="-180" />
    <Scale x="1" y="1" z="0.5" />
</BoxCollider>
<BoxCollider>
    <Position x="-0.75" y="0.0" z="0.55" />
    <Rotation x="0" y="-45" z="-270" />
    <Scale x="1" y="1" z="0.5" />
</BoxCollider>
 
<BoxCollider trigger="true" layer="2">
    <Position x="0.0" y="0.0" z="1.15" />
    <Rotation x="0" y="0" z="0" />
    <Scale x="2.25" y="2.25" z="1.0" />
</BoxCollider>

The last collider will be used to activate the drum when something hits it.

<AddingPoint>
    <Position x="0.0" y="-1" z="0.6" />
    <Rotation x="-90" y="0" z="0" />
</AddingPoint>
<AddingPoint>
    <Position x="0.9" y="-0.48" z="0.6" />
    <Rotation x="210" y="90" z="0" />
</AddingPoint>
<AddingPoint>
    <Position x="0.9" y="0.48" z="0.6" />
    <Rotation x="150" y="90" z="0" />
</AddingPoint>
<AddingPoint>
    <Position x="0.0" y="1" z="0.6" />
    <Rotation x="90" y="0" z="0" />
</AddingPoint>
<AddingPoint>
    <Position x="-0.9" y="0.48" z="0.6" />
    <Rotation x="30" y="90" z="0" />
</AddingPoint>
<AddingPoint>
    <Position x="-0.9" y="-0.48" z="0.6" />
    <Rotation x="330" y="90" z="0" />
</AddingPoint>

And lastly, add a BasePoint element:

<BasePoint hasAddingPoint="false">
    <Motion x="false" y="false" z="false" />
    <Stickiness enabled="true" radius="0.5" />
</BasePoint>

For more information on any of these elements, see the Block element.

Adding behaviour

The mod can now be loaded and the block should appear in-game correctly. However, it won't do anything yet, which doesn't make for a very interesting block.

We'll need to set up something that allows us to write custom code for the game, as described in Custom Code, for example a Visual Studio project with the appropriate references.

With that set up, first add the assembly to the mod manifest:

<Mod>
    ...
    <Assemblies>
        <Assembly path="WarDrum.dll" />
    </Assemblies>
    ...
</Mod>

That'll cause the assembly to be recognized by the mod loader. Now, specify the class that will serve as behaviour for the block in Drum.xml:

<Block>
    ...
    <Script>WarDrum</Script>
    ...
</Blocks>

The name specified could also include a namespace to make the code more organized, but we won't do that here since there aren't any other classes.

Now, here's the actual code. Some of it is omitted for brevity. The code is explained using comments, especially where it relates directly to the mod loader.

using System;
using System.Collections.Generic;
using Modding;
using UnityEngine;
 
// BlockScript is the base-class for all behaviours of blocks.
public class WarDrum : BlockScript {
    // For the sound played when the drum is activated
    private AudioSource audioSource;
    private bool hasSound;
 
    // The elements displayed in the block mapper for a war drum
    private MKey activateKey;
    private MSlider powerSlider;
    private MToggle shockwaveToggle;
 
    public override void SafeAwake() {
        // Create the needed elements in the block mapper.
        //               Display Name, unique key, default value
        activateKey = AddKey("Strike", "strike", KeyCode.B);
        //              Display Name, unique key, default, min, max
        powerSlider = AddSlider("Power", "power", 1f, 0.5f, 2f);
        //                        Display Name, unique key, default
        shockwaveToggle = AddToggle("Shockwave", "display", true);
    }
 
    // Called once when the prefab is initially created.
    public override void OnPrefabCreation() {
        // Create the shockwave prefab
        CreateShockWavePrefab();
    }
 
    // Called whenever the simulation is started.
    public override void OnSimulateStart() {
        // If it was loaded successfully, set up an audio source with our clip from the
        // resources section.
        var res = ModResource.GetAudioClip("drum-sound");
        if (res.Available) {
            hasSound = true;
            audioSource = GetComponent<AudioSource>();
            if (!audioSource) audioSource = gameObject.AddComponent<AudioSource>();
            audioSource.clip = res.AudioClip;
        }    
    }
 
    // Called every frame while simulating.
    public override void SimulateUpdate() {
        // These properties are defined in BlockScript
        if (!HasBurnedOut && !IsDestroyed) {
            if (activateKey.IsPressed) {
                StrikeDrum();
            }
        }
    }
 
    // Called when a trigger collision happens during simulation
    public override void OnSimulateTriggerEnter(Collider collision) {
        // Remember the trigger collider we specified to activate the drum when it was hit
        // by something? Do that here!
 
        if (<some checks to see if the collision should trigger the drum>) {
            StrikeDrum();
        }
    }
 
    // Actually strike the drum!
    public void StrikeDrum() {
        // Some of these methods are pretty complicated, but they're not concerned with
        // the mod loader, they could work the same way in Unity in general.
        // That's why we omitted their actual implementation here.
        AnimateDrum();
        ForceWave();
        if (hasSound)
            PlaySound();
        if (shockwaveToggle.IsActive)
            DisplayShockWave();
    }
}

Great! This should be enough to make the drum work in singleplayer games. But what good is a block you can't use with your friends? Let's add some networking glue to make the drum work in the Multiverse.

The basic concept behind multiplayer in Besiege is that the simulation is only performed by one game instance and its effects are transmitted to all others (or not, in the case of local simulation).

So the first thing to ensure, is that all of the code handling user input, physics and so on is only executed on the instance running the current simulation.

We're in luck though, BlockScript ensures that methods like OnSimulateStart, SimulateUpdate, etc are only called on the correct instance in multiplayer. (There are also Client variants of these in case you do need to execute some code on all machines.)

So, we're not doing anything we shouldn't be doing right now. However this also means that the visual effects of striking the drum, as well as the sound, only happen on the instance running simulation right now, that's not what we want! We're going to have to transmit these effects to other player manually.

First things first, the Networking documentation contains the basic information about the APIs we're going to use here, without a rough idea of how it works, it's going to be hard to follow along.

According to that, the first step is to define the message types we're going to need. To do so, we're actually going to create to more classes: Add ModEntryPoint to define the message types when the mod is loaded, and a Messages class that makes accessing them from the drum code easy.

public static class Messages {
    public static MessageType DrumStrike;
}
 
public class WarDrumMod : ModEntryPoint {
    // This is called when the mod is first loaded.
    public override void OnLoad() {
        Messages.DrumStrike = ModNetworking.CreateMessageType(DataType.Block);
        ModNetworking.Callbacks[Messages.DrumStrike] += message => {
            var block = (Block) message.GetData(0);
            var drum = block.SimBlock.GameObject.GetComponent();
            drum.StrikeDrumLocal();
        };
    }
}

Now, we're going to have to add that StrikeDrumLocal method and actually send some network messages.

public class WarDrum : BlockScript {
    ...
 
    public void StrikeDrumLocal() {
        AnimateDrum();
        // Note the missing ForceWave() here! The physics effect should only be performed
        // on the instance running the simulation.
        if (hasSound)
            PlaySound();
        if (shockwaveToggle.IsActive)
            DisplayShockWave();
    }
 
    public void StrikeDrum() {
        StrikeDrumLocal();
        ForceWave();
 
        // Send the message to all other instance participating in the same simulation.
        var msg = Messages.DrumStrike.CreateMessage(Block.From(this));
        ModNetworking.SendInSimulation(msg);
    }
}

And lastly, actually mark the mod as multiplayer compatible in the manifest, otherwise the game will refuse to load it in a multiplayer session.

<Mod>
    ...
    <MultiplayerCompatible>true</MultiplayerCompatible>
    ...
</Mod>

And that's it! The war drum should now be fully functioning in both single- and multiplayer.

For the complete mod, including the omitted code, see TODO.

besiege/modding/example-guides/wardrum.1531234500.txt.gz · Last modified: 2018/07/10 16:55 by spaar