#Converting SO to plain class?

1 messages · Page 1 of 1 (latest)

dusty steppe
#

Well, why do you want to convert the events to non SO? Or am I misunderstanding what you're trying to do?

elder idol
#

I was told not to use SO in my code

dusty steppe
#

Oof... It was not a single message..

elder idol
#

Except from using it for immutable data

dusty steppe
#

Okay, but from what I see so far, your events are immutable data.

elder idol
#

Honestly, everyone in code-begginer told me to not use SOs for that

#

Saying it could have problems in memory and such

#

I don't really understand why

#

Oh

#

I remember

#

sorry, i'm so sleepy

dusty steppe
#

Can you link what exactly you've been told?

#

And about what part of your code

elder idol
#

Essentially, I wanted to make my CardDataSO mutable to use it as my main way of handling mutable card data

#

And that's not a good thing, as stated before.

#

So I uncoupled the CardData logic from CardDataSO

#

So far so good

#

problem is when I try to pass the EventsSO to the Events

#

Ok, now i get the problem

#

I DON'T have to have a non SO Events xD

#

Because, like you said, my events are not mutable

#

theoreticaly, at least

#

so it' okay to construct SOs and pass them around in the code, as long as they don't mutate?

#

I had understood that it would lead to major memory leaks later on so that's why I stopped using them

#

Ok, maybe i got it

#

Hold on

#

So when building a scriptable object directly into the inspector the Awake() gets called instead of the constructor, right?

#

OKAY

#

I found the problem

#

Thing is, each Event does something for a player

#

or against a player

#

But that can't always be known beforehand

#

at least not in the way I had structured my events

#

Because I have this:

#
[CreateAssetMenu(fileName = "Summon Card Play Event", menuName = "Game Events/Summon Card Play Event")]public class SummonCardPlayEvent : GameEvent
{
    private readonly Player player;
    [SerializeField] private CardData cardData;
    public SummonCardPlayEvent(Player player, CardData cardData)
    {
        this.player = player;
        this.cardData = cardData;
    }
    public override IEnumerator ExecuteCoroutine()
    {
        Board.Instance.SummonCard(player, cardData);
        yield return null;
    }
    public override IEnumerator UndoCoroutine()
    {
        throw new System.NotImplementedException();
    }
}
#

I know which card to summon (because CardData is serializable) but not to which player (because Player is not)

#

And my logic resolves that at RunTime

#

That's the problem I was trying to solve!

#

Thing is, I should decide beforehand which player I should summon my card to!

#

That is why I was having all this trouble in the first place

#

Bad architecture

#

The big question is, why can't I solve the problem on my own but when someone asks me questions suddenly I crack it down?

#

Always the same thing

#

MAkes me think something is wrong with me

dusty steppe
#

You should probably avoid using constructors with unity objects(including SO).

You could have a method ExecuteEvent In the GameEvent that takes both the invoking and the target player as a parameter.

dusty steppe
#

As you get more experienced, you'll learn to notice this kind of things on your own more.

elder idol
#

Thank you for your help, friend!

elder idol
#

I've been recommended that before but I don't understand it

dusty steppe
#

You calling the constructor manually, bypasses some of the necessary initialization process.

elder idol
#

Oh ok

#

So, theoreticaly, I should always have an INitiate function if I want to initialize my values??

dusty steppe
#

Yes.

elder idol
#

Understood.

dusty steppe
#

Again, that only applies to Objects. So mostly MonoBehaviour and ScriptableObject.

elder idol
#

Got it.

#

I really like your suggestion of implementing a Execute methods that takes both players as a parameter

#

It really solves the problem of having to decide who will be the caller of the event and who will be target at run time, even if there are neither caller or target.

#

Huge problem, though!

#

That breaks the Command Pattern I implemented

#

We go back to my starting problem

dusty steppe
#

How does it break it?

elder idol
#
    private IEnumerator DispatchActions()
    {
        while (GameEventsQueue.Count > 0)
        {
            eventsDispatching = true;
            GameEvent nextEvent = PopEvent();
            EventsRecorder.Instance.Record(nextEvent);
            yield return StartCoroutine(nextEvent.ExecuteCoroutine());
        }

        eventsDispatching = false;
        yield break;
    }

There's no way to know at runtime who the invoker and target are without specifying it on the command!

#

It's a command!

#

IT's written already.

#

X kills Y.

#

BeginTurn.

#

X Damages Y.

#

The variables should already be known right at the creation of the command.

#

Which means, my commands should be more specific!

#

I should really have a factory.

dusty steppe
#

I see, so you need the command to be mutable.

elder idol
#

