#How can I better organize class objects holding runtime/scene related information?

1 messages · Page 1 of 1 (latest)

pallid drift
#

Let's say that I have three classes:

Task

  • Derives MonoBehaviour.
  • Holds variables and functions needed to perform a task.
  • Variables may be dependent on scene/runtime data.

TaskContainer

  • Derives MonoBehaviour.
  • Holds a list of Task class instances.

Button

  • Derives MonoBehaviour.
  • Holds a TaskContainer class instance.
  • When activated, runs all Task class instances within the TaskContainer class instance.

Current Procedure

Goal: Inject Button with a TaskContainer.

1.) Create multiple empty GameObjects and attach the Task script to each of them. Fill out fields through the inspector.
2.) Create an empty GameObject and attach the TaskContainer script.
3.) Fill out the TaskContainer script's list of Tasks by dragging the multiple Task GameObjects created earlier.
4.) Create an empty GameObject and attach the Button script. Drag the TaskContainer GameObject into the button script.

Thoughts

I don't like how this current situation works because:

  • It feels messy instantiating so many GameObjects for each task.
  • I don't like the idea of data classes being MonoBehaviour because they don't need to be actively running.

Possible Short Solutions

Storing Task scripts and TaskContainer scripts on the same GameObject.

  • Petty Complaint: If I have a long list of Task scripts, maybe the process of dragging it up and down within the same Inspector is more tedious than dragging from the GameHierarchy to the Inspector.

Keep the objects MonoBehaviour and disable it so that it functions as a "normal" class.

  • Complaint: It's kinda disturbing to make a class script MonoBehaviour just so it can be attached on a GameObject and hold runtime/scene dependent information.
#

Alternate Solutions

--- Solution 1 ---
If I want a Button object to do different sets of Tasks, maybe I should just create a derived classes and instantiate tasks in Start().

But then there would be so many different kinds of buttons at different locations in the world. Like if it was a game with many doors it would be like: "1stFloorBedroomDoorCorner.cs", "1stFloorBedroomDoorByKitchen.cs", "1stFloorKitchenDoor.cs". Like maybe naming these scripts would get pretty difficult if you had so many buttons in different locations. Maybe this solution isn't too bad and the scripts just need to be organized by folder?

--- Solution 2 ---
Maybe another solution would be creating custom editor functions to make this adding and dropping functionality way easier?

--- Solution 3 ---
Scriptable objects but they don’t seem designed to grab runtime/scene related objects.

alpine heart
#

So how about two examples of what you're looking for here. I assume if you had two different doors then this task class would give information to them for how they should rotate, right?

#

What you have there sounds fine, but I'm not understanding why you need explicit scripts like "1stFloorBedroomDoorCorner". If you don't want to hardcode all this stuff then you've the options of SOs (creates asset bloat but allows you to share instances across other objects and allows polymorphism) or you can just use SeralizedClasses (unique instance to that one gameobject)

#

The only time you want to create more of these scripts is if you require extended behaviours, but for example if you're creating a script that defines a behaviour for how something to rotate that can apply to many different objects as the only difference would be the values of the rotations.

#

So for instance you would have a set of Task scripts like
RotationTask
TranslationTask
ScaleTask

#

But this also implies everything has a transform (which they should cause all GOs do) so if you decide you need behaviour that requires params other than that then you need to extend the signature of how task executes

pallid drift
#

Ah I think I should provide a better context. Here's an example of a prototype implementations of a system like this. Under a singular button, I created different tasks/events that I want to be executed when this button is activated. These tasks are tied to different game objects. It's kinda like setting up a scene in a game.

alpine heart
#

Right so button is just a wrapper and what you have here seems fine. I'm not seeing any "1stFloorBedroomDoorCorner" scripts though

pallid drift
#

Oh that was when I was imagining what it would be like if I switched to a script based system. Those scripts would need hardcoded values programmed in (like in maybe some sort of initialization function) which is sorta disturbing and like you said, is only needed when extended behaviours is needed.

I just don't think my current solution is very scalable and was looking for alternatives to this organizing all this data.

alpine heart
#

Right, I understand what you're trying to do. The unfortunate part seems to be that you've got references on the scene that you need, so if you do want to reduce it all from having that scene requirement then you need to figure a way to prefab it

#

But, what you have here seems fine tbh

tough lotus
#

Maybe use a context pattern - have some kind of context object in the scene that has references to anything you might need in it. While the task system would be scene agnostic and use the context object to retrieve the required dependencies.

alpine heart
#

