Use a string to call a class
Hi
I have several classes for each different level of the game. I'm using a state machine approach and when winning or losing a level, a intermediate state is loaded. I'd like to call previous level or next level class by a string, so I don't need to create a intermediate state for each level.
The name space is Code.States and the classes for each level are named PlayStateLevel1, PlayStateLevel2, etc.
This is my approach:
public WinLoseState (StateManager managerRef)
{
.
.
string currentLevel = "PlayStateLevel" + CalculateLevelNumber ();
Type type = Type.GetType(string.Format("Code.States.{0}", currentLevel));
System.Object obj = System.Activator.CreateInstance(type);
}
Then, the idea is to use the obj reference to restart level or go to next level:
if (win)
{
.
.
manager.SwitchState (new obj (manager));
}
So, instead of using:
manager.SwitchState (new PlayStateLevel2 (manager));
I want to use:
manager.SwitchState (new obj (manager));
But it's giving me a long error:
MissingMethodException: Method not found: 'Default constructor not found...ctor() of Code.States.PlayStateLevel1'.
System.Activator.CreateInstance (System.Type type, Boolean nonPublic) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System/Activator.cs:368)
System.Activator.CreateInstance (System.Type type) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System/Activator.cs:254)
Code.States.WinLoseStateLevel1..ctor (.StateManager managerRef) (at Assets/Code/States/WinLoseStateLevel1.cs:22)
Code.States.PlayStateLevel1.StateUpdate () (at Assets/Code/States/PlayStateLevel1.cs:34) StateManager.Update () (at Assets/Code/Scripts/StateManager.cs:37)
I've tried several different combinations, but neither seems to work. I've followed some instructions, like here: http://forums.asp.net/t/1904441.aspx?Possible+Use+a+string+variable+to+call+a+class+in+C+
Any idea on how to approach this?
Answer by pekalicious · Nov 28, 2015 at 11:18 PM
There are several issues with your code. Reflection is kinda tricky, so I'll suggest you make some changes in your code for convenience.
First, you cannot call new on an object (a class that has instantiated). You can only do that with a class. Luckily, Activator.CreateInstance already does that for you (takes the type of a class and calls new and then returns the instantiated object).
However, your PlayStateLevel class has a constructor with a parameter. Now, there is a way to call it by passing your variable, but to make things easy, I suggest you have a constructor with no parameters and simply expose the manager variable so you can set it later.
Finally, you'll need to cast (explicitly tell C# what type the object is) so you can pass it to your method.
By the way, I assume that all PlayStateLevel class have a base class called PlayStateLevel. If not, I highly recommend you do.
Thus, you will end up with the following:
public WinLoseState (StateManager managerRef)
{
.
.
string currentLevel = "PlayStateLevel" + CalculateLevelNumber ();
Type type = Type.GetType(string.Format("Code.States.{0}", currentLevel));
PlayStateLevel pLevel = (PlayStateLevel)System.Activator.CreateInstance(type);
pLevel.manager = manager;
}
and
manager.SwitchState (pLevel);
Answer by Cheo · Nov 29, 2015 at 01:15 PM
Hi pekalicious
Thanks for your reply and suggestions. I still have the same system error as before. The lines triggering the error are these, both for my previous script and your script:
My line:
System.Object obj = System.Activator.CreateInstance(type);
Your line:
PlayStateLevel pLevel = (PlayStateLevel)System.Activator.CreateInstance(type);
I'm not using a PlayStateLevel base class as you mentioned. These are the classes involved so you can have a bigger picture of the design. I'm only including the relevant snippets for each class:
IStateBase class:
namespace Code.Interfaces
{
public interface IStateBase
{
}
}
StateManager class:
using UnityEngine;
using Code.States;
using Code.Interfaces;
public class StateManager : MonoBehaviour {
private IStateBase activeState;
public void SwitchState(IStateBase newState)
{
activeState = newState;
}
}
PlayStateLevel1 class:
using UnityEngine;
using Code.Interfaces;
namespace Code.States
{
public class PlayStateLevel1 : IStateBase
{
private StateManager manager;
public PlayStateLevel1 (StateManager managerRef)
{
manager = managerRef;
Application.LoadLevel("Level1");
}
public void StateUpdate()
{
if ( manager.statTracker.GetLifePoints() <= 0) {
manager.SwitchState (new WinLoseStateLevel1 (manager));
}
}
}
}
WinLoseStateLevel1 class:
using UnityEngine;
using Code.Interfaces;
namespace Code.States
{
public class WinLoseStateLevel1 : IStateBase
{
private StateManager manager;
public WinLoseStateLevel1 (StateManager managerRef)
{
manager = managerRef;
/*** THIS IS THE CODE I ORIGINALLY ADDED BUT GIVES AN ERROR ***/
Type type = Type.GetType(string.Format("Code.States.{0}", currentLevel));
System.Object obj = System.Activator.CreateInstance(type)
/**************************************************************/
}
public void ShowIt()
{
if (GUI.Button (new Rect (10, 10, 150, 100), stringNextLevel))
{
manager.SwitchState (new PlayStateLevel2 (manager));
}
if (GUI.Button (new Rect (10, 110, 150, 100), stringPlayLevelAgain)) {
manager.SwitchState (new PlayStateLevel1 (manager));
}
}
}
}
As you can see, the idea is to use a string for:
manager.SwitchState(new STRING (manager));
Any suggestion?
So the problem is that your 'SwitchState', method expects an object of type IStateBase, which means that some way or another, you need to pass an object of that type.
Don't get me wrong, this is correct design. You should not change that method to expect an object of type 'object'. However, you still need to figure out how to get a IStateBase given a string.
The reason my code fails is because you don't have a PlayStateLevel base class. But since you only care about IStateBase, you can also do this:
IStateBase pLevel = (IStateBase)System.Activator.CreateInstance(type);
And to be completely correct, since your constructor needs a manager, you will call this:
IStateBase pLevel = (IStateBase)System.Activator.CreateInstance(type, manager);
Remember, this will ONLY work if the 'type' is a class that is a subtype of 'IStateBase' AND has a constructor that expects a 'State$$anonymous$$anager'.
Hi, thanks for your reply and sorry for the late response, but I've spent the last days trying code lines you suggested but I was not able to make it work. I got a bunch of errors.
First error I encounter is: "
$$anonymous$$issing$$anonymous$$ethodException: $$anonymous$$ethod not found: 'Default constructor not found...ctor() of Code.States.PlayStateLevel1'
" So, to fix it, I create a default constructor for PlayStateLevel1.
When I fixed the first error, a second error occurs: "
NullReferenceException: Object reference not set to an instance of an object
" The error refers to the methods StateUpdate()
and ShowIt()
in PlayStateLevel1 class, which are referenced in the State$$anonymous$$anager class as follows:
void Update ()
{
if (activeState != null)
activeState.StateUpdate ();
}
void OnGUI()
{
if (activeState != null)
activeState.ShowIt ();
}
If I comment both methods StateUpdate()
and ShowIt()
in PlayStateLevel1 class in order to prevent the compiler from computing them, the running game gives no error, but when I click on the button to restart the level, the GUI disappears but nothing else happen, no restart, nothing.
A Debug.log of pLevel variable yields '
Code.States.PlayStateLevel1
'
Any new idea welcomed!!