knockback component

This commit is contained in:
2026-01-18 12:39:01 +01:00
parent 9690280cd7
commit 35b9ea383c
21 changed files with 180 additions and 49 deletions

View File

@@ -2,7 +2,7 @@ using Godot;
namespace Movementtests.interfaces; namespace Movementtests.interfaces;
public interface IDamageMaker public interface IDamageDealer
{ {
[Export] [Export]
RDamage RDamage { get; set; } RDamage RDamage { get; set; }

View File

@@ -1,9 +1,12 @@
using System; using System;
using Godot;
namespace Movementtests.interfaces; namespace Movementtests.interfaces;
public record DamageRecord(Node3D Source, RDamage Damage);
public interface IDamageable public interface IDamageable
{ {
event Action<IDamageable, float> DamageTaken; event Action<IDamageable, DamageRecord> DamageTaken;
float TakeDamage(RDamage damage); DamageRecord TakeDamage(DamageRecord damageRecord);
} }

View File

@@ -12,5 +12,5 @@ public interface IHealthable
float CurrentHealth { get; set; } float CurrentHealth { get; set; }
void ReduceHealth(IDamageable source, float amount); void ReduceHealth(IDamageable source, DamageRecord damageRecord);
} }

View File

@@ -1,6 +1,11 @@
using Godot;
namespace Movementtests.interfaces; namespace Movementtests.interfaces;
public interface IKnockbackable public interface IKnockbackable
{ {
[Export] RKnockback RKnockback { get; set;}
public void RegisterKnockback(IDamageable source, DamageRecord damageRecord);
public Vector3 ComputeKnockback();
} }

View File

@@ -16,7 +16,6 @@
[sub_resource type="Resource" id="Resource_2e4ci"] [sub_resource type="Resource" id="Resource_2e4ci"]
script = ExtResource("2_sysok") script = ExtResource("2_sysok")
DamageDealt = 10.0
metadata/_custom_type_script = "uid://jitubgv6judn" metadata/_custom_type_script = "uid://jitubgv6judn"
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_0xm2m"] [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_0xm2m"]
@@ -145,6 +144,7 @@ MovementInputs = ExtResource("7_caohq")
HealthInputs = SubResource("Resource_ybosk") HealthInputs = SubResource("Resource_ybosk")
DamageInputs = ExtResource("9_dmw1t") DamageInputs = ExtResource("9_dmw1t")
Target = NodePath("../Player") Target = NodePath("../Player")
IsActiveOnStart = false
[node name="FlyingSpawner" parent="." node_paths=PackedStringArray("Target") instance=ExtResource("6_7m3bq")] [node name="FlyingSpawner" parent="." node_paths=PackedStringArray("Target") instance=ExtResource("6_7m3bq")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 18, 11, -14) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 18, 11, -14)
@@ -153,3 +153,4 @@ MovementInputs = ExtResource("10_spw1u")
HealthInputs = ExtResource("11_2e4ci") HealthInputs = ExtResource("11_2e4ci")
DamageInputs = ExtResource("9_gp7s3") DamageInputs = ExtResource("9_gp7s3")
Target = NodePath("../Player") Target = NodePath("../Player")
IsActiveOnStart = false

View File

