Missile projectile refactor.

Introduces:

Vertical rate of turn
Customizable vertical launch angle
Delayed activation of the homing mechanism / propulsion
Freefall while propulsion deactivated
Customizable explosion altitude (for airburst)
Customizable cruise altitude (when target out of range)
Height checks for terrain impact and shadow rendering
Acceleration (instead of constant speed from launch to impact)
This commit is contained in:
Matija Hustić
2015-07-14 20:10:56 +01:00
parent a79b71c608
commit 41f57f2a15
2 changed files with 200 additions and 42 deletions

View File

@@ -22,18 +22,29 @@ namespace OpenRA.Mods.Common.Effects
{
public class MissileInfo : IProjectileInfo
{
[Desc("Name of the image containing the projectile sequence.")]
public readonly string Image = null;
[Desc("Projectile sequence name.")]
[SequenceReference("Image")] public readonly string Sequence = "idle";
[Desc("Palette used to render the projectile sequence.")]
[PaletteReference] public readonly string Palette = "effect";
[Desc("Should the projectile's shadow be rendered?")]
public readonly bool Shadow = false;
[Desc("Projectile speed in WDist / tick")]
public readonly WDist Speed = new WDist(8);
public readonly WDist InitialSpeed = new WDist(8);
[Desc("Maximum vertical pitch when changing altitude.")]
public readonly WAngle MaximumPitch = WAngle.FromDegrees(30);
[Desc("Vertical launch angle (pitch).")]
public readonly WAngle LaunchAngle = WAngle.Zero;
[Desc("Maximum projectile speed in WDist / tick")]
public readonly WDist MaximumSpeed = new WDist(512);
[Desc("Projectile acceleration when propulsion activated.")]
public readonly WDist Acceleration = WDist.Zero;
[Desc("How many ticks before this missile is armed and can explode.")]
public readonly int Arm = 0;
@@ -47,29 +58,59 @@ namespace OpenRA.Mods.Common.Effects
[Desc("Probability of locking onto and following target.")]
public readonly int LockOnProbability = 100;
[Desc("In n/256 per tick.")]
public readonly int RateOfTurn = 5;
[Desc("Horizontal rate of turn.")]
public readonly int HorizontalRateOfTurn = 5;
[Desc("Explode when following the target longer than this many ticks.")]
[Desc("Vertical rate of turn.")]
public readonly int VerticalRateOfTurn = 5;
[Desc("Run out of fuel after being activated this many ticks. Zero for unlimited fuel.")]
public readonly int RangeLimit = 0;
[Desc("Trail animation.")]
public readonly string Trail = null;
[Desc("Explode when running out of fuel.")]
public readonly bool ExplodeWhenEmpty = true;
[Desc("Interval in ticks between each spawned Trail animation.")]
public readonly int TrailInterval = 2;
[Desc("Altitude above terrain below which to explode. Zero effectively deactivates airburst.")]
public readonly WDist AirburstAltitude = WDist.Zero;
[Desc("Cruise altitude. Zero means no cruise altitude used.")]
public readonly WDist CruiseAltitude = new WDist(512);
[Desc("Activate homing mechanism after this many ticks.")]
public readonly int HomingActivationDelay = 0;
[Desc("Image that contains the trail animation.")]
public readonly string TrailImage = null;
[Desc("Smoke sequence name.")]
[SequenceReference("Trail")] public readonly string TrailSequence = "idle";
[Desc("Palette used to render the smoke sequence.")]
[PaletteReference("TrailUsePlayerPalette")] public readonly string TrailPalette = "effect";
[Desc("Use the Player Palette to render the smoke sequence.")]
public readonly bool TrailUsePlayerPalette = false;
[Desc("Interval in ticks between spawning smoke animation.")]
public readonly int TrailInterval = 2;
[Desc("Should smoke animation be spawned when the propulsion is not activated.")]
public readonly bool TrailWhenDeactivated = false;
public readonly int ContrailLength = 0;
public readonly Color ContrailColor = Color.White;
public readonly bool ContrailUsePlayerColor = false;
public readonly int ContrailDelay = 1;
[Desc("Should missile targeting be thrown off by nearby actors with JamsMissiles.")]
public readonly bool Jammable = true;
[Desc("Range of facings by which jammed missiles can stray from current path.")]
public readonly int JammedDiversionRange = 20;
[Desc("Explodes when leaving the following terrain type, e.g., Water for torpedoes.")]
public readonly string BoundToTerrainType = "";
@@ -87,12 +128,19 @@ namespace OpenRA.Mods.Common.Effects
readonly ProjectileArgs args;
readonly Animation anim;
readonly WVec gravity = new WVec(0, 0, -10);
int ticksToNextSmoke;
ContrailRenderable contrail;
string trailPalette;
int terrainHeight;
[Sync] WPos pos;
[Sync] int facing;
[Sync] WVec velocity;
[Sync] int hFacing;
[Sync] int vFacing;
[Sync] bool activated;
[Sync] int speed;
[Sync] WPos targetPosition;
[Sync] WVec offset;
@@ -109,7 +157,13 @@ namespace OpenRA.Mods.Common.Effects
this.args = args;
pos = args.Source;
facing = args.Facing;
hFacing = args.Facing;
vFacing = info.LaunchAngle.Angle / 4;
speed = info.InitialSpeed.Length;
velocity = new WVec(WDist.Zero, -info.InitialSpeed, WDist.Zero)
.Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero))
.Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing)));
targetPosition = args.PassiveTarget;
@@ -126,8 +180,8 @@ namespace OpenRA.Mods.Common.Effects
if (!string.IsNullOrEmpty(info.Image))
{
anim = new Animation(world, info.Image, () => facing);
anim.PlayRepeating("idle");
anim = new Animation(world, info.Image, () => hFacing);
anim.PlayRepeating(info.Sequence);
}
if (info.ContrailLength > 0)
@@ -158,52 +212,144 @@ namespace OpenRA.Mods.Common.Effects
if (anim != null)
anim.Tick();
// Missile tracks target
var cell = world.Map.CellContaining(pos);
terrainHeight = world.Map.MapHeight.Value[cell] * 512;
// Switch from freefall mode to homing mode
if (ticks == info.HomingActivationDelay + 1)
{
activated = true;
hFacing = OpenRA.Traits.Util.GetFacing(velocity, hFacing);
speed = velocity.Length;
}
// Switch from homing mode to freefall mode
if (info.RangeLimit != 0 && ticks == info.RangeLimit + 1)
{
activated = false;
velocity = new WVec(0, -speed, 0)
.Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero))
.Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing)));
}
// Check if target position should be updated (actor visible & locked on)
if (args.GuidedTarget.IsValidFor(args.SourceActor) && lockOn)
targetPosition = args.GuidedTarget.CenterPosition;
targetPosition = args.GuidedTarget.CenterPosition + new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude);
// Compute current distance from target position
var dist = targetPosition + offset - pos;
var desiredFacing = OpenRA.Traits.Util.GetFacing(dist, facing);
var desiredAltitude = targetPosition.Z;
var jammed = info.Jammable && world.ActorsWithTrait<JamsMissiles>().Any(JammedBy);
var len = dist.Length;
var hLenCurr = dist.HorizontalLength;
if (jammed)
WVec move;
if (activated)
{
desiredFacing = facing + world.SharedRandom.Next(-20, 21);
desiredAltitude = world.SharedRandom.Next(-43, 86);
// If target is within range, keep speed constant and aim for the target.
// The speed needs to be kept constant to keep range computation relatively simple.
// If target is not within range, accelerate the projectile. If cruise altitudes
// are not used, aim for the target. If the cruise altitudes are used, aim for the
// target horizontally and for cruise altitude vertically.
// Target is considered in range if after an additional tick of accelerated motion
// the horizontal distance from the target would be less than
// the diameter of the circle that the missile travels along when
// turning vertically at the maximum possible rate.
// This should work because in the worst case, the missile will have to make
// a semi-loop before hitting the target.
// Get underestimate of distance from target in next tick, so that inRange would
// become true a little sooner than the theoretical "in range" condition is met.
var hLenNext = (long)(hLenCurr - speed - info.Acceleration.Length).Clamp(0, hLenCurr);
// Check if target in range
bool inRange = hLenNext * hLenNext * info.VerticalRateOfTurn * info.VerticalRateOfTurn * 314 * 314
<= 2L * 2 * speed * speed * 128 * 128 * 100 * 100;
// Basically vDist is the representation in the x-y plane
// of the projection of dist in the z-hDist plane,
// where hDist is the projection of dist in the x-y plane.
// This allows applying vertical rate of turn in the same way as the
// horizontal rate of turn is applied.
WVec vDist;
if (inRange || info.CruiseAltitude.Length == 0)
vDist = new WVec(-dist.Z, -hLenCurr, 0);
else
vDist = new WVec(-(dist.Z - targetPosition.Z + info.CruiseAltitude.Length + terrainHeight), -speed, 0);
// Accelerate if out of range
if (!inRange)
speed = (speed + info.Acceleration.Length).Clamp(0, info.MaximumSpeed.Length);
// Compute which direction the projectile should be facing
var desiredHFacing = OpenRA.Traits.Util.GetFacing(dist, hFacing);
var desiredVFacing = OpenRA.Traits.Util.GetFacing(vDist, vFacing);
// Check whether the homing mechanism is jammed
var jammed = info.Jammable && world.ActorsWithTrait<JamsMissiles>().Any(JammedBy);
if (jammed)
{
desiredHFacing = hFacing + world.SharedRandom.Next(-info.JammedDiversionRange, info.JammedDiversionRange + 1);
desiredVFacing = vFacing + world.SharedRandom.Next(-info.JammedDiversionRange, info.JammedDiversionRange + 1);
}
else if (!args.GuidedTarget.IsValidFor(args.SourceActor))
desiredHFacing = hFacing;
// Compute new direction the projectile will be facing
hFacing = OpenRA.Traits.Util.TickFacing(hFacing, desiredHFacing, info.HorizontalRateOfTurn);
vFacing = OpenRA.Traits.Util.TickFacing(vFacing, desiredVFacing, info.VerticalRateOfTurn);
// Compute the projectile's guided displacement
move = new WVec(0, -1024 * speed, 0)
.Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero))
.Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing)))
/ 1024;
}
else if (!args.GuidedTarget.IsValidFor(args.SourceActor))
desiredFacing = facing;
facing = OpenRA.Traits.Util.TickFacing(facing, desiredFacing, info.RateOfTurn);
var move = new WVec(0, -1024, 0).Rotate(WRot.FromFacing(facing)) * info.Speed.Length / 1024;
if (pos.Z != desiredAltitude)
else
{
var delta = move.HorizontalLength * info.MaximumPitch.Tan() / 1024;
var dz = (targetPosition.Z - pos.Z).Clamp(-delta, delta);
move += new WVec(0, 0, dz);
// Compute the projectile's freefall displacement
move = velocity + gravity / 2;
velocity += gravity;
var velRatio = info.MaximumSpeed.Length * 1024 / velocity.Length;
if (velRatio < 1024)
velocity = velocity * velRatio / 1024;
}
pos += move;
if (!string.IsNullOrEmpty(info.Trail) && --ticksToNextSmoke < 0)
// When move (speed) is large, check for impact during the following next tick
// Shorten the move to have its length match the distance from the target
// and check for impact with the shortened move
var movLen = move.Length;
if (len < movLen)
{
world.AddFrameEndTask(w => w.Add(new Smoke(w, pos - 3 * move / 2, info.Trail, trailPalette, info.Sequence)));
var npos = pos + move * 1024 * len / movLen / 1024;
if (world.Map.DistanceAboveTerrain(npos).Length <= 0 // Hit the ground
|| (targetPosition + offset - npos).LengthSquared < info.CloseEnough.LengthSquared) // Within range
pos = npos;
else
pos += move;
}
else
pos += move;
// Create the smoke trail effect
if (!string.IsNullOrEmpty(info.TrailImage) && --ticksToNextSmoke < 0 && (activated || info.TrailWhenDeactivated))
{
world.AddFrameEndTask(w => w.Add(new Smoke(w, pos - 3 * move / 2, info.TrailImage, trailPalette, info.TrailSequence)));
ticksToNextSmoke = info.TrailInterval;
}
if (info.ContrailLength > 0)
contrail.Update(pos);
var cell = world.Map.CellContaining(pos);
var shouldExplode = (pos.Z < 0) // Hit the ground
|| (dist.LengthSquared < info.CloseEnough.LengthSquared) // Within range
|| (info.RangeLimit != 0 && ticks > info.RangeLimit) // Ran out of fuel
var height = world.Map.DistanceAboveTerrain(pos);
var shouldExplode = (height.Length <= 0) // Hit the ground
|| (len < info.CloseEnough.Length) // Within range
|| (info.ExplodeWhenEmpty && info.RangeLimit != 0 && ticks > info.RangeLimit) // Ran out of fuel
|| (info.Blockable && BlocksProjectiles.AnyBlockingActorAt(world, pos)) // Hit a wall or other blocking obstacle
|| !world.Map.Contains(cell) // This also avoids an IndexOutOfRangeException in GetTerrainInfo below.
|| (!string.IsNullOrEmpty(info.BoundToTerrainType) && world.Map.GetTerrainInfo(cell).Type != info.BoundToTerrainType); // Hit incompatible terrain
|| (!string.IsNullOrEmpty(info.BoundToTerrainType) && world.Map.GetTerrainInfo(cell).Type != info.BoundToTerrainType) // Hit incompatible terrain
|| (height.Length < info.AirburstAltitude.Length && hLenCurr < info.CloseEnough.Length); // Airburst
if (shouldExplode)
Explode(world);

View File

@@ -2718,6 +2718,18 @@ namespace OpenRA.Mods.Common.UtilityCommands
}
}
if (engineVersion < 20150912)
{
if (depth == 2 && parentKey == "Projectile" && parent.Value.Value == "Missile" && node.Key == "Speed")
node.Key = "InitialSpeed";
if (depth == 2 && parentKey == "Projectile" && parent.Value.Value == "Missile" && node.Key == "RateOfTurn")
node.Key = "HorizontalRateOfTurn";
if (depth == 2 && parentKey == "Projectile" && parent.Value.Value == "Missile" && node.Key == "Trail")
node.Key = "TrailImage";
}
UpgradeWeaponRules(engineVersion, ref node.Value.Nodes, node, depth + 1);
}
}