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:
parent
e50f0aefb1
commit
4200a3e7df
9 changed files with 153 additions and 11 deletions
5
BouncyMaterial.tres
Normal file
5
BouncyMaterial.tres
Normal 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
23
Scenes/Enemy.tscn
Normal 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")
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
90
Scripts/Enemy.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
14
Scripts/SceneManager.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue