TL;DR: What is a simple, clean way to create an Entity
with custom behavior?
Per new framework tradition, I started with my fling-a-ball demo.
using System.Numerics;
using Dinghy;
class Ball : Scene
{
const int Size = 32;
const float GrabStrength = 400;
const float Gravity = 1400;
const float BounceHeight = 0.96f;
Vector2 _pos, _vel;
Shape _shape;
bool _grabbed;
public override void Create()
{
_shape = new(Palettes.ENDESGA[0], width: Size, height: Size)
{
PivotX = Size / 2,
PivotY = Size / 2,
};
_pos = new(Engine.Width / 2, Engine.Height / 2);
InputSystem.Events.Mouse.Down += Grab;
InputSystem.Events.Mouse.Up += Release;
}
public override void Cleanup()
{
InputSystem.Events.Mouse.Down -= Grab;
InputSystem.Events.Mouse.Up -= Release;
}
public override void Update(double dt)
{
// Is there a better way?
float deltaF = (float)dt;
if (_grabbed)
{
Vector2 distanceToMouse = new(InputSystem.MouseX - _pos.X, InputSystem.MouseY - _pos.Y);
_vel = distanceToMouse * GrabStrength * deltaF;
// _vel = (InputSystem.MousePosition - pos) * GrabStrength * deltaF;
}
else
{
_vel.Y += Gravity * deltaF;
}
_pos += _vel * deltaF;
CheckEdgeCollisions(ref _pos.X, ref _vel.X, Size / 2, Engine.Width - Size / 2);
CheckEdgeCollisions(ref _pos.Y, ref _vel.Y, Size / 2, Engine.Height - Size / 2);
_shape.X = _pos.X;
_shape.Y = _pos.Y;
// _shape.Position = _pos;
// public Vector2 Position { set { X = value.X; Y = value.Y; } };
}
static void CheckEdgeCollisions(ref float pos, ref float vel, int min, int max)
{
if (pos < min || pos > max)
{
vel = -vel * BounceHeight;
pos = Math.Clamp(pos, min, max);
}
}
void Grab(List<Modifiers> modifiers)
{
if (modifiers.Contains(Modifiers.LMB))
_grabbed = true;
}
void Release(List<Modifiers> modifiers)
{
// Modifiers seems to contain nothing.
//if (modifiers.Contains(Modifiers.LMB))
_grabbed = false;
}
}
I know this is wrong! Don’t worry, I had the moment of enlightenment shortly thereafter. “Scene” means Unity Scene, not Godot Scene. Should’ve guessed given Cantata. Okay, this is basically a Shape
with a couple extra tricks. I’ll just inherit from Shape
.
using System.Numerics;
using Dinghy;
class Ball : Shape
{
const int Size = 32;
const float GrabStrength = 400;
const float Gravity = 1400;
const float BounceHeight = 0.96f;
Vector2 Position
{
get => new(X, Y);
set
{
X = value.X;
Y = value.Y;
}
}
Vector2 Velocity
{
get => new(DX, DY);
set
{
DX = value.X;
DY = value.Y;
}
}
bool _grabbed;
public Ball() : base(Palettes.ENDESGA[0], width: Size, height: Size)
{
PivotX = Size / 2;
PivotY = Size / 2;
Position = new(Engine.Width / 2, Engine.Height / 2);
InputSystem.Events.Mouse.Down += Grab;
InputSystem.Events.Mouse.Up += Release;
}
~Ball()
{
InputSystem.Events.Mouse.Down -= Grab;
InputSystem.Events.Mouse.Up -= Release;
}
public void Update(double dt)
{
// Is there a better way?
float deltaF = (float)dt;
if (_grabbed)
{
Vector2 distanceToMouse = new(InputSystem.MouseX - X, InputSystem.MouseY - Y);
Velocity = distanceToMouse * GrabStrength * deltaF;
// _vel = (InputSystem.MousePosition - pos) * GrabStrength * deltaF;
}
else
{
Velocity += new Vector2(0, Gravity);
}
// To me, this was not immediately intuitive.
DX *= deltaF;
DY *= deltaF;
(X, DX) = CheckEdgeCollisions(X, DX, Size / 2, Engine.Width - Size / 2);
(Y, DY) = CheckEdgeCollisions(Y, DY, Size / 2, Engine.Height - Size / 2);
}
static (float, float) CheckEdgeCollisions(float pos, float vel, int min, int max)
{
if (pos < min || pos > max)
{
return (Math.Clamp(pos, min, max), -vel * BounceHeight);
}
return (pos, vel);
}
void Grab(List<Modifiers> modifiers)
{
if (modifiers.Contains(Modifiers.LMB))
_grabbed = true;
}
void Release(List<Modifiers> modifiers)
{
// Modifiers seems to contain nothing.
//if (modifiers.Contains(Modifiers.LMB))
_grabbed = false;
}
}
I realize that this is wrong too, because it doesn’t work. Collisions with walls cause the ball to oscillate between behind and in front of the wall on alternating frames. To be honest, I’m not sure why, even after peeking into VelocitySystem
and its friends (sorry!) Maybe execution order is non-deterministic, also like Unity? No, the behavior is too patterned for that. The un-coziness of these thoughts leads me to believe I’m missing something obvious.
I eventually realized it’s best to create a
BallScene
and create the ballEntity
inside it. But to get that customEntity
to work properly, I still had to bring out my custom_pos
and_vel
, which seems like a waste!
In short: how do you drive this thing? I know there’s a terrific solution right under my nose. I can smell it. Is it a system that modifies the ball’s Velocity
component? Is it a custom component? Is it some other thing to make it three? Any insights are appreciated.