Ok I’ll try to describe the entire problem better within this one post. Let me know if more detail is needed:
SCENES: I have 2 scenes in my game - Startup and Match.
Startup is my initial scene, and it is where I include all of my persistent game state, as well as some other common game objects I didn’t want to have to remember to include each scene. PersistentGameObjects
owns the GameManager
and DontDestroyOnLoad
scripts. MatchManager
owns the MatchManager
script, and MainCamera
owns the CameraController
script.
Match is my secondary scene. It only holds my main player object, which is what I am trying to reference in the players
array. This is the scene where the core gameplay takes place
SCRIPTS:
GameManager script:
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{ //This class essentially handles how to switch between scenes from a high-level perspective. It relies on data from my GameState class, and functionality from Unity's UnityEngine.SceneManagement class.
// Start is called before the first frame update
public GameState gameState;
public MatchManager matchManager;
void Start()
{
SceneManager.LoadScene("Match"); //ultimately will start with your company icon or something, but for now just jumps right into a match.
SceneManager.sceneLoaded += OnSceneLoaded;
SceneManager.sceneUnloaded += OnSceneUnloaded;
}
// Update is called once per frame
void Update()
{
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
Debug.Log(scene.name);
if(scene.name == "Match")
{
matchManager.Activate(); //Called when you want to start a new match.
}
}
void OnSceneUnloaded(Scene scene)
{
if(scene.name == "Match")
{
matchManager.Deactivate(); //Called when you want to end a match.
}
}
}```
MatchManager Script: Sorry, a lot of this code is probably superfluous. Note that the `MyCamera` field is set in the inspector to my prefab called `MainCamera`.
```using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MatchManager : MonoBehaviour
{ //Match Manager is basically a system. It should be a singleton, that handles all overarching rules and data for a match. Some of these things include transitioning between gameplay, animation, and character select, as well as how to start and finish a match. It will also build "climbs" by connecitng random "chunk" instances together.
//as of 7/25/23, each chunk is 1 screen, 15 tiles, or 30 meters tall. so each climb is about 150 meters.
public GameObject myCamera;
public List<GameObject> chunks = new List<GameObject>(); //an array of "chunk" game objects to use as the building blocks for constructing a level. Each chunk prefab will have a ChunkData cpnt describing various attributes it has.
public List<GameObject> bossChunks = new List<GameObject>(); //similar to chunks array, except these are specifically for boss battles. The order and size of this array matters, due to the way it is used in MapBossToChunk(). That's why I made it readonly.
public string boss; //String telling which tiki boss you're facing on a given climb.
public string phase = "inactive"; //string telling what phase of the climb you are in. Legal values: "inactive", "blackScreen", "startAnimation", "characterSelect", "playing", "endAnimation"
public int height = 0; //represents how far up the mountain(s) you have climbed. Each tile is 2 meters, or 16px, or 16 game units. (This field represents meters)
private int mountainNumber = 0; //keeps track of how many mountains you have climbed.
private string mountainName; //describes the predominant environment of the mountain, and which tiki boss will be on it. (Though some things will still be randomized for variety's sake.) legal values: "tide", "beach", "jungle", "snow", "town", "lava".
private readonly int chunkSizeInMeters = 30; //possible this may need to be updated, and it would be nice if it was only here.
private readonly int chunkSizeInGameUnits = 240; //possible this may need to be updated, and it would be nice if it was only here.
// Start is called before the first frame update
void Start()
{
//TEMP: this might be where you instanciate all the game objects in your game object lists, but I think more likely it will be in PositionChunks()
}
// Update is called once per frame
void Update()
{
}
public void Activate()
{ //Since the game object carrying this script will persist between scenes, it is important to ensure it only does it's job in the appropriate scenes. (during a match).
phase = "blackScreen";
BuildClimb();
}
public void Deactivate()
{ //see "Activate()"
phase = "inactive";
}
private List<GameObject> FindSuitableChunks(int level)
{ //at the start of each climb, you must BuildClimb() from chunks. This method creates a sub-array from the chunks[] field consisting only of desirable chunks for the given climb. (Easier chunks for the earlier mountains (level param), more tide pool chunks if you visit a tide pool mountain, etc (environment param).
List<string> legalEnvironments = new List<string> {"tide", "beach", "jungle", "town"}; //other environments that are allowed to be featured in a climb, even though they are less commonly seen.
List<int> dontRepeats = new List<int>(); //as you build a climb, you will select chunks from the "chunks" field. In order to not select the same one twice, this array of ints will keep track of the indexes of chunks that have already been selected, so you can remember not to pick them again.
List<GameObject> climbChunkRoster = new List<GameObject>(); //This is what
int heightInChunks = 5; //how many chunks should exist in the climb
string rememberLastMountainName;
int desiredChunkDifficulty; //int [1,3] to compare to ChunkData to see if you want to include it in the mountain.
string desiredChunkEnvironment; //string of the legal environment types that you can compare to ChunkData to see if it is a chunk you want to include in the mountain.
//handle when it is POSSIBLE to start seeing certain terrains.
if(level > 1)
{ //you might start seeing snow after the first mountain
legalEnvironments.Add("snow");
}
if(level > 2)
{ //you might see lava after the second mountain.
legalEnvironments.Add("lava");
}
//decide the predominant environment that will be featured on this climb.
rememberLastMountainName = mountainName;
legalEnvironments.Remove(mountainName); //don't go to the same mountain as last time.
mountainName = legalEnvironments[Utilities.RandomInt('[', 0, legalEnvironments.Count - 1, ']')]; //after you have decided on all the legally available environments, randomly pick one.
legalEnvironments.Add(rememberLastMountainName);
//Each match follows a pattern of two shorter climbs followed by a longer final boss battle climb. Use modulus to keep track of where you are in this loop.
if(level % 3 == 0)
{ //every "third" level guarantees you visit the lava mountain for final boss fight. As such, the match loop goes in intervals of 3. The lava mountain is also a little bit taller than the first and second.
heightInChunks++; //make the mountain a little higher than the first two.
}
else
{ //every "first" OR "second" level
if (Utilities.RandomInt('[', 0, 1, ']') == 0)
{ //50/50 chance for the climb to be either 4 or 5 chunks high.
heightInChunks--;
}
if (level % 3 == 1/3)
{ //every "first" level
}
else if(level % 3 == 2/3)
{ //every "second" level
}
}
//search through chunks field, starting at a random location, to find a suitable chunk to add to the suitableChunks list. Repeat as many times as the value of heightInChunks.
void DecideChunkAttributes()
{ //use probability to decide the difficulty and environment type of the chunk that you want to look for in the chunks array. Once you know these two data points, you can compare them to each chunk's ChunkData until you find a match. (By using SearchChunks)
List<string> bag = new List<string>();
foreach(string str in legalEnvironments)
{
if(str != mountainName)
{ //create a list of all the oddball environments
bag.Add(str);
}
}
if(Utilities.RandomInt('[', 1, 4, ']') == 1)
{ //25% chance to pick a legal oddball environment
desiredChunkEnvironment = bag[Utilities.RandomInt('[', 0, bag.Count - 1, ']')];
}
else
{ //75% chance to pick the primary environment
desiredChunkEnvironment = mountainName;
}
desiredChunkDifficulty = (int)Mathf.Ceil((float)height / chunkSizeInMeters * 5 + Random.value - .5f); //As you climb higher, the chunks will become more difficult, with a bit of random variation. Chunks' levels are from 1-3. chunkSizeInMeters * 5 = 150 at time of writing. The design intention is to have you "level up" at a rate of about 1 level per climb, max 3.
}
void SearchChunks() //TEMP: Note that early in the development process, before a bunch of different chunks have been implemented, this function will return the default, chunks[0] pretty frequently. There's nothing wrong with the logic of the function, but rather, you have an incomplete set of input data.
{ //will search sequentially through chunks collection and add 1 chunk to the climbChunkRoster collection.
int startingLocation = Utilities.RandomInt('[', 0, chunks.Count - 1, ']'); //pick a random place to start searching through the list to find chunks
int currentLocation = startingLocation;
bool meetsRequirements = false;
bool isFirstIteration = true;
bool IsRepeat()
{
for(var i = 0; i < dontRepeats.Count; i++)
{
if (currentLocation == dontRepeats[i])
{ //you are iterating over a chunk that has already been used. Don't repeat it.
return true;
}
}
return false;
}
do
{ //iterate through chunks[] until you find a chunk that meets the difficulty and environment requirements.
//De/bug.Log(chunks[currentLocation].GetComponent<ChunkData>().difficulty == desiredChunkDifficulty && chunks[startingLocation].GetComponent<ChunkData>().environment == desiredChunkEnvironment && !IsRepeat());
//De/bug.Log(currentLocation);
//De/bug.Log("startExp");
//De/bug.Log(chunks[currentLocation].GetComponent<ChunkData>().difficulty);
//De/bug.Log(desiredChunkDifficulty);
//De/bug.Log(chunks[currentLocation].GetComponent<ChunkData>().environment);
//De/bug.Log(desiredChunkEnvironment);
//De/bug.Log(IsRepeat());
//De/bug.Log("endExp");
if(currentLocation < 0 || currentLocation > chunks.Count - 1)
{ //keep from going out of bounds.
currentLocation = 0;
}
if (chunks[currentLocation].GetComponent<ChunkData>().difficulty == desiredChunkDifficulty && chunks[currentLocation].GetComponent<ChunkData>().environment == desiredChunkEnvironment && !IsRepeat())
{ //if the randomly selected chunk meets the difficulty and environment requirements, and has not already been selected in this climb, then add it to the final climbRoster.
climbChunkRoster.Add(chunks[currentLocation]);
Debug.Log("EUREKA");
dontRepeats.Add(currentLocation); //also, keep track of its index, so you don't select it again this climb.
meetsRequirements = true;
}
if(currentLocation == startingLocation && !isFirstIteration)
{ //you've iterated through the entire list and not found a match. this should never happen, but if it does, catch it here to avoid infinite lp.
Debug.Log("iterated through entire chunks list & no match. Inf lp");
Debug.Log("FOOLSGOLD");
climbChunkRoster.Add(chunks[0]); //better to just append the first chunk you can find than have an inf lp. (Might want to ensure this is isn't a boss battle though...)
meetsRequirements = true;
}
currentLocation++;
isFirstIteration = false;
}
while (!meetsRequirements);
}
for (int i = 0; i < heightInChunks; i++)
{ //search for a new chunk as many times as needed
if (i == heightInChunks - 1)
{ //always make the last chunk a boss battle
climbChunkRoster.Add(MapBossToChunk());
}
else
{
DecideChunkAttributes();
SearchChunks();
}
}
return climbChunkRoster;
}
private void PositionChunks(List<GameObject> climbChunkRoster)
{ //from an array of 4 to 6 chunks, position each of the chunks in the right locations to build a climb. "climbChunkRoster is the list of chunks you will need to position.
for(int i = 0; i < climbChunkRoster.Count; i++)
{
Debug.Log("---");
Debug.Log(climbChunkRoster[0].name);
Debug.Log(i * chunkSizeInGameUnits);
Debug.Log("----");
Instantiate(climbChunkRoster[i]);
climbChunkRoster[i].transform.position = new Vector2(climbChunkRoster[i].transform.position.x, i * chunkSizeInGameUnits);
}
}
private GameObject MapBossToChunk()
{
int index = 0;
switch(boss)
{
case "Coco": //Tide Pool Tiki that rolls coconuts
index = 0;
break;
case "Bumpa": //Beach Tiki that rolls bouncing beach balls
index = 1;
break;
case "Jungo": //Jungle Tiki that rolls giant banana balls
index = 2;
break;
case "Frifri": //Snow Tiki that rolls expanding snow balls
index = 3;
break;
case "Tona": //Town Tiki that ?plays music to speed up enemies? ?Rolls soccer ball that changes direction any time it hits an enemy?
index = 5;
break;
case "Easta": //Lava Tiki that rolls exploding lava boulders
index = 6;
break;
}
return bossChunks[index];
}
private void BuildClimb()
{ //called whenever you want to connect several random "chunks" of level data together to form a single "climb"
PositionChunks(FindSuitableChunks(mountainNumber));
//I think this line of code is unnecessary?: camera.GetComponent<CameraController>().following = "players";
int x = 2;
Instantiate(myCamera);
myCamera.GetComponent<CameraController>().FindAllPlayers(); //TEMP: This might be called someplace else once I have a menu for selecting characters, but the general concept remains; Once you have players loaded into the scene, alert the camera to follow the players.
}
private void BuildCharacterSelectMenu()
{ //instanciate any menu / ui items necessary to allow the player to select their character.
}
}```
CameraController Script: In the inspector, I assign the following serialized fields: `following = “players”`, and `testingString = “original”`
```using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraController : MonoBehaviour
{
private GameObject[] players = new GameObject[1]; //an array of all the players in the climb, for averaging their position to decide where the camera should be positioned.
public string following = "static"; //should the camera position itself to follow the players, or something else?
public string testingString = "original";
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
private void Update()
{
if (following == "players")
{
PositionByPlayers();
}
else
{
//keep the camera's default position
}
}
public void FindAllPlayers()
{ //Called whenever the number of players in the scene changes. This way, the average position of each player can be accounted for when changing the camera’s position.
Debug.Log("FindAllPlayers()");
this.players = GameObject.FindGameObjectsWithTag("Player");
string objectType = players[0] ? "Instance" : "Prefab";
Debug.Log($"Array element is {objectType}: {players[0].name}");
following = "players";
testingString = "desired";
int x = 2; //Place where I can set a breakpoint. Not sure if additional things happen if I try to set one at the end bracket of a method defn.
}
public void PositionByPlayers()
{ //Actually position the camera on a frame-by-frame position, based on the data in players array
float sum = 0; //Another place for a breakpoint.
foreach (GameObject player in players)
{ //iteratively build up the sum
if (player == null)
{
Debug.Log("Player null");
}
if (player != null)
{
Debug.Log("ReadingPlayerPosition");
sum += player.transform.position.y;
}
}
if (players.Length == 0)
{ //handle divide by zero exception
Debug.Log("SsnA: No players detected");
transform.position = new Vector3(transform.position.x, transform.position.y, transform.position.z);
}
else
{ //use sum and players.Length to find an average y position that the camera will update to. *THIS is the block I want to be able to enter, but can’t because of null array
Debug.Log("SsnA: Positioning relative to players"); //temp
transform.position = new Vector3(transform.position.x, sum / players.Length , transform.position.z);
}
}
}```
DontDestroyOnLoad Script:
```using UnityEngine;
public class DontDestroyOnLoad : MonoBehaviour
{
private void Awake()
{
DontDestroyOnLoad(gameObject);
}
}```
Can't really tell without more info.. but keep in mind that you are not modifying / populating an array, you are replacing the array in the variable with another array returned by 'FindGameObjectsWithTag'. If you are keeping a reference to that array somewhere else, then that may still be referring to the old array. Also, making it public means it's probably serialized, and therefore can be modified by serialization. If you are only modifying the array through code of that class, make it private. All active scenes would show in the hierarchy.
– NorwoodIn my personal opinion, it is nearly impossible unity engine modifies to null. How about check if there are any code that reinitializing players, if not tracking with debug.log () to trace your code -like FindGameObjectsWithTag- by the logic.
– Marla