Jump to content
  • Advertisement
Sign in to follow this  
flodihn

C# All my logic in static classes.

Recommended Posts

Hello guys, I just want to share some of my findings after more than 10 years of software development. 

During my last project, I made the decision for our dev team to go against what is quite commonly the default programming behaviour in Unity and most likely many other game engines. What we put all persistent data (as serialisable JSON) in a separate layer and use static functions to operate on those. This is more similar how C would operate, or most functional programming languages.

It worked very well and caused no problem for us, so now I decided to take it one step further and make a sub system in Unity that facilitates that coding style in a more generalised way. I call this system AOEngine.

This sub system looks like this:

Model -> Stores data as json serialisable objects, dictionaries or both.

View -> Listens to model, reacts when data is create, changed or destroyed and tries to render the object. The view is a monobehaviour and can be seen in the Unity Editor unlike the model and the controller.

Controller -> Runs a state machine which executes game specific code that creates, updates or destroys data.

State Machine -> Switches/keep/updates the global state of the game.


The model, view and controllers are standard OOP classes communicating with interfaces using a observer pattern. So far everything is pretty standard.

But when you realise that after separating the logic and data, what you have left are logic classes that always the same, just executing on different types of data. This means there is no need to instantiate pure logic and using static classes is actually a pretty neat idea.

The reason static classes are getting quite a bad reputation is that in most cases they are used to share some global state, and easily become a tight dependency to many other areas in the code. But when the static class is just pure logic, we do not have this problem.

The AO engine provides one point of entry, a GameInit prefab which is used to create the model, view and controller, the user have to provide an initial state for the state machine to run. When you import the AO Engine as a Unity package, you just need to drop one prefab into your scene, and that is the only prefab you need to start. 

Here is the code for the GameInit:

{

    public class GameInit : MonoBehaviour
    {
        public GameObject viewPrefab;

        public string InitialState;

        void Start() {
            IModel model = new AOEngine.Model.Model();
            IView view = CreateUnityViewFromPrefab();
            IController controller = new AOEngine.Controller.Controller();

            view.Setup(model);
            controller.Setup(model, view);
            StateMachine.Setup(model);

            GameState initialGameState = TryCreateInitialGameState();

            StateMachine.SwitchState(initialGameState);
        } 

The InitialState have to be defined in the Unity editor, you give it the full name to the initial game state for your game, for example MyGame.MenuGameState, then you have to make sure this class exists (goes without saying) and it must inherit AOEngine.StateMachine.GameState.

This is how the a game state would look like:

namespace MyGame 
{

    public class MenuGameState : GameState
    {   
        public override void OnEnter() {

            GameObjectData uiCamera = new GameObjectData {
                {"uid", "camera"},
                {"prefab", "Cameras/UICamera"}  
            };  

            model.CreateData<GameObjectData>(uiCamera);

            UIData mainMenuData = new UIData {
                {"uid", GameUids.MAIN_MENU_DATA},
                {"prefab", "UI/Prefabs/MainMenuCanvas"}
            };
          
            model.CreateData<UIData>(mainMenuData);
            controller.BindLogic(typeof(MainMenuLogic), mainMenuData);

And here is where things get interesting, first I create the some data, all data should have a unique uid to identify it, if no uid is given the model will generate an uid automatically. Optionally, a piece of data can be bound to one or more logic classes, which are static, using the controller.BindLogic providing the logic class and the data uid to bind it to. This is basically a component system, but not using monobehaviours or standard OOP.

The static logic class would look something like this:

namespace MyGame 
{

    public static class MainMenuLogic
    {   
        private static IModel model;

		[OnLogicSetup]
        public static void Setup(IModel _model) {
            model = _model;
        }   

        [OnLogicCreate]
        public static void OnCreate(string dataUid) {
            UIData data = model.GetData<UIData>(dataUid);

            model.UpdateProperty(data, new Dictionary<string, object> {
                { "ui_callbacks", new Dictionary<string, object> {
                        {"OnButtonClicked", "MyGame.MainMenuLogic#OnButtonClicked"}
              }}  
            });         
        }       

        [OnLogicUpdate]
        public static void OnUpdate(string dataUid) {
            UnityEngine.Debug.Log("MainMenuLogic.OnUpdate");
        } 
                                  
        [OnLogicDestroy]
        public static void OnDestroy(string dataUid) {
            model.DestroyData(GameUids.MAIN_MENU_DATA);
        }       

        public static void OnButtonClicked(string buttonName) {
            if(buttonName == "QuitButton")
                UnityEngine.Application.Quit();

            if(buttonName == "StoryButton") {
                StateMachine.SwitchState(new StoryGameState());
            }
        }
    }
}

So not being a fan of monobehaviours, I use them to a minimum, only for position translations, particle systems, collision detection and audio playing, and then these monobehaviours would be bare components without any logic or state.

I need to figure how communication between my static components, I will probably use a game event system for this. 

So far I am quite happy with system where all my logic are running in static classes. I wonder if there are any other people out there that reached the same conclusions, what are your opinions of this approach?

Edited by flodihn

Share this post


Link to post
Share on other sites
Advertisement
41 minutes ago, flodihn said:

The reason static classes are getting quite a bad reputation is that in most cases they are used to share some global state, and easily become a tight dependency to many other areas in the code. But when the static class is just pure logic, we do not have this problem.

Yes, you do, because your class is not pure logic.

What you're talking about as "functional" or "how C would operate" is not what you've actually shown in your MainMenuLogic class. In that class, you have a static state... the model. 

If you want that class to be functional, you should remove the model static member and refactor the class like this:

namespace MyGame 
{

    public static class MainMenuLogic
    {   

        [OnLogicCreate]
        public static void OnCreate(IModel model, string dataUid) {
            UIData data = model.GetData<UIData>(dataUid);

            model.UpdateProperty(data, new Dictionary<string, object> {
                { "ui_callbacks", new Dictionary<string, object> {
                        {"OnButtonClicked", "MyGame.MainMenuLogic#OnButtonClicked"}
              }}  
            });         
        }       

        [OnLogicUpdate]
        public static void OnUpdate(string dataUid) {
            UnityEngine.Debug.Log("MainMenuLogic.OnUpdate}
        
        [OnLogicDestroy]
        public static void OnDestroy(IModel model, string dataUid) {
            model.DestroyData(GameUids.MAIN_MENU_DATA);
        }       

        public static void OnButtonClicked(string buttonName) {
        	if(buttonName == "QuitButton")
                UnityEngine.Application.Quit();

            if(buttonName == "StoryButton") {
                StateMachine.SwitchState(new StoryGameState());
            }
        }
    }
}

 

Share this post


Link to post
Share on other sites
21 minutes ago, ChaosEngine said:

Yes, you do, because your class is not pure logic.

What you're talking about as "functional" or "how C would operate" is not what you've actually shown in your MainMenuLogic class. In that class, you have a static state... the model. 

If you want that class to be functional, you should remove the model static member and refactor the class like this:


namespace MyGame 
{

    public static class MainMenuLogic
    {   

        [OnLogicCreate]
        public static void OnCreate(IModel model, string dataUid) {
            UIData data = model.GetData<UIData>(dataUid);

            model.UpdateProperty(data, new Dictionary<string, object> {
                { "ui_callbacks", new Dictionary<string, object> {
                        {"OnButtonClicked", "MyGame.MainMenuLogic#OnButtonClicked"}
              }}  
            });         
        }       

        [OnLogicUpdate]
        public static void OnUpdate(string dataUid) {
            UnityEngine.Debug.Log("MainMenuLogic.OnUpdate}
        
        [OnLogicDestroy]
        public static void OnDestroy(IModel model, string dataUid) {
            model.DestroyData(GameUids.MAIN_MENU_DATA);
        }       

        public static void OnButtonClicked(string buttonName) {
        	if(buttonName == "QuitButton")
                UnityEngine.Application.Quit();

            if(buttonName == "StoryButton") {
                StateMachine.SwitchState(new StoryGameState());
            }
        }
    }
}

 

You are actually right about this, looking at the changes I start to like it better because then I could get rid of my setup function and reduce complexity.

The reasoning behind actually storing a pointer to the IModel class was that if the model should be re-created, the setup function would have to be called again for every static logic class, so it should not in theory be much of a problem.

I thought about inspecting a logic class, trying to find member variables that are not of IModel and throw an exception if found.

Thanks for the feedback, I will consider this change.

PS. I also forgot to mention that the Setup function is only called the first time the static class is used, will OnLogicCreate is called everytime the class is bound to a data though the controller.BindLogic method.

Edited by flodihn

Share this post


Link to post
Share on other sites
1 hour ago, flodihn said:

I also forgot to mention that the Setup function is only called the first time the static class is used, will OnLogicCreate is called everytime the class is bound to a data though the controller.BindLogic method.

Yep, and in theory, there's nothing wrong with that. 

But in practice, things go wrong and you're suddenly trying to figure out why the model is a different instance. :)

 

Share this post


Link to post
Share on other sites
8 hours ago, ChaosEngine said:

Yep, and in theory, there's nothing wrong with that. 

But in practice, things go wrong and you're suddenly trying to figure out why the model is a different instance. :)

 

Destroying the model is more of a theoretical problem rather than a practical, because destroying the model basically means destroying the whole game. Since all persistent state is in the model, if one would destroy it, recreating the controller or view with it would have no side effects (in theory). So I can hardly find a scenario in real life where the instance would actually change.

In fact, the only real application I see for recreating or creating multiple instances, is for the view.

Let me explain, if you have a game, and what to log everything from file, one solution would be to create a second instance of a view, which does not render an graphics but log everything to file or similar.

If you would add support for the model, view and controller to register themselves over the network (TCP), a whole new domain of possibilities opens up.

If you start a model and controller on a machine, you have server. The clients would run a special controller which just forwards events to the server through the input system (keyboard/mouse etc), and a view that is registered on the model running on the server.

Maybe even better, maybe setup a special controller that listens to all changes to the servers model and apply the same changes or the clients local model.
In case the server would go down, all the clients could vote for a new servers, which then in theory, would just need to fire up the standard controller the game would just continue running.

Also, imagine the case of 2 players network game, if other would want to observer the game, it could be done by just creating a special observing view hooked up on any of the models running in the game the want to observe.

 

But I would argue that any of these scenarios is not as nearly as important as the clean code you will get out from forcing this structure on your game. My goal here is to become a better developer, to be able to make games faster and avoid growing the complexity even as the game increases in scope.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!