Ga naar inhoud

Weapons

Doelstelling

Wij willen verschillende manieren wapens die de speler kan gebruiken om op verschillende manieren het spel te spelen.

Toegepaste oplossing

BaseWeapon

Hiervoor heb ik een basis wapen gemaakt waarmee gemakkelijk veel verschillende soorten wapens gemaakt kunnen worden.

Elk wapen kan op de grond liggen, opgepakt worden, op de grond gelegd worden en natuurlijk gebruikt worden. Het grootste deel van de logica, dat voor de meeste wapens hetzelfde is, word hierin gedefinieerd zodat het makkelijk is om meerdere soorten wapens te maken.

public abstract class BaseWeapon : GameObject, IHasCollider
{
    public override GameObject? Parent {
        get => base.Parent;
        set {
            if (value is not GameObjectList && value is not BaseEntity && value is not null)
                throw new InvalidOperationException($"Parent ({value.GetType().FullName}) of weapon must either be a world, an entity that can hold weapons or null.");

            base.Parent = value;
        }
    }

    public virtual Texture2D Texture { get; set; }
    public Vector2 Size { get; set; } = new(25f);
    public override Rectangle BoundingBox
        => new((Position - Size/2).ToPoint(), Size.ToPoint());

    public TimeSpan AttackCooldown { get; set; } = TimeSpan.FromSeconds(1);
    public TimeSpan AttackDuration { get; set; } = TimeSpan.FromSeconds(1);
    public int Damage { get; set; } = 10;

    public TimeSpan LastAttackTime { get; set; } = TimeSpan.Zero;
    public TimeSpan AttackProgress
        => App.LastUpdateTime.TotalGameTime - LastAttackTime;
    public virtual bool IsAttacking
        => AttackProgress <= AttackDuration && Parent is BaseEntity;
    public float GroundRotation { get; set; } = 0;
    public Vector2 AttackDirection { get; set; } = Vector2.UnitX;
    public HashSet<IHasCollider> hits = new();


    public BaseWeapon(Vector2 position = default, int layer = 0, string id = "") : base(layer, id)
        => Position = position;

Hier bijvoorbeeld moet zelfs voor elke wapen een manier gegeven worden om het te tekenen, wanneer het niet op de grond ligt. Daarin kan dan ook de beweging van de aanval worden laten zien.

    protected abstract void DrawInHand(GameTime gameTime, SpriteBatch spriteBatch, BaseEntity holder);

    public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
    {
        if (Parent is BaseEntity)
        {
            DrawInHand(gameTime, spriteBatch, (BaseEntity)Parent);
        }
        else
        {
            spriteBatch.BetterDraw(Texture, GlobalPosition, Size, GroundRotation);
        }
    }

    public override void HandleInput(InputHelper input)
    {
        if (Parent is not BaseEntity holder || IsAttacking)
            return;

        AttackDirection = Vector2.Normalize(input.MousePosition - holder.GlobalPosition);
    }

    public void Equip(BaseEntity equipper)
    {
        if (Parent is GameObjectList list)
            list.RemoveLazy(this);

        Parent = equipper;
    }

    public void Unequip()
    {
        Position = Parent!.GlobalPosition - (Parent.Parent?.GlobalPosition ?? Vector2.Zero);
        GroundRotation = App.Random.NextSingle() * 2 * MathF.PI;

        Parent = Parent.Parent;
        if (Parent is GameObjectList list)
            list.Add(this);
    }

    public virtual bool Attack(GameTime gameTime)
    {
        if (AttackProgress > AttackDuration + AttackCooldown)
        {
            LastAttackTime = gameTime.TotalGameTime;
            hits.Clear();

            App.Systems.GetSystem<EventBus>()
                .Publish(new WeaponsUsedEvent((BaseEntity)Parent!, this));

            return true;
        }

        return false;
    }

    public override void Update(GameTime gameTime)
    {
        if (IsAttacking)
        {
            var state = App.Systems.GetSystem<StateManager>().currentState;
            var holder = (BaseEntity)Parent!;

            foreach (var hit in GetHits(gameTime, holder, state!))
            {
                if (hits.Contains(hit))
                {
                    continue;
                }

                hits.Add(hit);

                ColliderHit(hit, holder);
            }
        }
    }

En hier moet de manier waarop geraakte dingen gevonden worden aangepast. Het is niet heel anders dan wat in DrawInHand word gedaan, alleen dat de positionering word gebruikt voor een query in plaats van om iets te tekenen.

De logica zodat vijanden echt geraakt worden, niet de speler zelf en dat dezelfde vijand niet meer dan één keer word geraakt word hierbuiten gedaan.

    protected abstract IEnumerable<IHasCollider> GetHits(GameTime gameTime, BaseEntity holder, GameObjectList world);

    public virtual void ColliderHit(IHasCollider hit, BaseEntity holder)
    {
        if (hit != Parent && hit is BaseEntity entity)
            entity.Damage(Damage * holder.Strength);
    }
}

Wapens

De wapens zelf zijn dan gemaakt met de BaseWeapon en zijn daardoor niet heel interessant. Hier is een voorbeeld van de zwaard die een speciale zwaai beweging maakt.

public class Sword : BaseWeapon, IHasRectCollider
{
    public int armLength = 25;
    public int Range => (int)MathF.Round(Size.Length());
    public float swingAngle = MathF.PI * .65f;

    public Sword(Vector2 position = default, int layer = 0, string id = "")
        : base(position, layer, id)
    {
        Texture = App.AssetManager.GetSprite("Images/Weapons/Sword");
        AttackCooldown = TimeSpan.FromSeconds(.125);
        AttackDuration = TimeSpan.FromSeconds(.4);
        Damage = 3;
        Size = new(50f);
    }

    protected override void DrawInHand(GameTime gameTime, SpriteBatch spriteBatch, BaseEntity holder)
    {
        var angle = MathExtras.CartesianToPolar(AttackDirection.ToNumerics()).X;

        if (IsAttacking)
            angle += MathExtras.LinearConversion(
                (float)AttackProgress.TotalSeconds,
                0, (float)AttackDuration.TotalSeconds,
                -swingAngle/2, swingAngle/2
            );

        var dir = MathExtras.PolarToCartesian(new(angle, armLength + Range/2));

        spriteBatch.BetterDraw(Texture, holder.GlobalPosition + dir, Size, -angle - MathF.PI * .25f);
    }

    protected override IEnumerable<IHasCollider> GetHits(GameTime gameTime, BaseEntity holder, GameObjectList world)
    {
        var attackAngle = MathExtras.CartesianToPolar(AttackDirection.ToNumerics()).X;

        var attackDirection = MathExtras.PolarToCartesian(new(
                attackAngle + MathExtras.LinearConversion(
                    (float)AttackProgress.TotalSeconds,
                    0, (float)AttackDuration.TotalSeconds,
                    -swingAngle/2, swingAngle/2
                ),
                1
            ));

        var attackLine = new Line(
            holder.GlobalPosition + attackDirection * armLength,
            holder.GlobalPosition + attackDirection * (armLength + Range)
        );

        App.Systems.GetSystem<GizmoSystem>()
            .AddLine(attackLine, Color.Red);

        return CollisionHandler.QueryLine(attackLine, world);
    }
}

Klassendiagram

classDiagram BaseWeapon --|> GameObject class BaseWeapon { <<abstract>> +TimeSpan AttackCooldown +TimeSpan AttackDuration +int Damage +TimeSpan LastAttackTime +TimeSpan AttackProgress +bool IsAttacking +Vector2 AttackDirection +HashSet~IHasCollider~ hits +float GroundRotation +DrawInHand(GameTime, SpriteBatch, BaseEntity)* +Draw(GameTime, SpriteBatch) +Equip(BaseEntity) +Unequip() +bool Attack(GameTime) +IEnumerable~IHasCollider~ GetHits(GameTime, BaseEntity, GameObjectList)* +ColliderHit(IHasCollider, BaseEntity) } Fists --|> BaseWeapon Knife --|> BaseWeapon Sword --|> BaseWeapon Hammer --|> BaseWeapon

Bronnen


Laatst geüpdatet: June 9, 2024
Gecreëerd: April 16, 2024