| Project Type | Group project | 4 students |
| Project Timeline | 8 weeks | 2020 |
| Software Used | Unity |
| Languages Used | C# |
Hunter Gatherer is a multiplayer real-time strategy game located on an island.
You manage your tribe by gathering resources, crafting items, and setting up your camp to rest.
Meanwhile all players are pushed towards each other by the flood.
You prepare for combat against other tribes to come out victorious.
Setting up the base structure as modular as possible was the main focus since future student teams will continue development on this game.
For networking we used photon. It uses a peer to peer solution where one of the players is the host.
Benjamin van der Wolf (one of the team members)
wrote a wrapper for photon's SDK.
During the project I worked on the win/los condition using this photon wrapper. The system is made to make a fair judgement on who has won the
game when two players die at approximately the same time.
When a player dies it sends a 'loss request' with a time stamp to the host. The host will wait for 200 ms to collect all requests.
If other players send a 'loss request' to the host within this wait time the host will determine who died first.
If the last two players die at the same time, the game will result in a draw.
The flow chart below shows how the host handles a loss request.
These code blocks show the two methods used to decide whether a player wins, loses, or draws.
//Decides on what decision to make based on the time difference from the requests
private void DecideOnEndGameState(NetworkingRequest finishedRequest, NetworkingRequestType type, int targetViewId)
{
//Create array of requests ordered by their timestamp
KeyValuePair<int, string>[] requests = finishedRequest.timeStamps.OrderBy(timeStamp => DateTime.Parse(timeStamp.Value)).ToArray();
Dictionary<int, int> content = new Dictionary<int, int>();
//Get the amount of players still alive
IEnumerable<PhotonPlayer> players = PhotonNetwork.PlayerList.Where(player => (int)player.CustomProperties[PlayerPropertyHandler.PlayerStateKey] == (int)PlayerState.Playing);
int amountOfPlayersAlive = players.Count();
if (requests.Length < amountOfPlayersAlive)
{
//If the amount of request is smaller than the amount of players alive all players requesting will lose
for (int i = 0; i < requests.Length; i++)
{
content.Add(requests[i].Key, (int)NetworkingLoseDesicionType.Lost);
}
if (requests.Length == amountOfPlayersAlive - 1)
{
//If there is only one player left that didn't make a loss request that player will win
Dictionary<int, string> dict = requests.ToDictionary(x => x.Key, x => x.Value);
foreach (var player in players)
{
if (!dict.ContainsKey(player.ActorNumber))
{
//Raise event to notify the winner cliet it won
RaiseEvent(NetworkingEventType.PlayerWinGame, null, new int[] { player.ActorNumber }, true);
break;
}
}
}
}
else { content = GetCloseEndGameStateResults(requests); }
PhotonNetwork.RaiseEvent((byte)NetworkingRequestDecision.LoseDecision, new object[] { content, targetViewId }, GetEventOptions(finishedRequest), SendOptions.SendReliable);
}
//Gets the EndGame state result when it needs to decide if between winning, losing or a draw
private Dictionary<int, int> GetCloseEndGameStateResults(KeyValuePair<int, string>[] requests)
{
Dictionary<int, int> content = new Dictionary<int, int>();
NetworkingLoseDesicionType endState = NetworkingLoseDesicionType.Lost;
for (int i = 0; i < requests.Length; i++)
{
//Once it is draw all other request will be draw so no logic needs to be applied
if (endState != NetworkingLoseDesicionType.Draw)
{
if (i == requests.Length - 1)
{
//Win if request is the latest and no other requests are draw
endState = NetworkingLoseDesicionType.Win;
}
else if ((DateTime.Parse(requests[i].Value) - DateTime.Parse(requests[requests.Length - 1].Value)).TotalMilliseconds < GameStateManager.Instance.DrawMargin)
{
//Draw if the time between the the last request and this request is inside of the drawMargin
endState = NetworkingLoseDesicionType.Draw;
}
else { endState = NetworkingLoseDesicionType.Lost; }
}
//Add the state with player to the content dictionary
content.Add(requests[i].Key, (int)endState);
}
return content;
}
To make sure we don't run into problems with the User Interface when switching scenes, like having to reset references or having to rework a lot of elements for small changes, I used a scalable solution. Instead of having a prefab of the UI, there is a scene that loads additively with the game scene and displays the UI. All UI elements only need references to objects within the UI scene. Using the MVP pattern the UI script only needs to subscribe to events from a presenter to update correctly.
The flow chart below shows how the scene loading solution works inside Hunter Gatherer.
The following code snippet shows the LoadGameScene method which loads the new scene and if needed the UI. After the new scene is loaded it unloads the previous scene.
//Loads a game scene
public void LoadGameScene(string sceneName)
{
//Don't continue if scene doesn't exits
if (!SceneIsLoadable(sceneName))
{
return;
}
//Don't load the scene if it's already loaded
if (!UnitySceneManager.GetSceneByName(sceneName).isLoaded && !isLoading)
{
isLoading = true;
//Load new game scene
UnitySceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive).completed += loadOperation =>
{
//Unload current game scene
UnitySceneManager.UnloadSceneAsync(UnitySceneManager.GetActiveScene()).completed += unloadOperation =>
{
//Set new game scene as active scene
UnitySceneManager.SetActiveScene(UnitySceneManager.GetSceneByName(sceneName));
};
isLoading = false;
StartCoroutine(CheckPlayersLoaded());
//Load the GameUI scene
LoadGameUIScene(gameUISceneName);
};
}
else
{
LoadLoadedOrLoadingSceneError(sceneName);
}
}
The flooding system is made to be a designer friendly tool. It allows the designer to easily change the amount of flood sections depending on the map. The height of flood sections can easily be adjusted with a flood indicator that shows the flood on top of the section.
The following code shows a method used to add new flood sections.
//Adds flood sections
private void AddSections(int amount)
{
if (amount <= 0) { return; }
for (int i = 0; i < amount; i++)
{
//Spawn section and set parent
Transform sectionEdge = Instantiate(floodSectionPrefab, base.transform);
sectionEdge.transform.parent = gameObject.transform;
//Set position of section
sectionEdge.position = new Vector3(
sectionEdge.position.x,
SectionEdges.Count == 0 ? 0 : SectionEdges[SectionEdges.Count - 1].transform.position.y + 10,
sectionEdge.position.z);
SectionEdges.Add(sectionEdge.GetComponent<SectionSettings>());
}
}
The following code shows a method used to remove flood sections.
//Removes and deletes flood sections
private void RemoveSections(int amount)
{
if (amount <= 0) { return; }
#if UNITY_EDITOR
for (int i = 0; i < amount; i++)
{
//Delay the deletion of object because destroyImmediate can't be called in OnValidate and Destroy can't be called in the editor
UnityEditor.EditorApplication.delayCall += () =>
{
DestroyImmediate(SectionEdges[SectionEdges.Count - 1].gameObject);
SectionEdges.RemoveAt(SectionEdges.Count - 1);
};
}
#endif
}