state machines

item-info
HumanoidSandvichDispenser 2022-11-13 15:42:04 -08:00
parent 9cd76af028
commit ae0b914717
19 changed files with 503 additions and 35 deletions

View File

@ -1,5 +1,4 @@
using Godot;
using System;
namespace SupaLidlGame.Characters
{
@ -16,17 +15,35 @@ namespace SupaLidlGame.Characters
// Get the gravity from the project settings to be synced with RigidBody nodes.
public float Gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle();
public Vector2 Direction { get; protected set; } = Vector2.Zero;
public Vector2 Direction { get; set; } = Vector2.Zero;
public Vector2 Target { get; set; } = Vector2.Zero;
[Export]
public State.Machine StateMachine { get; set; }
public override void _Process(double delta)
{
if (StateMachine != null)
{
StateMachine.Process(delta);
}
}
public override void _Input(InputEvent @event)
{
if (StateMachine != null)
{
StateMachine.Input(@event);
}
}
public override void _PhysicsProcess(double delta)
{
// movement would be more crisp with no acceleration
Velocity = Direction * Speed;
MoveAndSlide();
if (StateMachine != null)
{
StateMachine.PhysicsProcess(delta);
}
}
}
}

View File

@ -1,14 +1,18 @@
[gd_scene load_steps=4 format=3 uid="uid://dymwd5ihpwyqm"]
[gd_scene load_steps=7 format=3 uid="uid://dymwd5ihpwyqm"]
[ext_resource type="Script" path="res://Characters/NPC.cs" id="1_4x3dm"]
[ext_resource type="Texture2D" uid="uid://bw052v8ikfget" path="res://icon.svg" id="2_ujqd7"]
[ext_resource type="Script" path="res://Characters/States/Machine.cs" id="3_k4ypw"]
[ext_resource type="Script" path="res://Characters/States/NPCIdleState.cs" id="4_8r2qn"]
[ext_resource type="Script" path="res://Characters/States/NPCMoveState.cs" id="5_utogm"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_uict5"]
size = Vector2(32, 16)
[node name="ExampleEnemy" type="CharacterBody2D"]
[node name="ExampleEnemy" type="CharacterBody2D" node_paths=PackedStringArray("StateMachine")]
script = ExtResource("1_4x3dm")
Speed = 32.0
StateMachine = NodePath("StateMachine")
[node name="Icon" type="Sprite2D" parent="."]
scale = Vector2(0.25, 0.25)
@ -17,3 +21,16 @@ texture = ExtResource("2_ujqd7")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0, 8)
shape = SubResource("RectangleShape2D_uict5")
[node name="StateMachine" type="Node" parent="." node_paths=PackedStringArray("InitialState", "Character")]
script = ExtResource("3_k4ypw")
InitialState = NodePath("Idle")
Character = NodePath("..")
[node name="Idle" type="Node" parent="StateMachine" node_paths=PackedStringArray("MoveState")]
script = ExtResource("4_8r2qn")
MoveState = NodePath("../Move")
[node name="Move" type="Node" parent="StateMachine" node_paths=PackedStringArray("IdleState")]
script = ExtResource("5_utogm")
IdleState = NodePath("../Idle")

View File

