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 { /// /// draw shapes to the screen (like ). /// [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"); } 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(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); } /// /// track the distance between the ball and the enemy paddle on the y axis and move velocity accordingly. /// /// 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. /// a dictionary of objects that collided with the cast. this method only works if /// theres an object that has the Ball class. /// private void TrackBall(double delta, IReadOnlyList result, PhysicsDirectBodyState2D state) { // checks if the collider is a ball, if not, return. if (result[0][_collider].As() 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); } /// /// generate the shape that the cast uses to detect collisions. /// /// if the collision shape is not a rectangle shape. 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); } }