add enemy ai to the game

the enemy can now track the player with a cast that uses a shape generated in code. the enemy moves using some fancy math that gets the sign of the distance between the ball and the enemy paddles y position to get a normalised number for velocity and the velocity is lerped to smooth out any janky movement.
This commit is contained in:
Fries 2023-05-17 23:41:26 -07:00
parent e50f0aefb1
commit 4200a3e7df
9 changed files with 153 additions and 11 deletions

5
BouncyMaterial.tres Normal file
View file

@ -0,0 +1,5 @@
[gd_resource type="PhysicsMaterial" format=3 uid="uid://e05n66x8ug77"]
[resource]
friction = 0.0
bounce = 1.25

23
Scenes/Enemy.tscn Normal file
View file

@ -0,0 +1,23 @@
[gd_scene load_steps=5 format=3 uid="uid://krt6x241s3x6"]
[ext_resource type="PhysicsMaterial" uid="uid://e05n66x8ug77" path="res://BouncyMaterial.tres" id="1_e3kk5"]
[ext_resource type="Script" path="res://Scripts/Enemy.cs" id="1_fbrtv"]
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_iw3nx"]
size = Vector2(50, 75)
[sub_resource type="RectangleShape2D" id="RectangleShape2D_dbik4"]
size = Vector2(12, 150)
[node name="Enemy" type="RigidBody2D"]
physics_material_override = ExtResource("1_e3kk5")
gravity_scale = 0.0
lock_rotation = true
script = ExtResource("1_fbrtv")
[node name="Sprite" type="Sprite2D" parent="."]
scale = Vector2(0.25, 2)
texture = SubResource("PlaceholderTexture2D_iw3nx")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_dbik4")

View file

@ -1,11 +1,8 @@
[gd_scene load_steps=5 format=3 uid="uid://bklo6torhapa0"] [gd_scene load_steps=5 format=3 uid="uid://bklo6torhapa0"]
[ext_resource type="PhysicsMaterial" uid="uid://e05n66x8ug77" path="res://BouncyMaterial.tres" id="1_76uik"]
[ext_resource type="Script" path="res://Scripts/Paddle.cs" id="1_uv7s3"] [ext_resource type="Script" path="res://Scripts/Paddle.cs" id="1_uv7s3"]
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_r5a55"]
friction = 0.0
bounce = 1.0
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_iw3nx"] [sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_iw3nx"]
size = Vector2(50, 75) size = Vector2(50, 75)
@ -13,7 +10,7 @@ size = Vector2(50, 75)
size = Vector2(12, 150) size = Vector2(12, 150)
[node name="Paddle" type="RigidBody2D"] [node name="Paddle" type="RigidBody2D"]
physics_material_override = SubResource("PhysicsMaterial_r5a55") physics_material_override = ExtResource("1_76uik")
gravity_scale = 0.0 gravity_scale = 0.0
lock_rotation = true lock_rotation = true
script = ExtResource("1_uv7s3") script = ExtResource("1_uv7s3")

View file

@ -1,10 +1,13 @@
[gd_scene load_steps=4 format=3 uid="uid://kmfgtiugs4m0"] [gd_scene load_steps=6 format=3 uid="uid://kmfgtiugs4m0"]
[ext_resource type="PackedScene" uid="uid://bklo6torhapa0" path="res://Scenes/Paddle.tscn" id="1_5rs0o"] [ext_resource type="PackedScene" uid="uid://bklo6torhapa0" path="res://Scenes/Paddle.tscn" id="1_5rs0o"]
[ext_resource type="Script" path="res://Scripts/SceneManager.cs" id="1_m8437"]
[ext_resource type="PackedScene" uid="uid://cggi01qnnlnwg" path="res://Scenes/Ball.tscn" id="2_u2ksv"] [ext_resource type="PackedScene" uid="uid://cggi01qnnlnwg" path="res://Scenes/Ball.tscn" id="2_u2ksv"]
[ext_resource type="PackedScene" uid="uid://c5n541vsuvfk8" path="res://Scenes/Walls.tscn" id="3_jfis7"] [ext_resource type="PackedScene" uid="uid://c5n541vsuvfk8" path="res://Scenes/Walls.tscn" id="3_jfis7"]
[ext_resource type="PackedScene" uid="uid://krt6x241s3x6" path="res://Scenes/Enemy.tscn" id="4_uwvof"]
[node name="Pong" type="Node2D"] [node name="Pong" type="Node2D"]
script = ExtResource("1_m8437")
[node name="Paddle" parent="." instance=ExtResource("1_5rs0o")] [node name="Paddle" parent="." instance=ExtResource("1_5rs0o")]
position = Vector2(-350, 0) position = Vector2(-350, 0)
@ -18,3 +21,7 @@ _ballSpeed = 50.0
_maxRandomAngle = 0.42 _maxRandomAngle = 0.42
[node name="Walls" parent="." instance=ExtResource("3_jfis7")] [node name="Walls" parent="." instance=ExtResource("3_jfis7")]
[node name="Enemy" parent="." instance=ExtResource("4_uwvof")]
position = Vector2(350, 0)
_moveSpeed = 50.0