Maybe that solves the problem, because the factory jsut takes the caller and target as input and outputs a immutable command.

elder idol
#

I'd need something else which is more malleable than a command.

dusty steppe
#

Why not?

elder idol
#

Because a command is concrete

dusty steppe
#

A command carrying some context is still a command

elder idol
#

What I need is a command template.

#

I think I know what to do: A command factory Scriptable Object!

dusty steppe
#

What I'd do, is either have a runtime wrapper for the command that would hold additional runtime data, like who dispatched the event and who it's target.
Or have that same data in a Context object passed with the command.

elder idol
#

That should solve the problem nicely.

elder idol
#

I mean, it is that.

#

Because what I really need is a wrapper, like you described.

dusty steppe
#

If you think too much in terms of design patterns, you'll miss the particular details required for your specific use case.

elder idol
#

Got it.

#

This is a bad vice I have!

#

IT's a lot easier with a factory tho

#

I mean, whatever.

#

I'll just make a wrapper and see what I get.

dusty steppe
#

I'm not saying you shouldn't use it. It's just that mentioning a design pattern doesn't really tell me anything about the way you want to implement it.

elder idol
#

Thing is. I have two options:

#
  1. I change the Scriptable Object to the Wrapper and maintain my code logic.
#
  1. I maintain the Scriptable Object I have and modify my code logic to work with the wrapper instead.
#
  1. is Factory.
#

I MUCH prefer it, because I only have to modify the creation part of the Command.

#

Then all I have to do is modify the parts of the code in which the Commands are being instantiated.

#

Sounds easy enough.

#

I'll go with that.

#

So, basically, what I'm gonna do:

#

Step1:

Create an abstract 'factory' class to instatiate concrete 'event factories'.
Example: abstract Event'Factory' <---- BeginTurnEvent'Factory'

#

Step 2:

Transform it into an SO and modify CardDataSO and CardData.

dusty steppe
#

Would be easier to just implement a CreateRuntimeEvent inside each of the SO events and passing in whatever they might need. It would then return a wrapper of the event.

elder idol
#

Step 3:

Modify each part of the code that instantiates a new command so that my factory instantiates it, to maintain SRP.

#

Sounds like a good idea!

elder idol
#

Unless!

#

CreateRuntimeEvent returned Game Event Objects!

#

How do I instantiate a SO without a constructor tho?

#

It needs a template, right?

elder idol
#

But then if I create it like this I'll have to modify it!

#

And I can't, because Scriptable Objects are not good for mutability, are they?

#

I think I should create another Class for this.

dusty steppe
elder idol
#

Ok!

#

Problem solved!

elder idol
#

Again

#

I ran into the same problem

#

How do I know which wrapper to use?

#

All I have is General GameEvent Objects

#

I think I need to tag them with a variable then select which wrap I'll use

#

Probably with a switch statement or something like this.

dusty steppe
#

You'll need to share some code for me to provide any answer

elder idol
#

!code

frigid sirenBOT
elder idol
#

At the Wrap method

#

I can't decipher which gameEvent is which so I can wrap them properlu

#

public class SummonEventWrapper : GameEventWrapper
{
    private Player invoker;
    private CardData cardData;

    public SummonEventWrapper(Player invoker, CardData cardData)
    {
        this.invoker = invoker;
        this.cardData = cardData;
    }

    public override IEnumerator ExecuteCoroutine()
    {
        Board.Instance.SummonCard(invoker, cardData);
        yield return null;
    }
    public override IEnumerator UndoCoroutine()
    {
        throw new System.NotImplementedException();
    }
}

[CreateAssetMenu(fileName = "Summon Event", menuName = "Game Events/Summon Card Play Event")]
public class SummonEvent : ScriptableObject { }
#

Example of a Event and EventWrapper classes 👆

#

The CardData class is the one that's going to store which behaviours it has.

#

It essentialy reads a CardDataSO object to do that.

#

Because, CardData is mutable.

#
//Command Pattern
public abstract class GameEventWrapper
{
    public abstract IEnumerator ExecuteCoroutine();
    public abstract IEnumerator UndoCoroutine();
}

public abstract class GameEvent : Scripta

Game Event and GameEventWrapper classes 👆

dusty steppe
#

GameEvent should have a method that creates the corresponding runtime event/wrapper.

Also, I'm not sure what you're doing with this method:


    private List<GameEventWrapper> Wrap(List<GameEvent> gameEvents)
    {
        return gameEvents.Select(x => new GameEventWrapper(x)).ToList();
    }
elder idol
#

I'm just wrapping the event

#

IT's not complete, I just stopped like that because I realized it wasn't going nowhere

#

How do I proceeed from this?

public abstract class GameEvent : ScriptableObject
{
    private object obj;
    private EVENT_TYPE type;
    public GameEventWrapper Wrap()
    {
        return type switch
        {
            EVENT_TYPE.beginTurn => new BeginTurnEventWrapper(),
            EVENT_TYPE.cardDiscard => new CardDiscardEventWrapper(),
            _ => null
        };
    }
}
#

CardDiscardEventWrapper requires different parameters than BeginTurnEventWrapper to be constructed

#

But the one building it is a GameEvent object, which has no clue which event it is and which parameters it has received

#

Maybe it can have a big dict of Event types that lead to a list of those event types

#

But then their order relation would be lost

#

I could keep their original order in another array as the Event is created

#

maybe I could have a big array with all items I could need rn, but that doesn't really scale

#

The dict it is then?

#

Wait I"m thinking about the entire CardData

#

I need to think of just the parameters of the Event

dusty steppe
elder idol
#

I don't follow.

dusty steppe
#

aa

#

Nvm. type confused me a bit

#

I thought it was an actual Type variable.

dusty steppe
#

Wrap in GameEvent should be abstract.

#

And the inheriting type should provide the correct implementation.

elder idol
#

Ohhhhh

#

There is still the problem with the parameters, which are all different

dusty steppe
#

What kind of parameters are they? Can they be accessed from the player data or something?

elder idol
#

for example, DamageEvent has a Invoker, Target and Damage parameters

#

BeginTurnEvent has no parameters

#

while DiscardCardEvent has Invoker and numberOfDiscards parameters

dusty steppe
#

What determines the Damage?

elder idol
#

whoever creates the Command decides that

dusty steppe
#

Or number of discards

elder idol
#

The card

#

Or the event

#

But generally, the card will have that parameter

dusty steppe
elder idol
#

Yes, Or summons on board

dusty steppe
#

You have several options:

  1. Pass the player/card/board data into every Wrap(bad naming) method, so that each specific event can retrieve the data needed for it.
  2. Create an abstract EventParameter class and inherit it with specific parameter classes that contain the require data and pass it into the Wrap method
elder idol
#

Ok I like the second option better

#

I was gonna suggest the first

#

Also, why is Wrap a bad naming convention?

dusty steppe
#

Not a convention. But I would name it something like CreateEvent, since that's what it does basically.

elder idol
#

I think I'll go with WrapEvent

dusty steppe
#

Wrapper and Wrap doesn't really explain what that class does in the specific context

elder idol
#

Oh okay

#

Should I make it explicit that an Event is being wrapped though?

dusty steppe
#

I think the issue is that you consider the SO and Event.

elder idol
#

Main thing is that there is this thing that the card does that does not quite have a name yet.

dusty steppe
#

I'd rather call it an EventDefinition or something

#

While the runtime class is the actual event

elder idol
#

Or maybe EventPrototype

#

OK, sounds perfect

elder idol
#

Omg I did it!

elder idol
#

Now how am i gonna know which parameter to pass lmaooo

#

this looks like I'm passing responsability ever forward

#

like an endless hotel

#

I"m thinking just checking which kind of event it is with an event tag

elder idol
#

maybe something like (Player, generic event, Target)

#

The first and third are always known, the ssecond can be inside de class implementation

#

Anyways, I'm done for today

#

Thank you for the help!

dusty steppe
elder idol
#
private void EnqueueBeginTurnEvents()
{
    foreach (Player player in players)
    {
        foreach (Canvas canvas in Board.Instance.summons[player.id])
        {
            CardData data = canvas.GetComponentInChildren<Summon>().data;
            EventsQueue.Instance.EnqueueEvents(data.BeginTurnCardEffects); // this line won't work because Card Effects haven't been wrapped by an Event 
        }
    }
}
#

The problem is all I have is a List<EventPrototype> interface in BeingTurnCardEffects.

#

And each EventPrototype has an CreateEvent which expects a specific implementation of EventParameters.

#

I can't know that implementation here.

#

But I need to know what Event I'm gonna execute right before I enqueue.

#

And, in this case, the 'BeginTurnEvents' being enqueued need to be determined right when EnqueueBeginTurnEvents function is called.

#

So one option would be to implement something inside EventPrototype to get what kind of parameters it's expecting and how to generate them. I could split the generation part outside of the EventPrototype code.

#

So, something like an EventParameterGenerator that has a EventParameters GenerateParameters(EventPrototype prototype) method.

elder idol
#

I know, what if, everytime an Event is Executed, it has a ReceiveInput() which will start an input collection?

#

Then each event can collect input according to its requirements.

elder idol
#

oh btw I think I found a way