Reading Assets from folders was only possible in Editor over LoadAssetFromPath. Now I'm using asset bundles for ScriptedObjects and Audio Prefabs, when the project is built. When in Editor the folders are still used for convenience (can make changes to assets without rebuilding asset bundles)
485 lines
14 KiB
C#
485 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using log4net;
|
|
using PrimeTween;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using GameLogic;
|
|
using System.Threading.Tasks;
|
|
using System.Text.RegularExpressions;
|
|
using Unity.VisualScripting;
|
|
|
|
/// <summary>
|
|
/// States the match can be in.
|
|
/// </summary>
|
|
public enum MatchState
|
|
{
|
|
CharacterSelect,
|
|
Pause,
|
|
Starting,
|
|
Match,
|
|
End
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update data relevant to this matches rules.
|
|
/// </summary>
|
|
public class MatchConditionUpdate
|
|
{
|
|
public WinCondition Condition { get; set; }
|
|
public Ship Ship { get; set; }
|
|
public int Count { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The properties a player is assigned at the start
|
|
/// of the match, according to the rules.
|
|
/// </summary>
|
|
public class MatchPlayerStatistic
|
|
{
|
|
public bool IsOut { get; set; }
|
|
public int RoundsWon { get; set; }
|
|
public int Lives { get; set; }
|
|
public int Score { get; set; }
|
|
public int Time { get; set; }
|
|
}
|
|
|
|
namespace Managers
|
|
{
|
|
/// <summary>
|
|
/// Manages the initialization and progression of matches.
|
|
/// </summary>
|
|
public class MatchManager : MonoBehaviour
|
|
{
|
|
private static ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
|
|
|
private static readonly string arenaAssetsBundleName = "arenas";
|
|
private static readonly string ruleAssetsBundleName = "rules";
|
|
private static readonly string arenaAssetsPath = "Assets/ScriptedAssets/Arenas";
|
|
private static readonly string ruleAssetsPath = "Assets/ScriptedAssets/Rules";
|
|
private GameResult CurrentMatchResult;
|
|
|
|
public GameObject MatchArena;
|
|
public GameObject MatchCamera;
|
|
|
|
public event Action OnStartPressed;
|
|
/// <summary>
|
|
/// Globally accessible member to use manager with.
|
|
/// </summary>
|
|
public static MatchManager G { get; private set; }
|
|
/// <summary>
|
|
/// The current state of the match.
|
|
/// </summary>
|
|
public MatchState matchState = MatchState.CharacterSelect;
|
|
/// <summary>
|
|
/// The statistics regarding the current match mapped to the players.
|
|
/// </summary>
|
|
public Dictionary<Player, MatchPlayerStatistic> MatchPlayerStatistics { get; set; } =
|
|
new Dictionary<Player, MatchPlayerStatistic>();
|
|
/// <summary>
|
|
/// Bundle of properties regarding the arena.
|
|
/// </summary>
|
|
public Arena ArenaProperties { get; private set; }
|
|
/// <summary>
|
|
/// The rules under which this match takes place.
|
|
/// </summary>
|
|
public MatchRule MatchRule { get; private set; }
|
|
/// <summary>
|
|
/// List of all the arenas in the assets.
|
|
/// </summary>
|
|
public List<Arena> AvailableArenas { get; private set; } = new List<Arena>();
|
|
/// <summary>
|
|
/// List of all the rules/game modes in the assets.
|
|
/// </summary>
|
|
public List<MatchRule> AvailableRules { get; private set; } = new List<MatchRule>();
|
|
|
|
|
|
void Awake()
|
|
{
|
|
G = this;
|
|
Log.Info("Awake");
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
LoadAvailableRules();
|
|
LoadAvailableArenas();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the different rules/game modes which are available,
|
|
/// at the fixed asset path.
|
|
/// </summary>
|
|
private void LoadAvailableRules()
|
|
{
|
|
#if UNITY_EDITOR
|
|
string[] files = Directory.GetFiles(arenaAssetsPath, "*.asset",
|
|
SearchOption.TopDirectoryOnly);
|
|
foreach (var file in files)
|
|
{
|
|
AvailableArenas.Add(AssetDatabase.LoadAssetAtPath<Arena>(file));
|
|
}
|
|
#else
|
|
var ruleAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, ruleAssetsBundleName));
|
|
if (ruleAssetBundle == null)
|
|
{
|
|
Log.Error("Failed to load rules asset bundle!");
|
|
return;
|
|
}
|
|
AvailableRules.AddRange(ruleAssetBundle.LoadAllAssets<MatchRule>());
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the characters a player can choose from a fixed path
|
|
/// in the project.
|
|
/// </summary>
|
|
private void LoadAvailableArenas()
|
|
{
|
|
#if UNITY_EDITOR
|
|
string[] files = Directory.GetFiles(ruleAssetsPath, "*.asset",
|
|
SearchOption.TopDirectoryOnly);
|
|
foreach (var file in files)
|
|
{
|
|
AvailableRules.Add(AssetDatabase.LoadAssetAtPath<MatchRule>(file));
|
|
}
|
|
#else
|
|
var arenasAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, arenaAssetsBundleName));
|
|
if (arenasAssetBundle == null)
|
|
{
|
|
Log.Error("Failed to load arenas asset bundle!");
|
|
return;
|
|
}
|
|
AvailableArenas.AddRange(arenasAssetBundle.LoadAllAssets<Arena>());
|
|
#endif
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Update and check the match conditions in regards to the rules.
|
|
/// </summary>
|
|
/// <param name="update">A change in the matches progression.</param>
|
|
public void UpdateMatchCondition(MatchConditionUpdate update)
|
|
{
|
|
Player updatedPlayer = null;
|
|
foreach (Player p in PlayerManager.G.MatchPlayers)
|
|
{
|
|
if (p.character.shipName == update.Ship.props.shipName)
|
|
{
|
|
updatedPlayer = p;
|
|
Log.Debug($"Players: {p.name} match statistic will be updated.");
|
|
}
|
|
}
|
|
if (updatedPlayer == null)
|
|
{
|
|
Log.Error($"Ship: {update.Ship.props.shipName} does not belong to a player in this match."
|
|
+ " Can't update match.");
|
|
return;
|
|
}
|
|
if (CurrentMatchResult == null)
|
|
{
|
|
Log.Error("Match has no result statistics attached and was set up incorrectly!");
|
|
return;
|
|
}
|
|
MatchLogic.UpdateMatchResult(updatedPlayer, update, MatchPlayerStatistics, CurrentMatchResult);
|
|
if (CurrentMatchResult.IsMatchWon)
|
|
{
|
|
Log.Info("Match has ended, winner will be declared.");
|
|
AnnounceMatchWinner(CurrentMatchResult);
|
|
}
|
|
else
|
|
{
|
|
Log.Info($"Round {CurrentMatchResult.RoundsPlayed} of {MatchRule.rounds} has ended." +
|
|
$"{CurrentMatchResult.Winner?.name} won this round.");
|
|
AnnounceRoundWinner(CurrentMatchResult);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Creates statistics for the players participating in the match.
|
|
/// </summary>
|
|
public void SetupMatchPlayerStatistics()
|
|
{
|
|
MatchPlayerStatistics.Clear();
|
|
foreach (Player p in MatchPlayers)
|
|
{
|
|
MatchPlayerStatistic mps = new MatchPlayerStatistic
|
|
{
|
|
IsOut = false,
|
|
Lives = MatchRule.lives,
|
|
Score = MatchRule.score,
|
|
Time = MatchRule.time
|
|
};
|
|
if (!MatchPlayerStatistics.TryAdd(p, mps))
|
|
{
|
|
Log.Info($"Player {p.name} already has statistics set up.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Just assigns new arena properties for now.
|
|
/// TODO: Should initiate all the necessary steps upon switching the arena.
|
|
/// </summary>
|
|
/// <param name="arenaIndex">Index of the arena (arenas scripted asset folder order from 0)</param>
|
|
public void LoadArenaProperties(int arenaIndex)
|
|
{
|
|
if (AvailableArenas.Count - 1 < arenaIndex)
|
|
{
|
|
Log.Error($"There are only: {AvailableArenas.Count} arenas loaded."
|
|
+ $" Couldn't load arena number: {arenaIndex + 1}");
|
|
return;
|
|
}
|
|
ArenaProperties = AvailableArenas[arenaIndex];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns rules to the match.
|
|
/// </summary>
|
|
/// <param name="ruleIndex">Index of the rule to be loaded
|
|
/// (rules scripted asset folder order from 0)</param>
|
|
public void LoadMatchRules(int ruleIndex)
|
|
{
|
|
if (AvailableArenas.Count - 1 < ruleIndex)
|
|
{
|
|
Log.Error($"There are only: {AvailableArenas.Count} rules loaded."
|
|
+ $" Couldn't load match rule number: {ruleIndex + 1}");
|
|
return;
|
|
}
|
|
MatchRule = AvailableRules[ruleIndex];
|
|
MatchLogic.currentRule = MatchRule;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Just sets the match state for now.
|
|
/// TODO: maybe use in character selection
|
|
/// </summary>
|
|
public void StartCharacterSelect()
|
|
{
|
|
matchState = MatchState.CharacterSelect;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Announcement of who won the match.
|
|
/// </summary>
|
|
/// <param name="mr">Result data of the completed match.</param>
|
|
async public void AnnounceMatchWinner(GameResult mr)
|
|
{
|
|
UIManager.G.Announcments.QueueAnnounceText($"{mr.Winner.playerName}" +
|
|
" has won the match!", 2f);
|
|
await Tween.Delay(2f * 0.33f);
|
|
matchState = MatchState.End;
|
|
await Tween.Delay(2f * 0.66f);
|
|
|
|
UIManager.G.ShowMatchEndMenu(MatchArena.transform);
|
|
|
|
}
|
|
|
|
async public void StartRematch()
|
|
{
|
|
ResetMatch();
|
|
|
|
UIManager.G.Announcments.QueueAnnounceText("Starting rematch.", 0.3f);
|
|
UIManager.G.Announcments.QueueAnnounceText("Starting rematch..", 0.3f);
|
|
UIManager.G.Announcments.QueueAnnounceText("Starting rematch...", 0.3f);
|
|
UIManager.G.Announcments.QueueAnnounceText("GO!", 0.5f);
|
|
await Tween.Delay(0.9f);
|
|
matchState = MatchState.Match;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Announcement of who won the Round.
|
|
/// </summary>
|
|
/// <param name="mr">Result data of the completed match.</param>
|
|
async public void AnnounceRoundWinner(GameResult mr)
|
|
{
|
|
var winnerStats = MatchPlayerStatistics[mr.Winner];
|
|
UIManager.G.Announcments.QueueAnnounceText($"{mr.Winner.playerName}" +
|
|
" has won the Round! \n" +
|
|
$"They won {winnerStats.RoundsWon} out of {MatchRule.rounds}.",
|
|
1.618f);
|
|
|
|
await Tween.Delay(1.618f * 0.33f);
|
|
matchState = MatchState.Pause;
|
|
await Tween.Delay(1.618f * 0.66f);
|
|
|
|
ResetMatchCharacters();
|
|
|
|
UIManager.G.Announcments.QueueAnnounceText("Starting next round.", 0.3f);
|
|
UIManager.G.Announcments.QueueAnnounceText("Starting next round..", 0.3f);
|
|
UIManager.G.Announcments.QueueAnnounceText("Starting next round...", 0.3f);
|
|
UIManager.G.Announcments.QueueAnnounceText("GO!", 0.5f);
|
|
await Tween.Delay(0.9f);
|
|
|
|
matchState = MatchState.Match;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets player positions and statistics.
|
|
/// </summary>
|
|
public void ResetMatch()
|
|
{
|
|
CurrentMatchResult = new GameResult();
|
|
ResetMatchCharacters();
|
|
SetupMatchPlayerStatistics();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the match, waits for match begin
|
|
/// confirmation by the players and counts down to start.
|
|
/// </summary>
|
|
async public void StartMatch()
|
|
{
|
|
foreach (Player p in MatchPlayers)
|
|
{
|
|
p.spawnedCharacter.transform.localPosition =
|
|
ArenaProperties.spawnPositions[p.playerNumber - 1];
|
|
}
|
|
SetupMatchPlayerStatistics();
|
|
CurrentMatchResult = new GameResult();
|
|
|
|
matchState = MatchState.Starting;
|
|
UIManager.G.ShowMatchStartPrompt();
|
|
|
|
// Wait until someone pressed start
|
|
var startPressed = new TaskCompletionSource<bool>();
|
|
OnStartPressed += () => ConfirmMatchStart(startPressed);
|
|
// TODO: This could break in WebGL due to threading
|
|
await startPressed.Task;
|
|
OnStartPressed = null;
|
|
|
|
UIManager.G.HideAnnouncement();
|
|
UIManager.G.Announcments.QueueAnnounceText("3", 1);
|
|
UIManager.G.Announcments.QueueAnnounceText("2", 1);
|
|
UIManager.G.Announcments.QueueAnnounceText("1", 1);
|
|
UIManager.G.Announcments.QueueAnnounceText("GO!", 0.5f);
|
|
await Tween.Delay(3);
|
|
|
|
matchState = MatchState.Match;
|
|
}
|
|
|
|
public void StartTestMatch()
|
|
{
|
|
foreach (Player p in MatchPlayers)
|
|
{
|
|
p.spawnedCharacter.transform.localPosition =
|
|
ArenaProperties.spawnPositions[p.playerNumber - 1];
|
|
}
|
|
SetupMatchPlayerStatistics();
|
|
CurrentMatchResult = new GameResult();
|
|
matchState = MatchState.Match;
|
|
}
|
|
|
|
public void ConfirmMatchStart(TaskCompletionSource<bool> startPressed)
|
|
{
|
|
startPressed.TrySetResult(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is called by the GameplayMetaInputEvents object, when someone pressed start.
|
|
/// </summary>
|
|
public void StartPressed()
|
|
{
|
|
if (matchState != MatchState.Match)
|
|
{
|
|
OnStartPressed?.Invoke();
|
|
return;
|
|
}
|
|
}
|
|
|
|
public void PausePressed()
|
|
{
|
|
// TODO: MetaInputEvent in InGameUI scene -> PausePressed here -> ShowPauseMenu in UIManager -> PauseMenu Show
|
|
// When clicking continue in PauseMenu -> PausePressed here -> UIManager HidePause -> PauseMenu Hide
|
|
if (matchState == MatchState.Pause)
|
|
{
|
|
ContinueMatch();
|
|
UIManager.G.HidePauseMenu();
|
|
MatchCamera.SetActive(true);
|
|
return;
|
|
}
|
|
if (matchState != MatchState.Match)
|
|
{
|
|
Log.Info("Can only pause when match is in progress.");
|
|
return;
|
|
}
|
|
matchState = MatchState.Pause;
|
|
UIManager.G.ShowPauseMenu(MatchArena.transform);
|
|
MatchCamera.SetActive(false);
|
|
}
|
|
|
|
async private void ContinueMatch()
|
|
{
|
|
UIManager.G.Announcments.QueueAnnounceText("Get Ready.", 0.3f);
|
|
UIManager.G.Announcments.QueueAnnounceText("Get Ready..", 0.3f);
|
|
UIManager.G.Announcments.QueueAnnounceText("Get Ready...", 0.3f);
|
|
UIManager.G.Announcments.QueueAnnounceText("GO!", 0.5f);
|
|
await Tween.Delay(0.9f);
|
|
matchState = MatchState.Match;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns the characters which were chosen by the players
|
|
/// and sets them up to be playable.
|
|
/// </summary>
|
|
/// <param name="players">List of players for this match</param>
|
|
public void SpawnCharacters(List<Player> players)
|
|
{
|
|
// TODO: This belongs in another intialization method
|
|
MatchArena = GameObject.Find("Arena Placement");
|
|
MatchCamera = GameObject.Find("Match Camera");
|
|
if (MatchArena == null)
|
|
{
|
|
Log.Error("ArenaPlacement not found in scenes. Cannot spawn players.");
|
|
return;
|
|
}
|
|
if (MatchCamera == null)
|
|
{
|
|
Log.Error("Match camera not found in scenes. Cannot initialize spawned Characters.");
|
|
return;
|
|
}
|
|
|
|
foreach (Player p in players)
|
|
{
|
|
GameObject shipObject = Instantiate(p.character.shipObject);
|
|
p.spawnedCharacter = shipObject;
|
|
shipObject.TryGetComponent(out Ship ship);
|
|
ship.state = new ShipHandling.ShipState();
|
|
ship.cameraOperator = MatchCamera.GetComponent<CameraOperator>();
|
|
shipObject.transform.SetParent(MatchArena.transform, false);
|
|
shipObject.transform.localPosition =
|
|
ArenaProperties.spawnPositions[p.playerNumber - 1];
|
|
|
|
shipObject.transform.localScale = new Vector3();
|
|
Tween.Scale(shipObject.transform, new Vector3(0.7f, 0.7f, 0.7f), 1f);
|
|
MatchPlayers.Add(p);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the position, rotation and state of the spawned characters.
|
|
/// </summary>
|
|
public void ResetMatchCharacters()
|
|
{
|
|
foreach (Player mp in MatchPlayers)
|
|
{
|
|
GameObject shipGO = mp.spawnedCharacter;
|
|
shipGO.TryGetComponent(out Ship ship);
|
|
shipGO.TryGetComponent(out Rigidbody body);
|
|
|
|
ship.state.boostCapacity = ship.props.maxBoostCapacity;
|
|
|
|
body.velocity = Vector3.zero;
|
|
|
|
shipGO.transform.localPosition =
|
|
ArenaProperties.spawnPositions[mp.playerNumber - 1];
|
|
shipGO.transform.rotation = shipGO.transform.parent.rotation;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|