I do wish Unity provided a non-transform gameobject. I think we may be getting something similar with the ECS/unified pipeline update

alpine heart
#

You can also use Unity's timeline to invoke events if you wanted to try your hand at that, but I guess this system works off callbacks

pallid drift
#

Oh I actually do use a list:

Putting the tasks/events under Button is just my way of initializing these class objects so that I know these tasks/events belong to this button. I can reorder and reuse these tasks/events multiple times within the Button list.

#

These problem is that these events, although they're just under this specific button, they can also be added to other Buttons which means that when looking for the right event to add it could get bloated quickly going through all the event scripts.

cunning sundial
#

Not sure I understand your situation completely. I am assuming your "Button" is not a literal Unity Button of any kind and simply an object with its own unique behavior within the game, but I could be wrong. Given the above example it seems like your button would be doing quite a lot, but perhaps this is just for the sake of an example of, when "thing" activated, your list of tasks execute.

As a general rule, I avoid deriving from Monobehavior unless it is necessary. ScriptableObject seems a better fit for Task because it skips the step of having to create specific objects for your scripts, but still allows you to create the objects in the editor without either having to construct objects via script or create specific classes that just do one unitask. It seems that might be what you have done in your last post. If that's the case, then you probably already understand why this is a good fit.

Your remaining concern seems to be about grabbing information at runtime, is that right?

pallid drift
# cunning sundial Not sure I understand your situation completely. I am assuming your "Button" is ...

Yes, Button is a custom made class made to hold a list of tasks. This Button's list of Tasks were made by grabbing Task scripts off of the GameObjects containing them. When it gets activated, it sends this list of tasks to an EventManager that linearly processes the tasks. In this case, the tasks are used in performing a cutscene when the button is pressed (Sound Effects, Player Position, Camera Position).

I really like the idea of ScriptableObjects but I don't think it fits the current situation because I have to grab objects at runtime. Like for example, the TeleportEvent. Currently I have situations where I "tp the player somewhere there" or "tp the camera somewhere there". Maybe in the future it might "tp this newly spawned npc object somewhere there."

The reason I made these tasks MonoBehaviour is so they can grab scene/runtime related objects. It is disturbing having to make these MonoBehaviour so these scripts can grab these sort of objects. I plan to disable these Task scripts so that they don't run Start(), Update() and other related MonoBehaviour functions. In a way, they now act as a "normal" class.

I do plan to explore the ideas stated by Mao and dlich soon to see if it can help avoid the current problem: Having to add attaching MonoBehaviour Task scripts to GameObjects for the sake of grabbing objects in the scene/runtime.

midnight lagoon
#

Another approach would be to pool the objects in one manager that your scripts can access through a static instance. So only one monobehaviour handling as manager that pools all objects requested. In this case, you also do not have to search for every object for every single event but first compare to the static list and if not found, query for it through the scene

cunning sundial
# pallid drift Yes, Button is a custom made class made to hold a list of tasks. This Button's l...

I think I get what you're doing more clearly. If being able to drag GameObjects or components that will exist from within the scene into the individual Task objects is a requirement, then SOs would complicate that, not to mention if you had to set up a lot of unique versions of these, your assets folder would be littered with them as someone already brought up. I am not sure you would avoid the management of them in any case as it seems like each Task may require that you set up variables on an individual basis, so you're picking your poison at this point. I suppose you could try just attaching multiple Tasks to one object but then you'll need to ensure they are acquired in the correct order which may be a problem, so hopefully Mao's suggestion of just making a List<Task> serialized will allow you access to all the variables you need in the inspector. I'd definitely try it.

I suppose it goes without saying that the more you simplify this system, the easier this task can be. If you have objects that you know will always exist in your scene, such as a singleton with a static Instance variable that can be acquired at any time by other scripts, you can have your Task objects autonomously acquire the specific references they need from those classes. Not needing to dictate any information for a Task at all would be most convenient. If it's impossible to predict what specific reference or value is needed in a given scenario however, then you're probably stuck with the suggestions already given in this thread, or you may need to seek out something more comprehensive than simply using GameObjects and Monobehaviors to make this more bearable.

alpine heart
#

I think the mono idea is fine and how it's displayed on the scene/prefab here is as best as you're going to get when it comes to clarity.

#

If you did want to stick to a single game object and just load a bunch of Task scripts on it then SOs would be the solution but leads to asset bloat which Unity doesn't ever want to fix by allowing you to embed them into the GO. So, the best solution here is SerializedReferences, but unfortunately their implementation is half-assed and required you create a drawer script for them to be displayed on the inspector.

