Skip to content

Projectile API

George Aleksandrovich edited this page Nov 9, 2018 · 11 revisions

Projectile API

Overview

Projectiles are most commonly created to be fired in any of the Shoot AISequences.

The general way of defining a new Projectile is in-line, in the following way:

new Projectile {
  Size = Size.MEDIUM,
  Speed = Speed.FAST,
  ...
  AngleOffset = -30f
}

Such a Projectile could be fired with the following AISequence:

new Shoot1
(
  new Projectile 
  { 
    Size = Size.MEDIUM 
  }
);

Customizeable Variables

Below is a list of the variables you can set.

Start

The spawning location of this Projectile. This is of type ProxyVector3, which accepts Vector3s, but also specifies a few "live" locations defined in AISequence. These consist of the following:

  • PLAYER_POSITION: The current position of the Player.
  • DELAYED_PLAYER_POSITION: If the AISequence PlayerLock is set to true, then returns the position of the Player as of the time when that method was called. Otherwise, returns the same as PLAYER_POSITION.
  • LEADING_PLAYER_POSITION: The current position of the Player, plus their current velocity. Can be used to "lead" ahead of players slightly. This is slightly jittery because it depends on the Player's input, but is more accurate than SMOOTHED_LEADING_PLAYER_POSITION.
  • SMOOTHED_LEADING_PLAYER_POSITION: Like LEADING_PLAYER_POSITION, but smooths the Player's input data over a few frames. This gets updated every time it is called; therefore, if you use this variable infrequently, your Projectile might be using a significantly out of date Player position. It's best to use this if you have rapid-fire Projectiles that value appearing smooth over being accurate.
  • BOSS_POSITION: The current position of the Boss.
  • RANDOM_IN_ARENA: Randomly chooses any position that is valid in the current arena's shape and size.

There are additionally a few constants referring to positions in the arena that may be useful, and can be found in World.Arena. They take the form [DIRECTION]_[DISTANCE], where [DIRECTION] is one of:

  • NORTH
  • SOUTH
  • EAST
  • WEST

and [DISTANCE] is one of:

  • CLOSE
  • MED
  • FAR

There is a constant for the center of the arena in World.Arena, conveniently named CENTER.

By default, Start = BOSS_POSITION so that Projectiles originate from the Boss's current position. However, any Vector3 is valid. Keep in mind that the x component of the Vector3 refers to the left-right location, the z component to the up-down location, and the y component to the height above the arena. Because the Projectiles have a height, it is recommended to set the start position to be nonzero, or else they will visually clip into the ground. Every constant described so far has its y component equal to a constant, BOSS_HEIGHT, defined in World.Arena to combat this; it is recommended that any custom positions use this y value as well.

Target

Provides the initial direction that the Projectile will aim at. This is of type ProxyVector3, just like Start, and is in absolute coordinates (that is, it is not relative to Start).

By default, Target = DELAYED_PLAYER_POSITION, and so Projectiles will by default respect the PlayerLock move. If you explicitly want your Projectile to always follow the Player, you can set Target = PLAYER_POSITION instead.

AngleOffset

The amount of (clockwise) degrees offset from the zero line this Projectile will fire. The zero line is defined as the vector originating at Start and ending at Target. For example, if you have Start = new Vector3(0, 0, 0) and Target = new Vector3(0, 0, -1), the zero line will be pointing straight south in the arena. If you then were to have AngleOffset = 45 with the same Start and Target, the firing line is rotated 45 degrees clockwise to point south-west.

This parameter is commonly used to vary the orientation of a Projectile without needing to change the Target every time; for example, if you wanted to sweep an arc of Projectiles from -30 to +30 degrees relative to the Player's position, you would have to do some complex math involving trigonometry to get Target to be correct. Instead, you can simply do:

For(-30, 30, 1, 
  angle => new Shoot1(new Projectile { AngleOffset = angle })
)

By default, AngleOffset = 0.

Speed

How fast this Projectile moves. There is an enum representing valid Speed values in BossCore.Speed. BossCore is included by default in the templates, and so is usually referred to as just Speed. The speed values are defined in units per second.

By default, Speed = Speed.MEDIUM, for a speed of 25 units per second.

Size

How large this Projectile is. Valid Size values are found in the Projectiles.Size enum, and are reproduced below:

  • TINY: 1 unit (in diameter)
  • SMALL: 1.5 units
  • MEDIUM: 2 units
  • LARGE: 2.5 units
  • HUGE: 3 units

For default Projectiles, this also affects the damage the Projectile does, as well as its color.

Damage is determined using the formula (INDEX + 0.5) * 2, where INDEX refers to the position of the Size in the list above (with TINY = 0). For example, to find the damage of a Size.SMALL projectile, you start by looking up its index in the list above. Because it is index number 1, we conclude it does (1 + 0.5) * 2 = 3 damage.

The color of the Projectile is determined by its Size, unless otherwise specified. TINY and SMALL Projectiles are blue; MEDIUM Projectiles are orange; and LARGE and HUGE Projectiles are orange-red. To help aid in identification, every built-in subclass of Projectile (such as ProjectileHoming) picks a custom color to apply to every Projectile of its type (such as purple for ProjectileHoming). If you want a custom color for your Projectile, see how to subclass Projectile.

By default, Size = Size.SMALL.

MaxTime

The amount of time (in seconds) this Projectile will stay in play before disappearing. Normally, a Projectile will die when it either collides with the Player (or their shield), or when it goes too far off of the arena. There is an additional time limit that you can set via MaxTime.

When a Projectile dies due to being active for longer than MaxTime seconds, it will call the OnDeathTimeout callback.

By default, MaxTime = 10.

Damage

The amount of damage this Projectile inflicts to the Player on contact. By default, this is determined by the Size parameter. If INDEX is the index of the Size in the enum, then the damage done is (INDEX + 0.5) * 2.

Velocity

The Velocity value will be set to (roughly):

Velocity = Speed * (Target - Start).normalized

You shouldn't really touch this value in the constructor or object initializer, because the default value will overwrite any customization you do here. If you want to specify a direction the Projectile moves in, you should set Start and Target appropriately; if you want more complex behavior for moving a Projectile, such as varying the Velocity over time, see how to subclass Projectile.

Customizeable Events

These events are callbacks that trigger upon certain events occurring. By default, they all do nothing, but can be used to do things like spawn additional Projectiles or AOEs upon the death of this Projectile.

They all are methods of the form ProjectileComponent => AISequence, where a ProjectileComponent is an object that gives you access to a few additional read-only variables. The convention is to label the ProjectileComponent object as self within the method, because it is a reference back to the parent Projectile data object. However, you can use any name that you'd like. Some of the more useful variables you can access in ProjectileComponent are listed below:

  • data: A reference back to the original Projectile you created. You can access the original customizeable variables here, which is useful if you want to share similar values with the parent.
  • transform: A Transform object of the actual spawned Projectile. You can access the death position of the Projectile using transform.position.
  • Start: The "baked" start position of the Projectile. This is an actual static Vector3 that was set on creation of this Projectile object, and locked any live variables such as Player position. Thus, if you want to query the start position when the Projectile was fired, you can access it here. If you want the "live" start position, access it via data.Start instead.
  • Target: The "baked" target position. Like above, but with Target instead of Start.

The method you can assign to the callback is restricted to a lambda expression, rather than a full arbitrary method. Here's a bit of example code to demonstrate the notation:

new Shoot1
(
  new Projectile 
  {
    MaxTime = 2f,
    OnDeathTimeout = self => new Shoot1(new Projectile { Start = self.transform.position })
  }
);

This will fire a single default Projectile with a MaxTime of 2 seconds. If it doesn't hit the Player, it will time out, which calls the OnDeathTimeout event. In this case, the event we set will shoot a new Projectile, with its starting location set to be the death point of the first Projectile.

Note that because you're limited to a lambda expression, you CANNOT do the following:

new Shoot1
(
  new Projectile 
  {
    MaxTime = 2f,
    OnDeathTimeout = self => {
      Debug.Log("Hello world!");
      float delay = 5f;
      return new Pause(delay);
    }
  }
);

You should be able to use the AISequence builder API to accomplish your goals; if you need more flexibility, you can create custom Projectiles with their own custom death behavior. See subclassing Projectiles for more information.

Note that there is one more gotcha to be aware of when using callback events: any methods with optional parameters have to be explicitly specified. The API is designed to avoid using optional parameters wherever possible to minimize the impact this has, but you should be aware of this limitation if you make custom constructors for Projectiles or Moves.

OnDestroyTimeout

Called when the Projectile has had MaxTime seconds elapse since it has spawned.

OnDestroyOutOfBounds

Called when the Projectile gets too far away from the arena's boundaries. By default, this is set to be a radius of 75 units away from the center of the arena. In other words, 25 units beyond the edge of the full arena size.

OnDestroyCollision

Called when the Projectile collides with the Player, their shield, or any obstacles in the arena.

Builtin Projectile Types

Projectile

This is the default Projectile type. Moves straight from Start to Target at a speed of Speed, has a color and damage determined by its Size, and does nothing special upon collision.

ProjectileHoming

A Projectile that homes in on the Player's position. Has a color of purple.

ProjectileCurving

A Projectile with a fixed curving speed. Its constructor has two parameters: curveAmount and leavesTrail. curveAmount expresses how many (clockwise) degrees per second this Projectile should curve by, and leavesTrail, if true, will make this Projectile leave a trail behind wherever it goes.

Has a color of green.

ProjectileLightning

A Projectile that rapidly splits into multiple other versions of itself, and leaves a trail behind wherever it goes. The lightning trail will never hit the Player if they don't move.

ProjectileReverse

A Projectile that starts by reversing its direction, and then accelerating to 1.5x its normal maximum speed towards Target.

ProjectileDeathHex

A Projectile that explodes into six ProjectileCurvings on death.

Subclassing Projectile

If you aren't able to accomplish what you want to do with the available Projectile customization defined above, then it's probably time to create your own custom Projectile subclass!

Custom constructor

You can include any required parameters into the constructor for your subclass. For example, ProjectileCurving wouldn't make sense if it had a default curving amount, and so it forces you to define that value to create a new instance.

Custom properties

Additional customization parameters can be created using C# properties. As an example, suppose you want to have a custom Color property. You can define it within your subclass like so:

public class MyProjectile : Projectile 
{
  public Color Color { get; set; } = Color.green;

  public MyProjectile() {
    // Custom logic for making MyProjectile a different color here
  }
}

Now, if you wanted to create a red MyProjectile, you can customize it like any other Projectile:

new Shoot1(new MyProjectile { Color = Color.red });

Essentially all the fields are virtual to provide the ability to customize the actual behavior for getting and setting values. For example, if you wanted to customize the color of your Projectile based on how fast it was going, you could write some code like this:

public class MyProjectile : Projectile 
{
  public Color Color { get; set; } = Color.green;

  public override Speed Speed {
    get { return base.Speed; }
    set { 
      // Real fast! Make it red so it looks fast, too
      if ((float)value > 30) 
      {
        Color = Color.red;
      }
      base.Speed = value;
    }
  }
}

Be sure to call the base property methods if possible, because some of them (like Size) have additional behavior that you might lose out on (like setting up the Damage value properly, in this case).

Custom Methods

There are three methods you can use to further specify the behavior of your Projectile subclass. Specifically, you have:

  • CustomUpdate: called once every game frame. You can do custom updating logic like changing Velocity to home onto the Player, spawning sub-Projectiles, or die early here.
  • CustomMaterial: sets the material for this Projectile. You can specifically force a color or material using this method.
  • CustomCreate: called once a new ProjectileComponent is created. This method is passed a reference to the new ProjectileComponent. This is your opportunity to get information about the "live" variables, such as actual spawn location (via component.transform.position).