#region File Description
//-----------------------------------------------------------------------------
// Ship.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion
#region Using Statements
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Content;
#endregion
namespace NetRumble
{
///
/// The ship, which is the primary playing-piece in the game.
///
public class Ship : GameplayObject
{
#region Constants
///
/// The full speed possible for the ship.
///
const float fullSpeed = 320f;
///
/// The amount of drag applied to velocity per second,
/// as a percentage of velocity.
///
const float dragPerSecond = 0.9f;
///
/// The amount that the right-stick must be pressed to fire, squared so that
/// we can use LengthSquared instead of Length, which has a square-root in it.
///
const float fireThresholdSquared = 0.25f;
///
/// The number of radians that the ship can turn in a second at full left-stick.
///
const float rotationRadiansPerSecond = 6f;
///
/// The maximum length of the velocity vector on a ship.
///
const float velocityMaximum = 320f;
///
/// The maximum strength of the shield.
///
const float shieldMaximum = 100f;
///
/// The maximum opacity for the shield, when it's fully recharged.
///
const float shieldAlphaMaximum = 150f;
///
/// How much the shield recharges per second.
///
const float shieldRechargePerSecond = 50f;
///
/// The duration of the shield-recharge timer when the ship is hit.
///
const float shieldRechargeTimerMaximum = 2.5f;
///
/// The base scale for the shield, compared to the size of the ship.
///
const float shieldScaleBase = 1.2f;
///
/// The amplitude of the shield pulse
///
const float shieldPulseAmplitude = 0.15f;
///
/// The rate of the shield pulse.
///
const float shieldPulseRate = 0.2f;
///
/// The maximum value of the "safe" timer.
///
const float safeTimerMaximum = 4f;
///
/// The maximum amount of life that a ship can have.
///
const float lifeMaximum = 25f;
///
/// The value of the spawn timer set when the ship dies.
///
const float respawnTimerOnDeath = 5f;
///
/// The number of variations in textures for ships.
///
const int variations = 4;
#endregion
#region Static Graphics Data
///
/// The primary ship textures.
///
private static Texture2D[] primaryTextures = new Texture2D[variations];
///
/// The overlay ship textures, which get tinted.
///
private static Texture2D[] overlayTextures = new Texture2D[variations];
///
/// The ship shield texture.
///
private static Texture2D shieldTexture;
///
/// The colors used for each ship.
///
public static readonly Color[] ShipColors =
{
Color.Lime, Color.CornflowerBlue, Color.Fuchsia,
Color.Red, Color.LightSeaGreen, Color.LightGray,
Color.Gold, Color.ForestGreen, Color.Beige,
Color.LightPink, Color.Lavender, Color.OrangeRed,
Color.Plum, Color.Tan, Color.YellowGreen,
Color.Azure, Color.Aqua, Color.Salmon
};
///
/// The particle-effect manager which recieves the effects from ships.
///
public static ParticleEffectManager ParticleEffectManager;
#endregion
#region Gameplay Data
///
/// The score for this ship.
///
private int score = 0;
public int Score
{
get { return score; }
set { score = value; }
}
///
/// The amount of damage that the ship can take before exploding.
///
private float life;
public float Life
{
get { return life; }
set { life = value; }
}
///
/// The ship's primary weapon.
///
private Weapon weapon;
public Weapon Weapon
{
get { return weapon; }
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
weapon = value;
}
}
///
/// The ship's mine-laying weapon.
///
private MineWeapon mineWeapon;
///
/// All of the projectiles fired by this ship.
///
private BatchRemovalCollection projectiles =
new BatchRemovalCollection();
public BatchRemovalCollection Projectiles
{
get { return projectiles; }
}
///
/// The strength of the shield.
///
private float shield;
public float Shield
{
get { return shield; }
set { shield = value; }
}
///
/// Timer for how long until the shield starts to recharge.
///
private float shieldRechargeTimer;
///
/// Timer for how long the player is safe after spawning.
///
private float safeTimer;
public bool Safe
{
get { return (safeTimer > 0f); }
set
{
if (value)
{
safeTimer = safeTimerMaximum;
}
else
{
safeTimer = 0f;
}
}
}
///
/// The tint of the ship.
///
private Color color = Color.White;
public Color Color
{
get { return color; }
set { color = value; }
}
///
/// The amount of time left before respawning the ship.
///
private float respawnTimer;
public float RespawnTimer
{
get { return respawnTimer; }
set { respawnTimer = value; }
}
///
/// The last object to damage the ship.
///
private GameplayObject lastDamagedBy = null;
public GameplayObject LastDamagedBy
{
get { return lastDamagedBy; }
}
#endregion
#region Input Data
///
/// The current player input for the ship.
///
private ShipInput shipInput;
public ShipInput ShipInput
{
get { return shipInput; }
set { shipInput = value; }
}
#endregion
#region Graphics Data
///
/// The variation of this ship.
///
private int variation = 0;
public int Variation
{
get { return variation; }
set
{
if ((value < 0) || (value >= variations))
{
throw new ArgumentOutOfRangeException("value");
}
variation = value;
}
}
///
/// The time accumulator for the shield pulse.
///
private float shieldPulseTime = 0f;
#endregion
#region Initialization Methods
///
/// Construct a new ship.
///
public Ship()
: base()
{
// set the collision data
this.radius = 24f;
this.mass = 32f;
}
///
/// Initialize a ship to its default gameplay states.
///
public override void Initialize()
{
if (!active)
{
// set the initial gameplay data values
shipInput = ShipInput.Empty;
rotation = 0f;
velocity = Vector2.Zero;
life = lifeMaximum;
shield = shieldMaximum;
shieldRechargeTimer = 0f;
safeTimer = safeTimerMaximum;
weapon = new LaserWeapon(this);
mineWeapon = new MineWeapon(this);
// play the player-spawn sound
AudioManager.PlaySoundEffect("player_spawn");
// add the ship-spawn particle effect
if (ParticleEffectManager != null)
{
ParticleEffectManager.SpawnEffect(ParticleEffectType.ShipSpawn,
this);
}
// clear out the projectiles list
projectiles.Clear();
}
base.Initialize();
}
#endregion
#region Updating Methods
///
/// Update the ship.
///
/// The amount of elapsed time, in seconds.
public override void Update(float elapsedTime)
{
// calculate the current forward vector
Vector2 forward = new Vector2((float)Math.Sin(Rotation),
-(float)Math.Cos(Rotation));
Vector2 right = new Vector2(-forward.Y, forward.X);
// calculate the new forward vector with the left stick
shipInput.LeftStick.Y *= -1f;
if (shipInput.LeftStick.LengthSquared() > 0f)
{
// change the direction
Vector2 wantedForward = Vector2.Normalize(shipInput.LeftStick);
float angleDiff = (float)Math.Acos(
Vector2.Dot(wantedForward, forward));
float facing = (Vector2.Dot(wantedForward, right) > 0f) ?
1f : -1f;
if (angleDiff > 0.001f)
{
Rotation += Math.Min(angleDiff, facing * elapsedTime *
rotationRadiansPerSecond);
}
// add velocity
Velocity += shipInput.LeftStick * (elapsedTime * fullSpeed);
if (Velocity.Length() > velocityMaximum)
{
Velocity = Vector2.Normalize(Velocity) *
velocityMaximum;
}
}
shipInput.LeftStick = Vector2.Zero;
// apply drag to the velocity
Velocity -= Velocity * (elapsedTime * dragPerSecond);
if (Velocity.LengthSquared() <= 0f)
{
Velocity = Vector2.Zero;
}
// check for firing with the right stick
shipInput.RightStick.Y *= -1f;
if (shipInput.RightStick.LengthSquared() > fireThresholdSquared)
{
weapon.Fire(Vector2.Normalize(shipInput.RightStick));
}
shipInput.RightStick = Vector2.Zero;
// check for laying mines
if (shipInput.MineFired)
{
// fire behind the ship
mineWeapon.Fire(-forward);
}
shipInput.MineFired = false;
// recharge the shields
if (shieldRechargeTimer > 0f)
{
shieldRechargeTimer = Math.Max(shieldRechargeTimer - elapsedTime,
0f);
}
if (shieldRechargeTimer <= 0f)
{
if (shield < shieldMaximum)
{
shield = Math.Min(shieldMaximum,
shield + shieldRechargePerSecond * elapsedTime);
}
}
// update the radius based on the shield
radius = (shield > 0f) ? 24f : 20f;
// update the weapons
if (weapon != null)
{
weapon.Update(elapsedTime);
}
if (mineWeapon != null)
{
mineWeapon.Update(elapsedTime);
}
// decrement the safe timer
if (safeTimer > 0f)
{
safeTimer = Math.Max(safeTimer - elapsedTime, 0f);
}
// update the projectiles
foreach (Projectile projectile in projectiles)
{
if (projectile.Active)
{
projectile.Update(elapsedTime);
}
else
{
projectiles.QueuePendingRemoval(projectile);
}
}
projectiles.ApplyPendingRemovals();
base.Update(elapsedTime);
}
#endregion
#region Drawing Methods
///
/// Draw the ship.
///
/// The amount of elapsed time, in seconds.
/// The SpriteBatch object used to draw.
public void Draw(float elapsedTime, SpriteBatch spriteBatch)
{
// draw the uniform section of the ship
base.Draw(elapsedTime, spriteBatch, primaryTextures[variation],
null, Color.White);
// draw the tinted section of the ship
base.Draw(elapsedTime, spriteBatch, overlayTextures[variation],
null, color);
if (shield > 0)
{
// calculate the current shield radius
float oldRadius = radius;
shieldPulseTime += elapsedTime;
radius *= shieldScaleBase + shieldPulseAmplitude *
(float)Math.Sin(shieldPulseTime / shieldPulseRate);
// draw the shield
base.Draw(elapsedTime, spriteBatch, shieldTexture, null,
new Color(color.R, color.G, color.B,
(byte)Math.Floor(shieldAlphaMaximum * shield / shieldMaximum)));
// return to the old radius
radius = oldRadius;
}
// draw the projectiles
foreach (Projectile projectile in projectiles)
{
projectile.Draw(elapsedTime, spriteBatch);
}
}
#endregion
#region Interaction Methods
///
/// Damage this ship by the amount provided.
///
///
/// This function is provided in lieu of a Life mutation property to allow
/// classes of objects to restrict which kinds of objects may damage them,
/// and under what circumstances they may be damaged.
///
/// The GameplayObject responsible for the damage.
/// The amount of damage.
/// If true, this object was damaged.
public override bool Damage(GameplayObject source, float damageAmount)
{
// if the safe timer hasn't yet gone off, then the ship can't be hurt
if ((safeTimer > 0f) || (damageAmount <= 0f))
{
return false;
}
// once you're hit, the shield-recharge timer starts over
shieldRechargeTimer = 2.5f;
// damage the shield first, then life
if (shield <= 0f)
{
life -= damageAmount;
}
else
{
shield -= damageAmount;
if (shield < 0f)
{
// shield has the overflow value as a negative value, just add it
life += shield;
shield = 0f;
}
}
Projectile sourceAsProjectile = source as Projectile;
if (sourceAsProjectile != null)
{
lastDamagedBy = sourceAsProjectile.Owner;
}
else
{
lastDamagedBy = source;
}
return true;
}
///
/// Kills this ship, in response to the given GameplayObject.
///
/// The GameplayObject responsible for the kill.
///
/// If true, the object dies without any further effects.
///
public override void Die(GameplayObject source, bool cleanupOnly)
{
if (active)
{
if (!cleanupOnly)
{
// update the score
Ship ship = source as Ship;
if (ship == null)
{
Projectile projectile = source as Projectile;
if (projectile != null)
{
ship = projectile.Owner;
}
}
if (ship != null)
{
if (ship == this)
{
// reduce the score, since i blew myself up
ship.Score--;
}
else
{
// add score to the ship who shot this object
ship.Score++;
}
}
else
{
// if it wasn't a ship, then this object loses score
this.Score--;
}
// play the player-death sound
AudioManager.PlaySoundEffect("explosion_shockwave");
AudioManager.PlaySoundEffect("explosion_large");
// display the ship explosion
if (ParticleEffectManager != null)
{
ParticleEffectManager.SpawnEffect(
ParticleEffectType.ShipExplosion, Position);
}
}
// clear out the projectiles list
foreach (Projectile projectile in projectiles)
{
projectile.Die(null, true);
}
projectiles.Clear();
// set the respawn timer
respawnTimer = respawnTimerOnDeath;
}
base.Die(source, cleanupOnly);
}
#endregion
#region Static Graphics Methods
///
/// Load all of the static graphics content for this class.
///
/// The content manager to load with.
public static void LoadContent(ContentManager contentManager)
{
// safety-check the parameters
if (contentManager == null)
{
throw new ArgumentNullException("contentManager");
}
// load each ship's texture
for (int i = 0; i < variations; i++)
{
primaryTextures[i] = contentManager.Load(
"Textures/ship" + i.ToString());
overlayTextures[i] = contentManager.Load(
"Textures/ship" + i.ToString() + "Overlay");
}
// load the shield texture
shieldTexture = contentManager.Load("Textures/shipShields");
}
///
/// Unload all of the static graphics content for this class.
///
public static void UnloadContent()
{
for (int i = 0; i < variations; i++)
{
primaryTextures[i] = overlayTextures[i] = null;
}
shieldTexture = null;
}
///
/// Determines if a color index is unique.
///
/// The gamer with the color index.
/// The session the player belongs to.
/// If true, the gamer has a unique color ID.
public static bool HasUniqueColorIndex(NetworkGamer networkGamer,
NetworkSession networkSession)
{
// safety-check the parameters
if (networkGamer == null)
{
throw new ArgumentNullException("networkGamer");
}
if (networkSession == null)
{
throw new ArgumentNullException("networkSession");
}
PlayerData playerData = networkGamer.Tag as PlayerData;
if (playerData == null)
{
throw new ArgumentNullException("networkGamer.Tag as PlayerData");
}
// search for a match
foreach (NetworkGamer gamer in networkSession.AllGamers)
{
if (gamer == networkGamer)
{
continue;
}
PlayerData gamerData = gamer.Tag as PlayerData;
if ((gamerData != null) &&
(gamerData.ShipColor == playerData.ShipColor))
{
return false;
}
}
return true;
}
///
/// Find the next unique color index among the players.
///
/// The current color index.
/// The network session.
/// The next unique color index.
public static byte GetNextUniqueColorIndex(byte currentColorIndex,
NetworkSession networkSession)
{
// safety-check the parameters
if ((currentColorIndex < 0) || (currentColorIndex >= ShipColors.Length))
{
throw new ArgumentOutOfRangeException("currentColorIndex");
}
if (networkSession == null)
{
throw new ArgumentNullException("networkSession");
}
if (networkSession.AllGamers.Count > ShipColors.Length)
{
throw new InvalidOperationException(
"There are more gamers than there are colors.");
}
// if there are as many gamers as there are colors, then we can't change
if (networkSession.AllGamers.Count == ShipColors.Length)
{
return currentColorIndex;
}
bool colorFound;
byte newColorIndex = currentColorIndex;
do
{
newColorIndex++;
if (newColorIndex >= ShipColors.Length)
{
newColorIndex = 0;
}
colorFound = false;
foreach (NetworkGamer networkGamer in networkSession.AllGamers)
{
PlayerData playerData = networkGamer.Tag as PlayerData;
if ((playerData != null) && (playerData.ShipColor == newColorIndex))
{
colorFound = true;
break;
}
}
}
while (colorFound && (newColorIndex != currentColorIndex));
return newColorIndex;
}
///
/// Find the previous unique color index among the players.
///
/// The current color index.
/// The network session..
/// The previous unique color index.
public static byte GetPreviousUniqueColorIndex(byte currentColorIndex,
NetworkSession networkSession)
{
// safety-check the parameters
if ((currentColorIndex < 0) || (currentColorIndex >= ShipColors.Length))
{
throw new ArgumentOutOfRangeException("currentColorIndex");
}
if (networkSession == null)
{
throw new ArgumentNullException("networkSession");
}
if (networkSession.AllGamers.Count > ShipColors.Length)
{
throw new InvalidOperationException(
"There are more gamers than there are colors.");
}
// if there are as many gamers as there are colors, then we can't change
if (networkSession.AllGamers.Count == ShipColors.Length)
{
return currentColorIndex;
}
bool colorFound;
byte newColorIndex = currentColorIndex;
do
{
if (newColorIndex == 0)
{
newColorIndex = (byte)(ShipColors.Length - 1);
}
else
{
newColorIndex--;
}
colorFound = false;
foreach (NetworkGamer networkGamer in networkSession.AllGamers)
{
PlayerData playerData = networkGamer.Tag as PlayerData;
if ((playerData != null) && (playerData.ShipColor == newColorIndex))
{
colorFound = true;
break;
}
}
}
while (colorFound && (newColorIndex != currentColorIndex));
return newColorIndex;
}
///
/// The number of variations in ships.
///
public static int Variations
{
get { return variations; }
}
#endregion
}
}