@ -8,9 +8,8 @@ namespace SupaLidlGame.Characters
/// <summary>
/// Time in seconds it takes for the NPC to think FeelsDankCube
/// </summary>
public const float ThinkTime = 0.125f;
public const float ThinkTime = 0.25f;
public Character Target { get; protected set; }
public float[] Weights => _weights;
float[] _weights = new float[16];
@ -30,6 +29,7 @@ namespace SupaLidlGame.Characters
}
}
/*
public override void _Process(double delta)
{
if ((_thinkTimeElapsed += delta) > ThinkTime)
@ -42,6 +42,7 @@ namespace SupaLidlGame.Characters
//Direction = (Target.GlobalPosition - GlobalPosition).Normalized();
base._Process(delta);
}
*/
public override void _Draw()
{
@ -83,10 +84,21 @@ namespace SupaLidlGame.Characters
return bestChar;
}
public void ThinkProcess(double delta)
{
if ((_thinkTimeElapsed += delta) > ThinkTime)
{
_thinkTimeElapsed = 0;
Think();
}
Direction = _weightDirs[_bestWeightIdx];
}
private void Think()
{
Vector2 pos = FindBestTarget().GlobalPosition;
Vector2 dir = GlobalPosition.DirectionTo(pos);
Vector2 dir = Target = GlobalPosition.DirectionTo(pos);
float dist = GlobalPosition.DistanceSquaredTo(pos);
for (int i = 0; i < 16; i++)
@ -99,9 +111,9 @@ namespace SupaLidlGame.Characters
Vector2 rotatedDir = new Vector2(-dir.y, dir.x);
float horizDot = Math.Abs(_weightDirs[i].Dot(rotatedDir));
// this is a smaller weight so they are more likely to
// pick the direction they are currently heading when
// choosing between two horizontal weights
// this is a smaller weight so they are more likely to pick the
// direction they are currently heading when choosing between two
// horizontal weights
float currDirDot = (_weightDirs[i].Dot(Direction) + 1) / 16;
// square so lower values are even lower
@ -158,7 +170,7 @@ namespace SupaLidlGame.Characters
else
{
float dot = _weightDirs[i].Dot(_weightDirs[j]);
_weights[j] -= (dot + 1) / 4;
_weights[j] -= (dot + 1) / 2;
}
}
}

View File

@ -9,10 +9,5 @@ namespace SupaLidlGame.Characters
{
}
public override void _Process(double delta)
{
Direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
}
}
}

View File

@ -1,13 +1,21 @@
[gd_scene load_steps=4 format=3 uid="uid://bncaar8vp3b84"]
[gd_scene load_steps=9 format=3 uid="uid://bncaar8vp3b84"]
[ext_resource type="Script" path="res://Characters/Player.cs" id="1_flygr"]
[ext_resource type="Texture2D" uid="uid://bw052v8ikfget" path="res://icon.svg" id="2_xmgd1"]
[ext_resource type="Script" path="res://Characters/States/Machine.cs" id="3_npkjp"]
[ext_resource type="Script" path="res://Characters/States/PlayerIdleState.cs" id="4_4k4mb"]
[ext_resource type="Script" path="res://Characters/States/PlayerMoveState.cs" id="5_tx5rw"]
[ext_resource type="Script" path="res://Characters/States/PlayerRollState.cs" id="6_6bgrj"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_bfqew"]
size = Vector2(32, 16)
[node name="Node2D" type="CharacterBody2D"]
[sub_resource type="LabelSettings" id="LabelSettings_q5h1n"]
font_size = 24
[node name="Player" type="CharacterBody2D" node_paths=PackedStringArray("StateMachine")]
script = ExtResource("1_flygr")
StateMachine = NodePath("StateMachine")
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.25, 0.25)
@ -21,4 +29,36 @@ shape = SubResource("RectangleShape2D_bfqew")
current = true
zoom = Vector2(4, 4)
[node name="StateManager" type="Node" parent="."]
[node name="StateMachine" type="Node" parent="." node_paths=PackedStringArray("InitialState", "Character")]
script = ExtResource("3_npkjp")
InitialState = NodePath("Idle")
Character = NodePath("..")
DebugLevel = 2
[node name="Idle" type="Node" parent="StateMachine" node_paths=PackedStringArray("MoveState")]
script = ExtResource("4_4k4mb")
MoveState = NodePath("../Move")
[node name="Move" type="Node" parent="StateMachine" node_paths=PackedStringArray("IdleState", "RollState")]
script = ExtResource("5_tx5rw")
IdleState = NodePath("../Idle")
RollState = NodePath("../Roll")
[node name="Roll" type="Node" parent="StateMachine" node_paths=PackedStringArray("IdleState")]
script = ExtResource("6_6bgrj")
IdleState = NodePath("../Idle")
[node name="Debug" type="Control" parent="."]
layout_mode = 3
anchors_preset = 0
[node name="State" type="Label" parent="Debug"]
offset_left = -20.0
offset_top = -60.0
offset_right = 20.0
offset_bottom = -34.0
text = "lol"
label_settings = SubResource("LabelSettings_q5h1n")
horizontal_alignment = 1
[node name="Node" type="Node" parent="."]