View file

@ -10,7 +10,7 @@ public partial class Ball : CharacterBody2D
private Vector2 _velocity; private Vector2 _velocity;
/// <summary> /// <summary>
/// this property multiples the ballSpeed by the Meter constant. /// this property multiples the ballSpeed by the <see cref="Constants.Meter">Meter</see> constant.
/// </summary> /// </summary>
private double BallSpeed => _ballSpeed * Constants.Meter; private double BallSpeed => _ballSpeed * Constants.Meter;
@ -32,7 +32,7 @@ public partial class Ball : CharacterBody2D
/// <summary> /// <summary>
/// this method moves the ball and bounces if it collides with something. /// this method moves the ball and bounces if it collides with something.
/// </summary> /// </summary>
/// <param name="delta">delta time from the _PhysicsProcess method.</param> /// <param name="delta">delta time from the <see cref="_PhysicsProcess">_PhysicsProcess</see> method.</param>
private void CollisionCheck(double delta) private void CollisionCheck(double delta)
{ {
var collision = MoveAndCollide(_velocity * delta); var collision = MoveAndCollide(_velocity * delta);
@ -42,9 +42,10 @@ public partial class Ball : CharacterBody2D
/// <summary> /// <summary>
/// this method generates a random number between 0 and 1 and /// this method generates a random number between 0 and 1 and
/// either returns Vector2.Left or Vector2.Right based on that number. /// either returns <see cref="Vector2.Left">Vector2.Left</see> or <see cref="Vector2.Right">Vector2.Right</see>
/// based on that number.
/// </summary> /// </summary>
private Vector2 GetRandomStartingDirection() { private static Vector2 GetRandomStartingDirection() {
using var rng = new RandomNumberGenerator(); using var rng = new RandomNumberGenerator();
rng.Randomize(); rng.Randomize();
var range = rng.RandiRange(0,1); var range = rng.RandiRange(0,1);

90
Scripts/Enemy.cs Normal file
View file

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using Godot;
using Godot.Collections;
namespace Pong.Scripts;
public partial class Enemy : RigidBody2D
{
/// <summary>
/// draw shapes to the screen (like <see cref="_scanArea"/>).
/// </summary>
[Export] private bool _drawDebugShapes;
[Export] private double _moveSpeed;
private CollisionShape2D _collisionShape;
private Rect2 _scanArea;
public override void _Ready()
{
_collisionShape = GetNode<CollisionShape2D>("CollisionShape2D");
GenerateCastArea();
}
public override void _PhysicsProcess(double delta)
{
Scan(delta);
}
public override void _Draw()
{
if (!_drawDebugShapes) return;
DrawRect(_scanArea, Colors.Aqua);
}
private void Scan(double delta)
{
var spaceState = GetWorld2D().DirectSpaceState;
var query = new PhysicsShapeQueryParameters2D
{
Shape = new RectangleShape2D { Size = _scanArea.Size },
Exclude = new Array<Rid>(new[] { GetRid() })
};
var result = spaceState.IntersectShape(query);
if (result.Count <= 0)
{
LinearVelocity = Vector2.Zero;
return;
}
TrackBall(delta, result);
}
/// <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>
private void TrackBall(double delta, IReadOnlyList<Dictionary> result)
{
// 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(0, Mathf.Sign(ball.Position.Y - Position.Y));
var linearVelocity = normalisedDistance * _moveSpeed * Constants.Meter;
// lerp the velocity to smooth out jerky movement.
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");
// grow the area the enemy can see by around half of the screen area.
_scanArea = shape.GetRect().GrowSide(Side.Left, 400).GrowIndividual(0, 190, 0, 190);
}
}

View file

@ -9,7 +9,7 @@ public partial class Paddle : RigidBody2D
private double _verticalInput; private double _verticalInput;
/// <summary> /// <summary>
/// property that multiples the moveSpeed by the Meter constant. /// property that multiples the moveSpeed by the <see cref="Constants.Meter">Meter</see> constant.
/// </summary> /// </summary>
private double MoveSpeed => _moveSpeed * Constants.Meter; private double MoveSpeed => _moveSpeed * Constants.Meter;

14
Scripts/SceneManager.cs Normal file
View file

@ -0,0 +1,14 @@
using Godot;
namespace Pong.Scripts;
public partial class SceneManager : Node2D
{
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("scene_reload"))
{
GetTree().ReloadCurrentScene();
}
}
}

View file

@ -43,6 +43,11 @@ paddle_down={
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"echo":false,"script":null) , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"echo":false,"script":null)
] ]
} }
scene_reload={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"echo":false,"script":null)
]
}
[rendering] [rendering]