@@ -7,7 +7,9 @@ using Movementtests.systems;
using Movementtests.player_controller.Scripts; using Movementtests.player_controller.Scripts;
using RustyOptions; using RustyOptions;
public partial class PlayerController : CharacterBody3D, IDamageable, IKnockbackable, IDamageMaker public partial class PlayerController : CharacterBody3D,
IDamageable,
IDamageDealer
{ {
// Enums // Enums
public enum AllowedInputs public enum AllowedInputs
@@ -33,7 +35,7 @@ public partial class PlayerController : CharacterBody3D, IDamageable, IKnockback
// Signals and events // // Signals and events //
/////////////////////////// ///////////////////////////
public event Action<IDamageable, float> DamageTaken; public event Action<IDamageable, DamageRecord> DamageTaken;
/////////////////////////// ///////////////////////////
// Public stuff // // Public stuff //
@@ -1735,9 +1737,9 @@ public partial class PlayerController : CharacterBody3D, IDamageable, IKnockback
} }
} }
public float TakeDamage(RDamage damage) public DamageRecord TakeDamage(DamageRecord damageRecord)
{ {
var finalDamage = CDamage.TakeDamage(damage); var finalDamage = CDamage.TakeDamage(damageRecord);
DamageTaken?.Invoke(this, finalDamage); DamageTaken?.Invoke(this, finalDamage);
return finalDamage; return finalDamage;
} }
@@ -1757,7 +1759,7 @@ public partial class PlayerController : CharacterBody3D, IDamageable, IKnockback
foreach (var body in bodies) foreach (var body in bodies)
{ {
if(body is IDamageable spawnable) if(body is IDamageable spawnable)
spawnable.TakeDamage(RDamage); spawnable.TakeDamage(new DamageRecord(this, RDamage));
} }
} }
} }

View File

@@ -6,7 +6,7 @@ using Movementtests.systems.damage;
[GlobalClass] [GlobalClass]
public partial class RDamageModifier : Resource, IDamageable public partial class RDamageModifier : Resource, IDamageable
{ {
public event Action<IDamageable, float> DamageTaken; public event Action<IDamageable, DamageRecord> DamageTaken;
[Export] [Export]
public EDamageTypes DamageType = EDamageTypes.Normal; public EDamageTypes DamageType = EDamageTypes.Normal;
@@ -24,11 +24,14 @@ public partial class RDamageModifier : Resource, IDamageable
DamageType = damageType; DamageType = damageType;
} }
public float TakeDamage(RDamage damage) public DamageRecord TakeDamage(DamageRecord damageRecord)
{ {
if (damage.DamageType != DamageType) return 0; if (damageRecord.Damage.DamageType != DamageType)
var finalDamage = damage.DamageDealt * Modifier; return damageRecord with { Damage = new RDamage(0, damageRecord.Damage.DamageType) };
DamageTaken?.Invoke(this, finalDamage);
return finalDamage; var finalDamage = damageRecord.Damage.DamageDealt * Modifier;
var finalDamageRecord = damageRecord with { Damage = new RDamage(finalDamage, damageRecord.Damage.DamageType) };
DamageTaken?.Invoke(this, finalDamageRecord);
return finalDamageRecord;
} }
} }

View File

@@ -0,0 +1,19 @@
using Godot;
using System;
using Movementtests.interfaces;
[GlobalClass]
public partial class RKnockback : Resource
{
[Export]
public float Modifier = 1.0f;
public RKnockback()
{
Modifier = 1.0f;
}
public RKnockback(float modifier)
{
Modifier = modifier;
}
}

View File

@@ -0,0 +1 @@
uid://b44cse62qru7j

View File

@@ -6,6 +6,8 @@ public partial class RMovement : Resource
{ {
[Export(PropertyHint.Range, "0,10,0.1,or_greater")] [Export(PropertyHint.Range, "0,10,0.1,or_greater")]
public float Speed; public float Speed;
[Export(PropertyHint.Range, "0,10,0.1,or_greater")]
public float Acceleration;
[Export(PropertyHint.Range, "0,20,1,or_greater")] [Export(PropertyHint.Range, "0,20,1,or_greater")]
public float TargetHeight; public float TargetHeight;
@@ -13,11 +15,13 @@ public partial class RMovement : Resource
public RMovement() public RMovement()
{ {
Speed = 3.0f; Speed = 3.0f;
Acceleration = 1.0f;
TargetHeight = 10.0f; TargetHeight = 10.0f;
} }
public RMovement(float speed, float targetHeight) public RMovement(float speed, float acceleration, float targetHeight)
{ {
Speed = speed; Speed = speed;
Acceleration = acceleration;
TargetHeight = targetHeight; TargetHeight = targetHeight;
} }
} }

View File

@@ -5,18 +5,19 @@ using Movementtests.interfaces;
[GlobalClass] [GlobalClass]
public partial class CDamageable : Node, IDamageable public partial class CDamageable : Node, IDamageable
{ {
public event Action<IDamageable, float> DamageTaken; public event Action<IDamageable, DamageRecord> DamageTaken;
[Export] [Export]
public RDamageModifier[] DamageModifiers { get; set; } public RDamageModifier[] DamageModifiers { get; set; }
public float TakeDamage(RDamage damage) public DamageRecord TakeDamage(DamageRecord damageRecord)
{ {
var finalDamage = 0f; var finalDamage = 0f;
foreach (var damageable in DamageModifiers.ToIDamageables()) foreach (var damageable in DamageModifiers.ToIDamageables())
finalDamage += damageable.TakeDamage(damage); finalDamage += damageable.TakeDamage(damageRecord).Damage.DamageDealt;
DamageTaken?.Invoke(this, finalDamage); var finalDamageRecord = damageRecord with { Damage = new RDamage(finalDamage, damageRecord.Damage.DamageType) };
return finalDamage; DamageTaken?.Invoke(this, finalDamageRecord);
return finalDamageRecord;
} }
} }

View File

@@ -3,37 +3,53 @@ using Godot;
using Movementtests.interfaces; using Movementtests.interfaces;
[GlobalClass] [GlobalClass]
public partial class Enemy : CharacterBody3D, IDamageable, IDamageMaker, IHealthable, IKillable, IMoveable, ISpawnable public partial class Enemy : CharacterBody3D,
IDamageable,
IDamageDealer,
IHealthable,
IKillable,
IMoveable,
ISpawnable,
IKnockbackable
{ {
public event Action<IDamageable, float> DamageTaken; // Signals and events
public event Action<IDamageable, DamageRecord> DamageTaken;
public event Action<IHealthable, float> HealthChanged; public event Action<IHealthable, float> HealthChanged;
public event Action<IHealthable> HealthDepleted; public event Action<IHealthable> HealthDepleted;
// Public export components
[Export] [Export]
public Node3D Target { get; set; } public Node3D Target { get; set; }
[ExportGroup("Health")]
[Export] [Export]
public Node CHealth { get; set; } public Node CHealth { get; set; }
[Export] [Export]
public RHealth RHealth { get; set; } public RHealth RHealth { get; set; }
public float CurrentHealth { get; set; } [Export]
public RDeathEffect[] DeathEffects { get; set; }
[ExportGroup("Damage")]
[Export] [Export]
public Node CDamage { get; set; } public Node CDamage { get; set; }
[Export] [Export]
public RDamage RDamage { get; set; } public RDamage RDamage { get; set; }
[Export]
public Node CKnockback { get; set; }
[Export]
public RKnockback RKnockback { get; set; }
[ExportGroup("Movement")]
[Export] [Export]
public Node CMovement { get; set; } public Node CMovement { get; set; }
[Export] [Export]
public RMovement RMovement { get; set; } public RMovement RMovement { get; set; }
// Public stuff
public float CurrentHealth { get; set; }
[Export] // Private stuff
public RDeathEffect[] DeathEffects { get; set; }
private Area3D _damageBox; private Area3D _damageBox;
public override void _Ready() public override void _Ready()
@@ -52,12 +68,17 @@ public partial class Enemy : CharacterBody3D, IDamageable, IDamageMaker, IHealth
healthable.RHealth = RHealth; healthable.RHealth = RHealth;
healthable.CurrentHealth = RHealth.StartingHealth; healthable.CurrentHealth = RHealth.StartingHealth;
} }
if (CKnockback is IKnockbackable knockbackable && RKnockback != null) knockbackable.RKnockback = RKnockback;
} }
public void SetupSignals() public void SetupSignals()
{ {
_damageBox.BodyEntered += OnDamageBoxTriggered; _damageBox.BodyEntered += OnDamageBoxTriggered;
if (CDamage is IDamageable damageable) damageable.DamageTaken += ReduceHealth; if (CDamage is IDamageable damageable)
{
damageable.DamageTaken += ReduceHealth;
damageable.DamageTaken += RegisterKnockback;
}
if (CHealth is IHealthable healthable) healthable.HealthDepleted += Kill; if (CHealth is IHealthable healthable) healthable.HealthDepleted += Kill;
} }
@@ -74,6 +95,7 @@ public partial class Enemy : CharacterBody3D, IDamageable, IDamageMaker, IHealth
delta: delta delta: delta
); );
Velocity = ComputeVelocity(inputs); Velocity = ComputeVelocity(inputs);
Velocity += ComputeKnockback();
MoveAndSlide(); MoveAndSlide();
} }
@@ -85,25 +107,28 @@ public partial class Enemy : CharacterBody3D, IDamageable, IDamageMaker, IHealth
public void OnDamageBoxTriggered(Node3D body) public void OnDamageBoxTriggered(Node3D body)
{ {
GD.Print("Take this!");
if (body is not IDamageable damageable) return; if (body is not IDamageable damageable) return;
damageable.TakeDamage(RDamage);
var damageRecord = new DamageRecord(this, RDamage);
damageable.TakeDamage(damageRecord);
} }
public float TakeDamage(RDamage damage) public DamageRecord TakeDamage(DamageRecord damageRecord)
{ {
GD.Print("Ouch!"); if (CDamage is not IDamageable damageable)
if (CDamage is not IDamageable damageable) return 0; return damageRecord with { Damage = new RDamage(0, damageRecord.Damage.DamageType) };
var finalDamage = damageable.TakeDamage(damage); var finalDamage = damageable.TakeDamage(damageRecord);
DamageTaken?.Invoke(this, damage.DamageDealt); DamageTaken?.Invoke(this, finalDamage);
GD.Print($"Received damage: {finalDamage.Damage.DamageDealt}");
return finalDamage; return finalDamage;
} }
public void ReduceHealth(IDamageable source, float amount) public void ReduceHealth(IDamageable source, DamageRecord damageRecord)
{ {
if (CHealth is not IHealthable healthable) return; if (CHealth is not IHealthable healthable) return;
healthable.ReduceHealth(source, amount); healthable.ReduceHealth(source, damageRecord);
} }
public void Kill(IHealthable source) public void Kill(IHealthable source)
@@ -114,4 +139,16 @@ public partial class Enemy : CharacterBody3D, IDamageable, IDamageMaker, IHealth
} }
QueueFree(); QueueFree();
} }
public void RegisterKnockback(IDamageable source, DamageRecord damageRecord)
{
if (CKnockback is not IKnockbackable knockbackable) return;
knockbackable.RegisterKnockback(source, damageRecord);
}
public Vector3 ComputeKnockback()
{
if (CKnockback is not IKnockbackable knockbackable) return Vector3.Zero;
return knockbackable.ComputeKnockback();
}
} }

View File

@@ -1,4 +1,4 @@
[gd_scene load_steps=17 format=3 uid="uid://dxt0e2ugmttqq"] [gd_scene load_steps=19 format=3 uid="uid://dxt0e2ugmttqq"]
[ext_resource type="Script" uid="uid://bn7sc6id7n166" path="res://scenes/enemies/Enemy.cs" id="1_r6506"] [ext_resource type="Script" uid="uid://bn7sc6id7n166" path="res://scenes/enemies/Enemy.cs" id="1_r6506"]
[ext_resource type="Resource" uid="uid://otfc2snh8umc" path="res://scenes/enemies/grounded_enemy/grounded_enemy_damage.tres" id="2_bn56u"] [ext_resource type="Resource" uid="uid://otfc2snh8umc" path="res://scenes/enemies/grounded_enemy/grounded_enemy_damage.tres" id="2_bn56u"]
@@ -9,9 +9,12 @@
[ext_resource type="Script" uid="uid://b0u23nkpaimyc" path="res://scenes/damage/CDamageable.cs" id="7_1tw73"] [ext_resource type="Script" uid="uid://b0u23nkpaimyc" path="res://scenes/damage/CDamageable.cs" id="7_1tw73"]
[ext_resource type="PackedScene" uid="uid://dbr7ioio158ew" path="res://scenes/movement/CGroundedMovement.tscn" id="7_qyswd"] [ext_resource type="PackedScene" uid="uid://dbr7ioio158ew" path="res://scenes/movement/CGroundedMovement.tscn" id="7_qyswd"]
[ext_resource type="Script" uid="uid://dtpxijlnb2c5" path="res://resource_definitions/RMovement.cs" id="8_6d4gl"] [ext_resource type="Script" uid="uid://dtpxijlnb2c5" path="res://resource_definitions/RMovement.cs" id="8_6d4gl"]
[ext_resource type="PackedScene" uid="uid://bctpe34ddamg5" path="res://scenes/knockback/CKnockback.tscn" id="10_jqqi6"]
[ext_resource type="Resource" uid="uid://cektf6waf4s04" path="res://scenes/enemies/grounded_enemy/grounded_enemy_knockback.tres" id="11_8k3xb"]
[sub_resource type="Resource" id="Resource_qj0ob"] [sub_resource type="Resource" id="Resource_qj0ob"]
script = ExtResource("2_r3cnf") script = ExtResource("2_r3cnf")
Modifier = 3.0
metadata/_custom_type_script = "uid://b6y3ugfydvch0" metadata/_custom_type_script = "uid://b6y3ugfydvch0"
[sub_resource type="Resource" id="Resource_6d4gl"] [sub_resource type="Resource" id="Resource_6d4gl"]
@@ -35,17 +38,19 @@ albedo_color = Color(0.06469653, 0.06469653, 0.06469653, 1)
[sub_resource type="BoxShape3D" id="BoxShape3D_4yfjf"] [sub_resource type="BoxShape3D" id="BoxShape3D_4yfjf"]
size = Vector3(1, 2, 2) size = Vector3(1, 2, 2)
[node name="GroundedEnemy" type="CharacterBody3D" node_paths=PackedStringArray("CHealth", "CDamage", "CMovement")] [node name="GroundedEnemy" type="CharacterBody3D" node_paths=PackedStringArray("CHealth", "CDamage", "CKnockback", "CMovement")]
collision_layer = 16 collision_layer = 16
collision_mask = 273 collision_mask = 273
script = ExtResource("1_r6506") script = ExtResource("1_r6506")
CHealth = NodePath("CHealth") CHealth = NodePath("CHealth")
RHealth = ExtResource("2_w4lm8") RHealth = ExtResource("2_w4lm8")
DeathEffects = Array[Object]([])
CDamage = NodePath("CDamageable") CDamage = NodePath("CDamageable")
RDamage = ExtResource("2_bn56u") RDamage = ExtResource("2_bn56u")
CKnockback = NodePath("CKnockback")
RKnockback = ExtResource("11_8k3xb")
CMovement = NodePath("CGroundedMovement") CMovement = NodePath("CGroundedMovement")
RMovement = ExtResource("4_na24f") RMovement = ExtResource("4_na24f")
DeathEffects = Array[Object]([])
[node name="CHealth" type="Node" parent="."] [node name="CHealth" type="Node" parent="."]
script = ExtResource("2_gsmti") script = ExtResource("2_gsmti")
@@ -61,6 +66,8 @@ metadata/_custom_type_script = "uid://b0u23nkpaimyc"
RMovement = SubResource("Resource_6d4gl") RMovement = SubResource("Resource_6d4gl")
WallInFrontRayCast = NodePath("../WallInFrontRayCast") WallInFrontRayCast = NodePath("../WallInFrontRayCast")
[node name="CKnockback" parent="." instance=ExtResource("10_jqqi6")]
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
shape = SubResource("CapsuleShape3D_62kkh") shape = SubResource("CapsuleShape3D_62kkh")