View File

@ -0,0 +1,36 @@
using Godot;
namespace SupaLidlGame.Characters.State
{
public partial class CharacterState : Node
{
public Character Character { get; set; }
/// <summary>
/// Called when the <c>Character</c> enters this <c>CharacterState</c>.
/// </summary>
/// <remarks>
/// This returns a <c>CharacterState</c> in case a state is being
/// transitioned to but wants to transition to another state. For
/// example, an attack state can return to an idle state, but that idle
/// state can override it to the move state immediately when necessary.
/// </remarks>
public virtual CharacterState Enter(CharacterState previousState) => null;
/// <summary>
/// Called when the <c>Character</c> exits this <c>CharacterState</c>.
/// </summary>
public virtual void Exit(CharacterState nextState) { }
public virtual CharacterState Process(double delta) => null;
public virtual CharacterState PhysicsProcess(double delta)
{
Character.Velocity = Character.Direction * Character.Speed;
Character.MoveAndSlide();
return null;
}
public virtual CharacterState Input(InputEvent @event) => null;
}
}

View File

View File

@ -0,0 +1,87 @@
using Godot;
namespace SupaLidlGame.Characters.State
{
public partial class Machine : Node
{
protected Character _character;
public CharacterState State { get; protected set; }
[Export]
public CharacterState InitialState { get; set; }
[Export]
public Character Character { get; set; }
[Export]
public int DebugLevel { get; set; }
public override void _Ready()
{
ChangeState(InitialState);
}
public void ChangeState(CharacterState nextState, bool isProxied = false)
{
if (DebugLevel >= 2)
{
if (State is not null)
{
string proxyNote = isProxied ? " (proxied)" : "";
GD.Print($"Transition{proxyNote} {State.Name} -> {nextState.Name}");
}
}
if (DebugLevel >= 1)
{
if (GetNode<Label>("../Debug/State") is Label label)
{
label.Text = nextState.Name;
}
}
nextState.Character = Character;
if (State != null)
{
State.Exit(nextState);
}
var nextNextState = nextState.Enter(State);
State = nextState;
if (nextNextState is not null)
{
ChangeState(nextNextState, true);
}
}
public void Process(double delta)
{
CharacterState nextState = State.Process(delta);
if (nextState is not null)
{
ChangeState(nextState);
}
}
public void PhysicsProcess(double delta)
{
CharacterState nextState = State.PhysicsProcess(delta);
if (nextState is not null)
{
ChangeState(nextState);
}
}
public void Input(InputEvent @event)
{
CharacterState nextState = State.Input(@event);
if (nextState is not null)
{
ChangeState(nextState);
}
}
}
}

View File

@ -0,0 +1,10 @@
namespace SupaLidlGame.Characters.State
{
public partial class MoveState : CharacterState
{
public override CharacterState PhysicsProcess(double delta)
{
return base.PhysicsProcess(delta);
}
}
}

View File

@ -0,0 +1,20 @@
using Godot;
namespace SupaLidlGame.Characters.State
{
public partial class NPCIdleState : NPCState
{
[Export]
public CharacterState MoveState { get; set; }
public override CharacterState Process(double delta)
{
base.Process(delta);
if (Character.Direction.LengthSquared() > 0)
{
return MoveState;
}
return null;
}
}
}

View File

@ -0,0 +1,20 @@
using Godot;
namespace SupaLidlGame.Characters.State
{
public partial class NPCMoveState : NPCState
{
[Export]
public CharacterState IdleState { get; set; }
public override CharacterState Process(double delta)
{
base.Process(delta);
if (Character.Direction.LengthSquared() == 0)
{
return IdleState;
}
return null;
}
}
}

View File

@ -0,0 +1,13 @@
namespace SupaLidlGame.Characters.State
{
public partial class NPCState : CharacterState
{
protected NPC _npc => Character as NPC;
public override CharacterState Process(double delta)
{
_npc.ThinkProcess(delta);
return base.Process(delta);
}
}
}

