You need to keep track of the isAlreadyIn state for each player instead of a single variable for all players. Better yet, keep track of which biome they were in before (you can use that to do biome to biome specific tasks, like things that only run when the player moved from Desert to Snow or something):
Dictionary<ulong, float> lastBiome = new Dictionary<ulong, float>();
Then you can use that list to check if a player is in a different biome than in the tick before and act upon it:
// Get the current biome of the player
float currentBiome = TerrainMeta.BiomeMap.GetBiome(player.transform.position, 1);
// Add the player and biome to the list if the player doesn't exist in it (first time, eg. after connecting)
if (!lastBiome.ContainsKey(player.userID))
lastBiome.Add(player.userID, currentBiome);
// Check if the biome has changed
if (currentBiome != lastBiome[player.userID])
{
// Player has moved from lastBiome[player.id] to currentBiome
switch (currentBiome)
{
case 0.5f: // Desert
SendReply(player, "You've entered the Desert.");
break;
// Mock values, just as an example
case 0.3f: // Snow
if (lastBiome[player.userID] == 0.5f) // Desert as last biome
SendReply(player, "You've moved from the Desert to Snow.");
else
SendReply(player, "You've moved to Snow.");
break;
}
// Save the current biome of this player in lastBiome
lastBiome[player.userID] = currentBiome;
}
Doing this every tick may not be optimal. Although there aren't any large tasks like looping through a big list, it shouldn't have a big performance impact but methods like GetBiome or SendReply are out of your control and may cause slowdowns. It would probably be better if you run this on a timer and loop through all players in a coroutine every second or so.
OnPlayerTick runs as many times as you have FPS on your server, that's easily over 20 times per second. What's best for you depends on how precise you need this to be. If you want there to be no chance that a player can move from biome A to B to C within a second and not have the A->B move trigger your code then you have to use a very small delay between your checks, so OnPlayerTick would be the most precise. If precision like that isn't necessary then I would recommend to use a timer and coroutine:
private IEnumerator checkPlayerBiomeChange()
{
BasePlayer[] players = BasePlayer.activePlayerList.ToArray();
foreach (BasePlayer player in players)
{
if (!player.IsConnected)
continue;
// Per-player code goes here
yield return new WaitForFixedUpdate();
}
}
This will run code that loops through all active players and waits for the next frame after each player, so if you have 40 players this will take 40 frames to complete. Adjust the timer accordingly so that this method doesn't get started while the previous one is still running or use a bool to prevent execution when it's already running.
This is how to run a timer that starts this method as a coroutine every 2 seconds:
timer.Every(2f, () => ServerMgr.Instance.StartCoroutine(checkPlayerBiomeChange()));
Hope that gives you a little something to work with.