1
\$\begingroup\$

I have the following case: I have a class called App.cs and here I have a function called Render, that runs once every frame:

public static class Render(){
   DrawBuffer.Clear(); //clear the last frame
   //DrawStuff, Logic, etc.

   DepthBuffer.Calculate(); //compute the depth of the scene for the current frame
}

Now, I want to be able to add game logic, and drawing objects, but not directly from within App.cs for cleanness, so I'm trying to implement a system of "scripting" for my own 3D render engine similar to Unity3D. So, lets suppose I have the following scripts: Script1.cs, Script2.cs and Script3.cs. I want to be able to have certain names for methods within those scripts that do certain things. For example let's say that scripts can definte the method GameLoop() which would be the equivalent of Update() from Unity and Init() which would be the equivalent of Start() from Unity. Let's focus on GameLoop() first. Script1.cs might look something like this:

using MyRenderEngine;
using System;

public class Script1
{
   public void Init(){
      //Called at the beginning
   }
   public void GameLoop(){
      //Called every frame
   }
}

Script2.cs and Script3.cs would look very similar to this.

Now, the subject of this question is how would I make every GameLoop() of every script be called in the main game loop?
The most obvious method would be in App.cs to have in the Render() method:

Script1 script1 = new Script1();
Script2 script2 = new Script2();
Script3 script3 = new Script3();
   
public static void Render(){
   //Clear last frame
   script1.GameLoop();
   script2.GameLoop();
   script3.GameLoop();
   //Compute depth buffer   
}

But would be very awkward to have to add a call in the main render loop(Render()) for each script.
I can create a list of object that can hold a reference to each script and to add a foreach loop in the Render() to call the GameLoop() from within each of them, but I would need to add scripts to that list manually, which would be the same as calling the method directly.

The second solution I found would be to create an interface, let's say Behaviour3D that each script need to implement and that contain the methods GameLoop() and Init() that can be overrode by each class implementing Behaviour3D, but I would need to create a list and update it manually. I could maybe use Reflexion to obtain automatically a list of all classes that implement Behaviour3D interface, but I have a motto: "Avoid Reflection as much a possible". Now I have 2 questions:

  1. How does Unity do it?(How MonoBehaviour works)
  2. How would you do it?
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Regarding reflection:gamedev.stackexchange.com/questions/164892/… \$\endgroup\$
    – Zibelas
    Dec 15, 2023 at 23:27
  • \$\begingroup\$ Your options are 1) use reflection (which you say you don't want) and 2) register the objects in a list of an interface or delegate type (which you also say you don't want). Sadly, you will have to pick one. Unity internally has lists of objects or function pointers, as the link Zibelas shared explains. This is why objects in Unity are created through the Instantiate / AddComponent methods and not constructed directly, so that inside those methods Unity can do the work of adding the object to the appropriate lists, without burdening the user with maintaining them manually. \$\endgroup\$
    – DMGregory
    Dec 16, 2023 at 0:45
  • \$\begingroup\$ @DMGregory Your 2) "you will have to pick one." point missed a third option, base class with automatic registration to a static List in the constructor. \$\endgroup\$
    – agone
    Dec 16, 2023 at 23:41
  • \$\begingroup\$ That's still registering objects in a list and calling them virtually, the same as a list of an interface type. But it has another issue, as discussed in the thread Zibelas links above: It removes your ability to not call one of these methods when it's not needed. You always get a virtual method call, even if that just calls into the default implementation and immediately returns. The performance cost of this adds up if, say, you have hundreds of objects that don't need any BeforeUpdate() processing but they get called anyway. \$\endgroup\$
    – DMGregory
    Dec 17, 2023 at 2:50

1 Answer 1

0
\$\begingroup\$

Add a base abstract class Behaviour3D.

There is no need for reflection.

public abstract class Behaviour3D
{
   internal static List<Behaviour3D> myObjects = new();
   internal static List<Behaviour3D> myObjectsRemove = new();

   // ...
   public Behaviour3D()
   {
      myObjects.Add(this);
      // ...
   }

   public virtual BeforeUpdate()
   { 
   }

   public virtual Update()
   {
   }
   public virtual Render()
   {
   }

   // Removal methods
   public void Remove()
   {
      myObjectsRemove.Add(this);
   }
   public static void Remove(Behaviour3D r)
   {
      myObjectsRemove.Add(r);
   }
   public static void RemoveAll()
   {
      myObjects.Clear();
      myObjectsRemove.Clear();
   }

   internal static void InternalGameLoop(){
      //Clear last frame

      // remove deleted objects
      foreach(var removeItem in Behaviour3D.myObjectsRemove)
         item.Behaviour3D.myObjects.Remove(removeItem);
      myObjectsRemove.Clear();

      foreach(var item in Behaviour3D.myObjects)
         item.BeforeUpdate();
      foreach(var item in Behaviour3D.myObjects)
         item.Update();
      foreach(var item in Behaviour3D.myObjects)
         item.Render();

      //Compute depth buffer   
   }

Each derived class has the opportunity to override the methods as they see fit:

  • BeforeUpdate()
  • Update()
  • Render()

Any parameters such as timing can be provided by protected variables.

Your main programs base entry point,App.cs simply calls every step:

Behaviour3D.InternalGameLoop() passing any of the needed protected variables.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .