FAQ & Common Issues - Gorzontrok/Bro-Maker GitHub Wiki

Table of Contents

Sections


General

How do I save settings between game sessions?

BroMaker provides the [SaveableSetting] attribute which you can set on any serializable static variable in a class that inherits from CustomHero. Variables with this attribute will automatically be loaded when the mod starts up, and automatically saved when you press "Save" in the Unity Mod Manager window or when the game shutdowns. If you force close the game the settings may not be saved however.

If you want to display these options and allow users to change them, you can use the UIOptions() method

Example:

[SaveableSetting]
public static bool doubleTapSwitch = true;

How do I get Visual Studio IntelliSense to show the description of members?

Visual Studio has the ability to show descriptions of methods when you're hovering over them or typing them out and looking at them in autocomplete:

image

In order for these to show up, you'll need to make sure that you have the .xml files for whichever assembly contains the given member. So for methods provided by BroMaker, you'll need the BroMakerLib.xml file, which you can find here.

You'll want to download that file and place it next to your BroMakerLib.dll. You need to make sure that it's the same .dll that you added to your solution's references though, as you may have some of these .dlls in multiple locations.

I'd recommend making sure you have all of the following .xml files from here, as well as BroMakerLib.xml, and RocketLib.xml


How do I check if my bro is attached to a helicopter?

You can check the isOnHelicopter variable to see if your bro is attached to a helicopter. It's a good idea to check this if you have some continuous visual / audio effect that you need to cancel when your bro gets on a helicopter.

Example:

// Stop proton gun when getting on helicopter
if ( this.isOnHelicopter )
{
    this.StopProtonGun();
    this.protonAudio.enabled = false;
}

How do I get a prefab by its resource name?

You can use InstantiationController.GetPrefabFromResourceName() to get a reference to any prefab in the game by its resource name. This is useful for getting references to effects, theme holders, or other game objects.

// Get a theme holder
ThemeHolder theme = InstantiationController.GetPrefabFromResourceName( "themeholders:Themeholder Jungle" ).GetComponent<ThemeHolder>();

For theme holders specifically, you can also access them directly through Map.Instance:

ThemeHolder jungleTheme = Map.Instance.jungleThemeReference;

How do I call a private method?

If you need to call a private method on a base game class, you can use reflection:

typeof(TestVanDammeAnim).GetMethod( "Gib", BindingFlags.NonPublic | BindingFlags.Instance ).Invoke( targetObject, new object[] { DamageType.InstaGib, 0f, 0f } );

You'll need to add using System.Reflection; at the top of your file.

The same approach works for accessing private fields:

typeof(TestVanDammeAnim).GetField( "fieldName", BindingFlags.NonPublic | BindingFlags.Instance ).GetValue( targetObject );

If you're doing this frequently, it's a good idea to cache the MethodInfo or FieldInfo in a static variable so you're not doing the reflection lookup every time.

Sprites

How do I set my bro's avatar or special icons?

Use the parameters section in your JSON file to set these sprites:

{
  "parameters": {
    "Avatar": "avatar.png",
    "SpecialIcons": "special_icons.png"
  }
}

See Parameters for all available sprite parameters.


How do I have multiple variants of my bro?

Provide arrays instead of single values in your parameters to create variants that are randomly selected on spawn:

{
  "parameters": {
    "Sprite": ["sprite1.png", "sprite2.png", "sprite3.png"],
    "Avatar": ["avatar1.png", "avatar2.png", "avatar3.png"]
  }
}

See this page for detailed information on variants.


How do I create a sprite with transparent pixels?

The default shader BroMaker uses (Unlit/Depth Cutout With ColouredImage) doesn't render partially-transparent pixels correctly — soft edges show up as black. There are two workarounds:

Option 1: Use a different shader. Unlit/Depth Cutout With Image handles partial alpha correctly, but doesn't support tinting (so effects like invulnerability flash won't show on materials that use it):

var mat = ResourcesController.CreateMaterial(
    Path.Combine(yourBroFolder, "YourSprite.png"),
    ResourcesController.Unlit_DepthCutoutImage
);

Option 2: Change the GameObject's layer. Layers 5, 19, and 28 support transparency. Layer 19 is usually the most useful — layers 5 and 28 draw the sprite in front of things like grass and bridge railings that it would normally appear behind. This keeps tinting working but changes the sort order against the rest of the world.

Audio

How do I play an audio clip continuously?

To play audio continuously (like for a proton gun, flamethrower, or other sustained effects), you need to add an AudioSource component to your bro or projectile and configure it for looping playback. Here's the typical pattern:

1. Add and configure the AudioSource:

// Declare as public field in your bro class (required for prefab serialization)
public AudioSource continuousAudio;

// In your bro's AfterPrefabSetup() method
this.continuousAudio = base.gameObject.AddComponent<AudioSource>();
this.continuousAudio.rolloffMode = AudioRolloffMode.Linear;
this.continuousAudio.minDistance = 200f;
this.continuousAudio.maxDistance = 500f;
this.continuousAudio.spatialBlend = 1f;
this.continuousAudio.volume = 0.33f;

2. Start the continuous loop:

// When starting your effect
if (!this.continuousAudio.isPlaying)
{
    this.continuousAudio.clip = myLoopSound;
    this.continuousAudio.loop = true; 
    this.continuousAudio.Play();
}

3. Stop audio when dying / level ending:

// In your Update() method
if (this.isOnHelicopter || this.actionState == ActionState.Dead)
{
    this.continuousAudio.enabled = false;
}

Transition from Startup to Loop:

// For effects with startup and loop sounds (like Brostbuster's proton gun)
if (!this.continuousAudio.isPlaying)
{
    this.continuousAudio.clip = loopSound;
    this.continuousAudio.loop = true;
    this.continuousAudio.Play();
}

Projectiles

How do I set the hitbox for a projectile?

The primary way to set a projectile's hitbox is through the projectileSize property, which defines the collision radius. This value is used for both horizontal and vertical collision detection by default.

Basic Configuration:

// In your projectile's Awake() method
this.projectileSize = 10f;  // Sets collision radius to 10 pixels

Additional Properties:

  • horizontalProjectile (default: true) - Controls wall collision detection method:

    • true: Raycasts from 2×projectileSize behind (X-offset only) in the velocity direction
    • false: Raycasts from projectile center in the velocity direction
  • isWideProjectile (default: false) - Only affects horizontal projectiles:

    • true: Performs dual collision checks above and below center (for very wide projectiles)
    • false: Single collision check at center

Important Notes:

  • Broforce's collision system assumes roughly circular/square hitboxes
  • Very large projectiles (>20 units) may need custom collision code as the default system wasn't designed for them
  • For complex shapes or very large projectiles, you'll need to override collision methods
  • The visual sprite size doesn't need to match projectileSize - they're independent

How do I make my projectile obey gravity?

Projectiles don't have gravity by default. You have two options:

Option 1: Inherit from CustomGrenade instead of CustomProjectile Grenades have gravity built-in, so if your projectile should arc and bounce, consider making it a grenade.

Option 2: Apply gravity manually in Update()

protected virtual void ApplyGravity()
{
    this.yI -= 500f * this.t;  // 500 is gravity strength
}

protected override void Update()
{
    this.ApplyGravity();
    base.Update();
}

The gravity value of 500 is typical for normal falling speed. Adjust higher for faster falling or lower for floatier projectiles.


How do I prevent a piercing projectile from one-shotting bosses?

Piercing projectiles (ones that pass through enemies instead of being destroyed on contact) have a problem with some bosses. Some bosses are made up of multiple BossBlockPiece segments, and a piercing projectile will hit each segment as it passes through, dealing its full damage to each one. This means a projectile that's balanced for normal enemies can do massive damage to bosses.

The solution is to check if the unit you're hitting is a boss and stop piercing if so. BroMaker provides BroMakerUtilities.IsBoss() for this:

// From Furibrosa - Harpoon.cs
firstUnit.Damage( damageInternal, DamageType.Melee, xI, yI, (int)Mathf.Sign( xI ), firedBy, X, Y );
MakeEffects( false, X, Y, false, raycastHit.normal, raycastHit.point );
Sound.GetInstance().PlaySoundEffectAt( soundHolder.specialSounds, 0.4f, transform.position, 1f, true, false, false, 0f );
if ( BroMakerUtilities.IsBoss( firstUnit ) )
{
    Death();
}

In this example, the harpoon normally pierces through enemies, but when it hits a boss it calls Death() on itself so it doesn't continue through and hit additional boss segments. You'll need to add using BroMakerLib; to use BroMakerUtilities.

Melee

How do I set up a custom melee?

Set useCustomMelee = true in your Awake() or before base.Start(). CustomHero will automatically set meleeType to MeleeType.Custom in Start(), which causes the base game to route melee presses to StartCustomMelee().

protected override void Awake()
{
    useCustomMelee = true;
    base.Awake();
}