View File

@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="RKnockback" load_steps=2 format=3 uid="uid://cektf6waf4s04"]
[ext_resource type="Script" uid="uid://b44cse62qru7j" path="res://resource_definitions/RKnockback.cs" id="1_vdia8"]
[resource]
script = ExtResource("1_vdia8")
Modifier = 100.0
metadata/_custom_type_script = "uid://b44cse62qru7j"

View File

@@ -5,4 +5,5 @@
[resource] [resource]
script = ExtResource("1_hsy8g") script = ExtResource("1_hsy8g")
Speed = 5.0 Speed = 5.0
Acceleration = 10.0
metadata/_custom_type_script = "uid://dtpxijlnb2c5" metadata/_custom_type_script = "uid://dtpxijlnb2c5"

View File

@@ -18,10 +18,10 @@ public partial class CHealth : Node, IHealthable
CurrentHealth = RHealth.StartingHealth; CurrentHealth = RHealth.StartingHealth;
} }
public void ReduceHealth(IDamageable source, float amount) public void ReduceHealth(IDamageable source, DamageRecord damageRecord)
{ {
GD.Print(CurrentHealth); GD.Print(CurrentHealth);
CurrentHealth -= amount; CurrentHealth -= damageRecord.Damage.DamageDealt;
HealthChanged?.Invoke(this, CurrentHealth); HealthChanged?.Invoke(this, CurrentHealth);
if (CurrentHealth <= 0) if (CurrentHealth <= 0)

View File

@@ -0,0 +1,25 @@
using Godot;
using System;
using Movementtests.interfaces;
[GlobalClass]
public partial class CKnockback : Node3D, IKnockbackable
{
[Export] public RKnockback RKnockback { get; set;}
private DamageRecord _damageRecord = null;
public void RegisterKnockback(IDamageable source, DamageRecord damageRecord)
{
_damageRecord = damageRecord;
}
public Vector3 ComputeKnockback()
{
if (_damageRecord == null) return Vector3.Zero;
var knockbackDirection = GlobalPosition - _damageRecord.Source.GlobalPosition;
_damageRecord = null;
return knockbackDirection.Normalized() * RKnockback.Modifier;
}
}

View File

@@ -0,0 +1 @@
uid://b8dprpcjeac7e

View File

@@ -0,0 +1,12 @@
[gd_scene load_steps=4 format=3 uid="uid://bctpe34ddamg5"]
[ext_resource type="Script" uid="uid://b8dprpcjeac7e" path="res://scenes/knockback/CKnockback.cs" id="1_ix2yg"]
[ext_resource type="Script" uid="uid://b44cse62qru7j" path="res://resource_definitions/RKnockback.cs" id="2_uqiml"]
[sub_resource type="Resource" id="Resource_gbu2d"]
script = ExtResource("2_uqiml")
metadata/_custom_type_script = "uid://b44cse62qru7j"
[node name="CKnockback" type="Node3D"]
script = ExtResource("1_ix2yg")
RKnockback = SubResource("Resource_gbu2d")

View File

@@ -19,9 +19,10 @@ public partial class CGroundedMovement : Node3D, IMoveable
var targetPlane = new Vector3(target.X, GlobalPosition.Y, target.Z); var targetPlane = new Vector3(target.X, GlobalPosition.Y, target.Z);
LookAt(targetPlane); LookAt(targetPlane);
float xAcc = (float) Mathf.Lerp(velocity.X, direction.X * RMovement.Speed, inputs.delta * RMovement.Acceleration);
velocity.X = direction.X * RMovement.Speed; float zAcc = (float) Mathf.Lerp(velocity.Z, direction.Z * RMovement.Speed, inputs.delta * RMovement.Acceleration);
velocity.Z = direction.Z * RMovement.Speed; velocity.X = xAcc;
velocity.Z = zAcc;
if (WallInFrontRayCast.IsColliding()) if (WallInFrontRayCast.IsColliding())
velocity.Y = RMovement.Speed; velocity.Y = RMovement.Speed;