Pong/Scripts/Objects/Enemy.cs
Fries 65ef70f583 the enemy can now see when the screen is adjusted.
the scaling manager now controls when the various objects are active so stuff won't be null because of stuff running too fast or slow.
2023-06-02 17:15:44 -07:00

127 lines
3.6 KiB
C#

using System;
using System.Collections.Generic;
using Godot.Collections;
using Pong.Scripts.Data;
using Pong.Scripts.Managers;
namespace Pong.Scripts.Objects;
public partial class Enemy : BasePaddle
{
/// <summary>
/// draw shapes to the screen (like <see cref="_scanArea"/>).
/// </summary>
[Export] private bool _drawDebugShapes;
private CollisionShape2D _collisionShape;
private Rect2 _scanArea;
private PhysicsDirectSpaceState2D _spaceState;
private readonly Variant _collider = "collider";
private PhysicsShapeQueryParameters2D _query;
public override void _EnterTree()
{
base._EnterTree();
_collisionShape = GetNode<CollisionShape2D>("CollisionShape2D");
}
public override void _ExitTree()
{
_spaceState.Dispose();
_query.Dispose();
_collisionShape.Dispose();
}
internal void GenerateCollisions()
{
GenerateCastArea();
GenerateQuery();
}
private void GenerateQuery()
{
_query = new PhysicsShapeQueryParameters2D
{
Shape = new RectangleShape2D { Size = _scanArea.Size },
Exclude = new Array<Rid>(new[] { GetRid() })
};
}
public override void _IntegrateForces(PhysicsDirectBodyState2D state)
{
_spaceState ??= GetWorld2D().DirectSpaceState;
if (!Running) return;
if (NewPosition.HasValue)
{
state.Transform = new Transform2D(0, NewPosition.Value);
_query.Dispose();
GenerateCastArea();
GenerateQuery();
NewPosition = null;
}
Scan(GetPhysicsProcessDeltaTime(), state);
}
public override void _Draw()
{
if (!_drawDebugShapes) return;
DrawRect(_scanArea, Colors.Aqua);
}
private void Scan(double delta, PhysicsDirectBodyState2D state)
{
var result = _spaceState.IntersectShape(_query);
if (result.Count <= 0)
{
LinearVelocity = Vector2.Zero;
return;
}
TrackBall(delta, result, state);
}
/// <summary>
/// track the distance between the ball and the enemy paddle on the y axis and move velocity accordingly.
/// </summary>
/// <param name="delta">how long it took to complete the last frame in seconds. this should be constant as
/// should be executed in the physics process method which should be separate from the main frame rate.</param>
/// <param name="result">a dictionary of objects that collided with the cast. this method only works if
/// theres an object that has the Ball class.</param>
/// <param name="state"></param>
private void TrackBall(double delta, IReadOnlyList<Dictionary> result, PhysicsDirectBodyState2D state)
{
// checks if the collider is a ball, if not, return.
if (result[0][_collider].As<Ball>() is not { } ball) return;
// gets the sign of the distance between the ball and the paddle on the y axis
var normalisedDistance = new Vector2 { Y = Mathf.Sign(ball.Position.Y - Position.Y) };
var linearVelocity = normalisedDistance * MoveSpeed.ByMeter();
// lerp the velocity to smooth out jerky movement.
state.LinearVelocity = LinearVelocity.Lerp(linearVelocity, delta);
}
/// <summary>
/// generate the shape that the cast uses to detect collisions.
/// </summary>
/// <exception cref="InvalidOperationException">if the collision shape is not a rectangle shape.</exception>
private void GenerateCastArea()
{
if (_collisionShape.Shape is not RectangleShape2D shape)
throw new InvalidOperationException("the collision shape needs to be a rectangle shape");
var gameArea = ScalingManager.GameArea;
var verticalGrowSize = gameArea.Y.ByWallSizeExtents / 2;
// grow the area the enemy can see by around half of the screen area.
_scanArea = shape.GetRect().GrowSide(Side.Left, gameArea.X.ByWallSizeExtents)
.GrowIndividual(0, verticalGrowSize, 0, verticalGrowSize);
}
}