See Melee Methods for the full melee call hierarchy.


How do I prevent flexing from resetting my melee animation?

CustomHero handles this automatically via the blockGesturesDuringMelee flag, which is true by default. Gestures are blocked during melee without any code on your part. Set blockGesturesDuringMelee = false if you want to disable this.


How do I allow jumping during melee?

By default, the game doesn't allow jumping during melee attacks. If you jump while doing a melee attack it'll just be cancelled. If you want to let your bro jump during a melee, you can override Jump() and set jumpingMelee = true when the jump happens during a melee. This tells the game that the melee should continue in the air rather than being cancelled.

protected override void Jump( bool wallJump )
{
    base.Jump( wallJump );

    if ( doingMelee )
    {
        jumpingMelee = true;
        dashingMelee = false;
    }
}

The default RunKnifeMeleeMovement() already handles gravity when jumpingMelee is true, so you shouldn't need to override RunCustomMeleeMovement() just for this.

You may also want to block wall jumping during melee by returning early when wallJump is true and doingMelee is true, since the wall jump animation won't play properly while in a melee animation.


How do I prevent double jumping after a melee jump?

If you've allowed jumping during melee (see above), you may notice that the bro can get extra height by holding the jump button after the melee jump. This is because Jump() sets jumpTime to JUMP_TIME, which is normally decremented by RunMovement(), but not while your melee is active, so once your melee ends, jumpTime will still be at its initial value and give you basically a full additional jump's worth of momentum.

To prevent this, set jumpTime to 0 in your CancelMelee() override:

// From Drunken Broster
protected override void CancelMelee()
{
    jumpTime = 0f;
    base.CancelMelee();
}

This stops the jump continuation, so the bro gets a normal-height jump out of the melee rather than being able to hold jump for extra height.

Special

How do I prevent wall jumping from resetting my special animation?

Wall jumping and climbing reset the base frame counter, which can reset special animations. The easiest solution is to track special frames separately:

// Declare a separate frame counter for specials
protected int usingSpecialFrame = 0;

// Override IncreaseFrame to increment it
protected override void IncreaseFrame()
{
    base.IncreaseFrame();
    if (this.usingSpecial)
    {
        ++this.usingSpecialFrame;
    }
}

// Use the separate counter in AnimateSpecial instead of base.frame
protected override void AnimateSpecial()

Death and State

How do I run cleanup when my bro dies?

Override OnDeath() to perform cleanup when your bro dies, such as stopping audio, dropping held items, or resetting visual state. Override OnRevived() to restore state when the bro is revived. These are called automatically when trackDeathRevival is enabled (the default).

protected override void OnDeath()
{
    base.OnDeath();
    this.StopProtonGun();
    this.protonAudio.enabled = false;
}

protected override void OnRevived()
{
    base.OnRevived();
    if ( this.thrownShield != null )
    {
        this.thrownShield.ReturnShieldSilent();
    }
    this.SpecialAmmo = 1;
}

You should also check if (acceptedDeath) return; after base.Update() in your Update() override to skip your own logic while dead:

protected override void Update()
{
    base.Update();
    if ( acceptedDeath ) return;

    // Your logic here won't run while dead
}

See Lifecycle and State for full details.


How do I fix invulnerability tint staying on my bro?

CustomHero automatically resets _TintColor on the main renderer and gun sprite when invulnerability ends (via trackInvulnerabilityTint, which is true by default).

If your bro uses additional materials (alternate sprites, weapon sprites, etc.), register them with RegisterTintMaterial() so they also get reset:

protected override void Start()
{
    useCustomMelee = true;
    base.Start();

    this.drunkSprite = ResourcesController.GetMaterial( directoryPath, "drunkSprite.png" );
    RegisterTintMaterial( this.drunkSprite );
}

You can also call ResetTintColors() directly if you need to manually clear tint at any point.


How do I prevent duplicate variants in multiplayer?

Set deduplicateVariants = true in your Awake(). CustomHero will automatically track active instances and ensure each one gets a different variant. Registration and cleanup are handled through OnDeath(), OnRevived(), and OnDestroy().

protected override void Awake()
{
    deduplicateVariants = true;
    base.Awake();
}

If you need to restrict which variants are available, override GetVariant() and call GetUniqueVariant() with a filtered list:

public override int GetVariant()
{
    List<int> allowed = new List<int> { 0, 1, 2 };
    return GetUniqueVariant( allowed );
}

See GetVariant() for more details.

⚠️ **GitHub.com Fallback** ⚠️