Fling-A-Ball -- seeking "The Dinghy Way"

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 ball Entity inside it. But to get that custom Entity 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.

1 Like

First, congratulations on the being the first non-me post in the Dinghy forums :slight_smile:

Secondly, I’m really interested in this line:

I had the moment of enlightenment shortly thereafter. “Scene” means Unity Scene, not Godot Scene.

I forgot that people that may try out Dinghy may be coming from Godot, where yeah Scene definitely means something different (more like a Unity prefab). I’m wondering if maybe I should rename Scene to Stage to avoid the collision in naming/understanding of the general concept?

Onto the code!

Your intuition was mostly right though in your first example — a ball is in a scene (though not that the ball is the scene). As far as I can tell the first option you sent in your code works fine — this is what I see:

For custom functionality, the question is more “where you want the functionality to be/what is the custom functionality”. Puppeting an entity’s behavior through the scene it’s inside of is fine (though maybe unmanagable if you have lots of entites).

For more complex custom behavior/interactions, you’d want to either create an entity that has that specific functionality and is put together through ECS components + systems, or through working with something that I devised as a bit of hack, which is to use ManagedComponent.

ECS/Systems Way

However, for your example the main thing it looks like you’re trying to custom do is adjust the position based on the velocity. Dinghy does right now have a default velocity system in (system in the ECS sense), so by trying to update the velocity on top of the velocity you’re like getting a strange result there. Additionally, Entities don’t get any Update call to them, they are only acted on externally by Systems (or otherwise other logic). The thing I would suggest in your model based on the above options is to write a system that updates the velocity based on the wall collisions, and have another component that represents Grab state. So something like this:

//add the component
public record struct Grabbed (bool value);
//elsewhere you create a shape, add the component to the shape
_shape.ECSEntity.Add(new Grabbed(false));
//set grabbed either in your scene with input callbacks
ref var grabbed = ref _shape.ECSEntity.Get<Grabbed>();
grabbed.value = true;

//then define a system that acts on this
public class WallBounceSystem : DSystem, IUpdateSystem
{
    QueryDescription query = new QueryDescription().WithAll<Active,HasManagedOwner,Position, Velocity,Grabbed>();
    public void Update(double dt)
    {
        Engine.ECSWorld.Query(in query, (Arch.Core.Entity e, ref HasManagedOwner owner, ref Position pos, ref Velocity vel, ref Grabbed grabbed) => {
           //do your wall check/vel update stuff in here
           //this line is only important if you access X/Y of the entity outside of a system
        //owner.e.SetPositionRaw(pos.x,pos.y,pos.rotation,pos.scaleX,pos.scaleY,pos.pivotX,pos.pivotY);
        });
    }
}

You’d then need to register that system with the game’s systems:

Engine.RegisterSystem(new WallBounceSystem());

This may however still act weird with the velocity system, which is also why I’ve been considering taking the velocity system out of the core engine stuff because it’s likely that devs may want to handle it somehow themselves.

Managed Component way

I don’t think working with ECS is “cozy” in the way I want Dinghy games to be (see some of what I’m thinking here), so something that’s in the engine as a bit of a short-term concession is the concept of a ManagedComponent. This is just a component that directly receives PreUpdate/Update/PostUpdate calls, acting more like a Unity-style monobehavior. You can define a component by extending Component:

public class WallCollision(Entity parentEntity) : Component(parentEntity)
{
    public override void PreUpdate() {}
    public override void Update() {}
    public override void PostUpdate(){}
}

And inside those functions write your logic. You can then add this to any entity like:

_shape.ECSEntity.Add(new WallCollision(_shape))

Future

The ECS way is technically the “correct” way but is definitely a little weird from an API standpoint because it requires you to understand a lot about the way ECS/Dinghy’s ECS works in order to set it up. The ManagedComponent way feels more familiar coming from Unity/Godot but also long term will remediate a lot of the pitfalls with that type of design (state is everywhere, hard to track logic flow, etc.). My goal is definitely doing something more “magic” here to allow you to easily author correct ECS “types” and connect up systems, as straddling the various ways of doing things now can quickly get sticky (like you’re seeing).

Let me know how you end up doing this and what you think of the above! And just to surface two possible API changes from this:

  1. Rename “Scene” to “Stage” to avoid conceptually mismatch for people coming from Godot vs. Unity
  2. Get rid of the default Velocity system — core systems should just handle rendering, not behavior

Many thanks for the incredibly thoughtful reply. Funnily enough, I’m most familiar with Unity. I made the Godot assumption based on this bit in the documentation:

Stage feels more accurate to me, but I’d be curious to hear other opinions.

I’ve spent a while mulling over the rest of your answer. I think it would be irresponsible for me to weigh in with opinions until I’ve built something more substantive. The most productive patterns will emerge with time, I think. My one takeaway: if you figure out a way to make proper ECS “cozy,” I will be amazed and delighted. (The proposals in the other thread look fantastic, as does the proposed attribute syntax from this update.)

I’m off to bring these little guys to life – I’ll post questions as they emerge. In the meantime, I’m eager to see questions and suggestions from other users.

1 Like

Spent some time last night renaming Scene to Stage and then realized it’s maybe weird that Stages also sort of have… stages that they move through. So maybe keeping scene for now haha.

For the attribute syntax for systems, that’s actually available now but I haven’t figure out how I want to expose/use it in the core of Dinghy. You can peek at the docs on it here:

I need to migrate my ECS to their World system and then ideally provide some additional source-generated wrapped where you can turn a single function into a system without needing to explicitly declare a class (or something…)

Regardless, excited to watch those little guys come to life!

@bgsulz Wanted to update you here — one thing I’ve sort of settled on for now for my own test game is basically embed the Dinghy entity inside of a declaration of another “entity”. So something like this:

public class MySpecificEntity {
  public int SomeValue;
  public Shape DinghyEntity;
  public MySpecificEntity() {
    DinghyEntity = new Shape(...);
  }
}

You can then wrap “notable” functionality of the underlying entity like:

public class MySpecificEntity {
  public int SomeValue;
  public int X => DinghyEntity.X;
  public Shape DinghyEntity;
  public MySpecificEntity() {
    DinghyEntity = new Shape(...);
  }

  public void Destroy() {
    DinghyEntity.Destroy();
  }
}

It’s a bit easier to work with entities this way and lets you do higher level game logic stuff in non-ECS code, and just access the underlying entity where necessary to handle stuff like collisions/rendering. You could even have multiple per whatever class you have!

One additional thing I’m doing is defining a “tag” component that corresponds to the type of the object I’m using so that the underlying ECS has visibility into my managed layer. This sounds complicated but is just this:

//define a tag component
public readonly record struct MySpecificEntityComponent();

public class MySpecificEntity {
  public int SomeValue;
  public int X => DinghyEntity.X;
  public Shape DinghyEntity;
  public MySpecificEntity() {
    DinghyEntity = new Shape(...);
    //add a component to the underlying entity
    DinghyEntity.Add(new MySpecificEntityComponent());
  }

The reason to do this is that stuff that comes “out” of ECS land (like collision callbacks) don’t have visibility into the higher level code land. However with something like this, if you ever get hold of an underlying ECS Entity, you can just check Entity.Has< MySpecificEntityComponent>() to see if it’s an entity that corresponds to a higher level wrapper class.

1 Like