From 4200a3e7df187829aad5bb38aae0c6df07eb3e4c Mon Sep 17 00:00:00 2001 From: Fries Date: Wed, 17 May 2023 23:41:26 -0700 Subject: [PATCH] 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. --- BouncyMaterial.tres | 5 +++ Scenes/Enemy.tscn | 23 +++++++++++ Scenes/Paddle.tscn | 7 +--- Scenes/Pong.tscn | 9 ++++- Scripts/Ball.cs | 9 +++-- Scripts/Enemy.cs | 90 +++++++++++++++++++++++++++++++++++++++++ Scripts/Paddle.cs | 2 +- Scripts/SceneManager.cs | 14 +++++++ project.godot | 5 +++ 9 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 BouncyMaterial.tres create mode 100644 Scenes/Enemy.tscn create mode 100644 Scripts/Enemy.cs create mode 100644 Scripts/SceneManager.cs diff --git a/BouncyMaterial.tres b/BouncyMaterial.tres new file mode 100644 index 0000000..3df6451 --- /dev/null +++ b/BouncyMaterial.tres @@ -0,0 +1,5 @@ +[gd_resource type="PhysicsMaterial" format=3 uid="uid://e05n66x8ug77"] + +[resource] +friction = 0.0 +bounce = 1.25 diff --git a/Scenes/Enemy.tscn b/Scenes/Enemy.tscn new file mode 100644 index 0000000..f97d24b --- /dev/null +++ b/Scenes/Enemy.tscn @@ -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") diff --git a/Scenes/Paddle.tscn b/Scenes/Paddle.tscn index e631d5b..0310304 100644 --- a/Scenes/Paddle.tscn +++ b/Scenes/Paddle.tscn @@ -1,11 +1,8 @@ [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"] -[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_r5a55"] -friction = 0.0 -bounce = 1.0 - [sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_iw3nx"] size = Vector2(50, 75) @@ -13,7 +10,7 @@ size = Vector2(50, 75) size = Vector2(12, 150) [node name="Paddle" type="RigidBody2D"] -physics_material_override = SubResource("PhysicsMaterial_r5a55") +physics_material_override = ExtResource("1_76uik") gravity_scale = 0.0 lock_rotation = true script = ExtResource("1_uv7s3") diff --git a/Scenes/Pong.tscn b/Scenes/Pong.tscn index 8323a60..73e9b48 100644 --- a/Scenes/Pong.tscn +++ b/Scenes/Pong.tscn @@ -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="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://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"] +script = ExtResource("1_m8437") [node name="Paddle" parent="." instance=ExtResource("1_5rs0o")] position = Vector2(-350, 0) @@ -18,3 +21,7 @@ _ballSpeed = 50.0 _maxRandomAngle = 0.42 [node name="Walls" parent="." instance=ExtResource("3_jfis7")] + +[node name="Enemy" parent="." instance=ExtResource("4_uwvof")] +position = Vector2(350, 0) +_moveSpeed = 50.0 diff --git a/Scripts/Ball.cs b/Scripts/Ball.cs index 59b0518..78518bb 100644 --- a/Scripts/Ball.cs +++ b/Scripts/Ball.cs @@ -10,7 +10,7 @@ public partial class Ball : CharacterBody2D private Vector2 _velocity; /// - /// this property multiples the ballSpeed by the Meter constant. + /// this property multiples the ballSpeed by the Meter constant. /// private double BallSpeed => _ballSpeed * Constants.Meter; @@ -32,7 +32,7 @@ public partial class Ball : CharacterBody2D /// /// this method moves the ball and bounces if it collides with something. /// - /// delta time from the _PhysicsProcess method. + /// delta time from the _PhysicsProcess method. private void CollisionCheck(double delta) { var collision = MoveAndCollide(_velocity * delta); @@ -42,9 +42,10 @@ public partial class Ball : CharacterBody2D /// /// this method generates a random number between 0 and 1 and - /// either returns Vector2.Left or Vector2.Right based on that number. + /// either returns Vector2.Left or Vector2.Right + /// based on that number. /// - private Vector2 GetRandomStartingDirection() { + private static Vector2 GetRandomStartingDirection() { using var rng = new RandomNumberGenerator(); rng.Randomize(); var range = rng.RandiRange(0,1); diff --git a/Scripts/Enemy.cs b/Scripts/Enemy.cs new file mode 100644 index 0000000..5f5e2fd --- /dev/null +++ b/Scripts/Enemy.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using Godot; +using Godot.Collections; + +namespace Pong.Scripts; + +public partial class Enemy : RigidBody2D +{ + /// + /// draw shapes to the screen (like ). + /// + [Export] private bool _drawDebugShapes; + + [Export] private double _moveSpeed; + + private CollisionShape2D _collisionShape; + private Rect2 _scanArea; + + public override void _Ready() + { + _collisionShape = GetNode("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(new[] { GetRid() }) + }; + + var result = spaceState.IntersectShape(query); + + if (result.Count <= 0) + { + LinearVelocity = Vector2.Zero; + return; + } + + TrackBall(delta, result); + } + + /// + /// 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) + { + // 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(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); + } + + /// + /// 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"); + + // 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); + } +} diff --git a/Scripts/Paddle.cs b/Scripts/Paddle.cs index 9949560..a2ff130 100644 --- a/Scripts/Paddle.cs +++ b/Scripts/Paddle.cs @@ -9,7 +9,7 @@ public partial class Paddle : RigidBody2D private double _verticalInput; /// - /// property that multiples the moveSpeed by the Meter constant. + /// property that multiples the moveSpeed by the Meter constant. /// private double MoveSpeed => _moveSpeed * Constants.Meter; diff --git a/Scripts/SceneManager.cs b/Scripts/SceneManager.cs new file mode 100644 index 0000000..b0472c5 --- /dev/null +++ b/Scripts/SceneManager.cs @@ -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(); + } + } +} diff --git a/project.godot b/project.godot index 984e028..2b8af3d 100644 --- a/project.godot +++ b/project.godot @@ -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) ] } +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]