I'm making a game in Unity and I learned to make my Player object static for reference by other objects quickly and easily. So I have a "public static Player player." I remember when I first made this and began referencing it, some scripts were telling me that I could not access certain things without a local instantiation. So I made a Player p and set it equal to Player.player and it then allowed me to reference its functions and variables. It seemed like I could have some kind of access like reading not writing or just accessing variables but not function calls without a local reference, but I later found I could access anything without the local reference. I was then able to get rid of the local references completely in scripts that were originally not able to access the elements without it. I have no idea what changed. Now, however, I'm making a static SaveData object and I'm running into the same issue. Things I'm accessing with Player.player is not working with my SaveData.saveData. What is going on? What could possibly be different? When can I and can't I access this stuff without a local reference?
2 Answers
You might be confusing the concepts of Access (who is allowed access data) and Allocation (where is data stored in memory). I hope the following explanation clears things up for you!
Access
Access refers to who is allowed to access objects of an object. You can specify this for both data ("who can read/write a variable?") and functions ("who can execute this function?") You will use three keywords for this:
- Public: any other object can see this variable/function
- Protected: only the object itself and derived classes can see this variable/function
- Private: only the object itself can see this variable/function
Here's an example to grasp the concept:
class Enemy {
private:
unsigned int m_id;
protected:
unsigned int m_health;
public:
void DamageMe(unsigned int damage_amount);
};
Let's say you have a class called Enemy that represents an enemy in your game. You want to give each enemy a unique ID, so you can retrieve it later. This ID is assigned upon creation via the constructor, and you never want it to change! Therefore, you mark it as private. This way, only the enemy class can initialize the ID, and it can not be messed up by derived classes or other classes.
Additionally, you want to store the enemy's health. We don't want any other object to go change the enemy's health willy-nillily, as we wanna be sure to trigger the right animations, and invincibility frames when the enemy gets hit. However, if we later decide to implement a more specific enemy that derives from the Enemy class, it should be able to modify its health. You might for example create class HealingEnemy : public Enemy
that heals itself up from time to time. Therefore, we mark it as protected. This way only the class itself and its derivatives can change the variable.
Finally, we want to be able to damage the enemy. We do this via a clean function, so that we can take care of animations and invincibility frames. We want any other game object to be able to cause damage to the enemy. Therefore we mark the function as public. This means any object that has a reference to the variable can execute the member function.
Allocation
Allocation refers to where data is stored in memory. There are two options:
- Member/Local: the data / function applies individually to each instance of the class
- Static: the data / function is shared among all individuals of the class
Here's an example to grasp this:
class Enemy {
unsigned int m_health;
static unsigned int number_of_enemies_created;
void DamageMe(unsigned int damage_amount);
static unsigned int GetNumberOfEnemies();
};
Let's say we create 3 instances of the class Enemy. We want each of these to individually store their health. After all, if we punch one enemy in the face for 5 damage, we don't want the health of the others to change. Therefore we use a local/member variable (no keyword used to specify this). By typing my_enemy_1.m_health
we will get the health of that specific Enemy instance. Notice that we use a specific instance of Enemy to access this variable to specify which enemy we want to retrieve the health of.
Additionally, we want to keep count of how many enemies we have created. This number doesn't really belong to a specific instance of the Enemy class. It's a property of the class itself. Therefore we use a static variable (using the static keyword). By typing Enemy::number_of_enemies_created
we get the total number of enemies. Notice that we don't specify a specific instance name to access this variable! We use the class to access this static variable.
Similarly, we can mark functions as local (no keyword) or static (static keyword). We want the function DamageMe
to apply to one specific enemy, not all enemies at once! In contrary, GetNumberOfEnemies
applies across all instances of the Enemy class, therefore we mark it as a static function. Calls to these functions would look like this: my_enemy_1.DamageMe(5);
and Enemy::GetNumberOfEnemies();
. Once again, notice how we use a specific instance's name in the local case, and the class name in the static case.
Where access problems can occur
From the information you've given, I can really see what exactly is going on. There's one problem that causes access problems that often occurs when using static functions/variables. You cannot access local/member properties from a static function!
Let's say you are implementing the void DamageMe(unsigned int damage_amount);
function:
void DamageMe(unsigned int damage_amount) {
m_health -= damage_amount;
if (m_health <= 0) { number_of_enemies--; }
}
This will work just fine. As DamageMe is a local/member function, it is executed on a specific instance of the Enemy class (e.g. my_enemy_1). The compiler therefore knows whose "m_health" to reduce. Additionally, if the enemy dies, we can reduce the number_of_enemies counter by one, as it is a property of the class, there is only one of this variable.
Now let's say we implement static unsigned int GetNumberOfEnemies();
like this:
static unsigned int GetNumberOfEnemies() {
PrintMessage("My health is: " + m_health);
return number_of_enemies;
}
Your compiler will violently complain reading this. We are trying to print the enemy's health, and then we return the total number of enemies. This last part is fine. We can retrieve a static variable in a static function, as there is only one copy of the number_of_enemies variable. When we try to retrieve m_health, things go wrong. The compiler has no way of knowing what instance of Enemy you are trying to read the health of. After all, its a static function (and it is called as Enemy::GetNumberOfEnemies()). There might not even be an instance of enemy yet!
I suspect that this is what is going wrong in your code. To fix it, consider whether you really need your Player class to be static. Maybe there is a different way to retrieve it. For example you could have a different object keep track of all player instances in the game world with an interface such as:
class PlayerTracker {
static std::vector<Player*> m_players;
static void RegisterPlayer(Player* player);
static Player* GetPlayer(unsigned int id);
static std::vector<Player*> GetAllPlayers();
}
You could even add all of these functions to your player class and keep the Player class members itself non-static. Hope this helps!
-
\$\begingroup\$ The prototype code is c++ code, but the concept are similar in C#. Also, these are obviously simplified examples, compared to real-world classes. But I hope the concepts are more understandable like this! \$\endgroup\$ Commented Jun 4, 2017 at 12:32
Okay, I figured it out. I knew the right way to do it but it turns out I wasn't actually doing it the way I knew to do it. The problem was that I was trying to reference the non-static elements directly rather than going through the static element within it. For example, I was trying to reference SaveData.someArray[i] rather than SaveData.saveData.someArray[i]. No one could have known that of course, especially since I claimed I was doing it right in the question. I'll mark the other answer as right since it does hold all the correct information.
-
\$\begingroup\$ From that example, it looks like you're using a singleton pattern - is that right? \$\endgroup\$– DMGregory ♦Commented Jun 4, 2017 at 21:59