#

You could also just create the list at runtime such that Button grabs all Task gameobjects from its childrens to populate it.

#

Well, you'd have to attach all the components to button thereafter if you plan on cleaning the GOs up afterwards.. meh

#

Actually, if you are using scene references then SO idea wouldn't work anyway. You'd have to prefab it all to make it work

pallid drift
#

I attempted to tie all ideas stated here together into a rough draft class.

Assumptions:

  • There will be a GameObject pool implemented either through List<GameObject> or Dictionary<string name, GameObject>.
  • Event/Task will no longer inherit MonoBehaviour but be a ScriptableObject.

Note: Opening thread in full view might help with the formatting below.

So an Event/Task like TPEvent can be written like:

Event : ScriptableObject { //Stuff }

TPEvent : Event
{
    // Fill through Inspector
    List<string> NameOfGameObjectsToTp;
    List<GameObject> Prefabs;
    List<Vector3> TpPositions;

    // Information to be added during Awake()/Enable() process
    List<Transform> Transforms;

    Awake()/Enable()
    {
        // 1.) Go through the list and attempt to find GameObjects with that name.
        // 2.) If successfully found, grab the GameObject's transform and add it 
               to the list of transforms. (Scene/Runtime Existing Objects)
        /* 
            3.) If not successfully found, instantiate a prefab, name it after what 
            it was supposed to be named, add it to the GameObject pool, and grab 
            transform and add it to the list of transforms. (Dynamic Instantiation)

            4.) If wanted, can reserve a name for straight up summoning objects at will. 
            Like if "None" was written into the NameOfGameObjects and paired it with a 
            Prefab, it doesn't care what this GameObject is called. When this task gets
            executed, it'll straight up instantiate it and teleport it to the location. 
            (Dynamic Instantiation)
        */
    }

    /* 
        Additional functions that originally existed in Event/Task that can be overridden.
        Logic that executes the Task/Event.
    */
}
#

Benefits:

  • It's now in a ScriptableObject.
  • It's now a "normal" class that isn't marked as MonoBehaviour (automatically drops Update(), FixedUpdate()...)
  • Can support grabbing existing Scene/Runtime objects
  • Can support grabbing dynamically instantiated GameObjects.

Neutral:

  • Organizing created ScriptableObjects now takes place within AssetFolders rather than the GameHierarchy.

Problems:

  • Figuring pool access.
  • All GameObjects now have to be prefabs.
  • Guessing prefab variables are memory hungry. Maybe another pool can be made that maps GameObject names to Prefabs. This would get rid of having to drag prefabs in the inspector to match the name of the GameObject issue.
  • ScriptableObjects get their Awake() and Enable() functions called before MonoBehaviour GameObjects. Maybe an additional function could be made that runs all Task initialization functions during runtime after all current MonoBehaviour objects are initialized.
  • Breaks Single Responsibility Principle? Summoning a GameObject kinda sounds like another Event/Task.
  • GameObjects instantiated outside this specific Task have to be added to the pool. Pool gets updated so now every task has to be updated with this potentially new information.

I haven't implemented/tested this idea yet but it looks interesting to try. Hoping that there wasn't any major pieces of knowledge missing that would immediately break this whole idea.
Also thank you to everyone in this post for the ideas, it was very helpful.

merry stump
# pallid drift Benefits: - It's now in a ScriptableObject. - It's now a "normal" class that isn...

i didnt read from the initial question, so i dont truly understand what gameplay element you're trying to implement with TPEvent, but i do have a few points.

  • List<string> NameOfGameObjectsToTp; this shouldnt exist. you shouldnt be finding game objects by name if thats the goal, this will be fragile and just annoying to work with. Find a way to pass the game object references to this SO asset however you want.
  • Point 4 here makes no sense "If wanted, can reserve a name for straight up summoning objects at will". You can do this anyways without the List<string> with a list of prefabs that'll always be spawned
  • You shouldn't need or use Awake/OnEnable here for an SO. This won't run when you think it will, plus you wont need it anyways when you move away from this List<string>

As for the benefits, you can get references to "existing Scene/Runtime objects" or "dynamically instantiated GameObjects" if this was a regular c# class, monobehaviour, or scriptable object. Nothing changes here because it's a scriptable object

#

Most of your problems here are solved when you just figure out a way to pass references to other objects.

stuck cedar
#

I would use an Initialize() method if you want to pass scene references into a SO. You can use whatever Event Manager class to loop through all Events and call their Initialize()