View File

@ -0,0 +1,41 @@
using Godot;
using Godot.Collections;
using Godot.NativeInterop;
namespace SupaLidlGame.Characters.State
{
public partial class PlayerAttackState : PlayerState
{
[Export]
public CharacterState IdleState { get; set; }
private float _attackTime = 0;
public override CharacterState Enter(CharacterState previousState)
{
_attackTime = 0;
return base.Enter(previousState);
}
public override void Exit(CharacterState nextState)
{
_attackTime = 0;
base.Exit(nextState);
}
public override CharacterState Input(InputEvent @event)
{
return base.Input(@event);
}
public override CharacterState PhysicsProcess(double delta)
{
return base.PhysicsProcess(delta);
}
public override CharacterState Process(double delta)
{
return base.Process(delta);
}
}
}

View File

@ -0,0 +1,37 @@
using Godot;
namespace SupaLidlGame.Characters.State
{
public partial class PlayerIdleState : PlayerState
{
[Export]
public CharacterState MoveState { get; set; }
public override CharacterState Enter(CharacterState previousState)
{
GD.Print("Entered idle state");
if (previousState is not PlayerMoveState)
{
if (Character.Direction.LengthSquared() > 0)
{
// other states like attacking or rolling can just delegate
// the work of checking if the player is moving to this idle
// state, so they do not have to manually check if the player
// wants to move to switch to the move state.
return MoveState;
}
}
return base.Enter(previousState);
}
public override CharacterState Process(double delta)
{
base.Process(delta);
if (Character.Direction.LengthSquared() > 0)
{
return MoveState;
}
return null;
}
}
}

View File

@ -0,0 +1,39 @@
using Godot;
namespace SupaLidlGame.Characters.State
{
public partial class PlayerMoveState : PlayerState
{
[Export]
public CharacterState IdleState { get; set; }
[Export]
public CharacterState RollState { get; set; }
public override CharacterState Enter(CharacterState previousState)
{
Godot.GD.Print("Started moving");
return base.Enter(previousState);
}
public override CharacterState Process(double delta)
{
base.Process(delta);
if (Character.Direction.LengthSquared() == 0)
{
return IdleState;
}
return null;
}
public override CharacterState Input(Godot.InputEvent @event)
{
if (@event.IsActionPressed("roll"))
{
return RollState;
}
return null;
}
}
}

View File

@ -0,0 +1,48 @@
using Godot;
namespace SupaLidlGame.Characters.State
{
public partial class PlayerRollState : PlayerState
{
[Export]
public CharacterState IdleState { get; set; }
private double _timeLeftToRoll = 0;
private Vector2 _rollDirection = Vector2.Zero;
public override CharacterState Enter(CharacterState previousState)
{
_timeLeftToRoll = 0.5;
// roll the direction we were previously moving in
_rollDirection = Character.Direction;
return base.Enter(previousState);
}
public override void Exit(CharacterState nextState)
{
// we want to reset our state variables in case we are forced out of
// this state (e.g. from death)
_timeLeftToRoll = 0;
_rollDirection = Character.Direction;
base.Exit(nextState);
}
public override CharacterState Process(double delta)
{
Character.Direction = _rollDirection;
if ((_timeLeftToRoll -= delta) <= 0)
{
return IdleState;
}
return null;
}
public override CharacterState PhysicsProcess(double delta)
{
Character.Velocity = Character.Direction * Character.Speed * 1.5f;
Character.MoveAndSlide();
return null;
}
}
}

View File

@ -0,0 +1,15 @@
namespace SupaLidlGame.Characters.State
{
public partial class PlayerState : CharacterState
{
//public PlayerMachine PlayerMachine => Machine as PlayerMachine;
protected Player _player => Character as Player;
public override CharacterState Process(double delta)
{
Character.Direction = Godot.Input.GetVector("ui_left", "ui_right",
"ui_up", "ui_down");
return base.Process(delta);
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -45,6 +45,11 @@ ui_down={
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
]
}
roll={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"pressed":false,"double_click":false,"script":null)
]
}
[physics]