I started a 2D Boardgame project and my partner decided to go with state pattern for the entire project so basically we sat down and thought about the states we need and we implemented like each scene in Unity has some states a state manager and state factory we are also using zenject. the problem is that i don't really like this approach because of the amount of code for a single scene and its kind of limited for example if i needed to add a new gamemode that needs more state or less whatever... i simply cant use this system and if the 2 modes are alike somehow the codes inside each state becomes huge so many nested if's for scene navigation or applying different logic for different modes. and i'm on a hunt for a good pattern to tackle this issue but i'm also afraid of over engineering. So i came here for help. I also thought about an event system for passing data between classes on the same scene and talking to each other via events but i don't know if it's good to for the enitre project or how i should handle scene navigation properly. so if events are the way to go tell me how can i use them.
Exmaple
so the game im working on is yahtzee and here are couple of the states some have controllers but others have the logic and states together and i know its a bad habit but i didnt have time :)
public class IdleState : IStateEntity
{
[Inject] private InGameHandler _inGameHandler;
public void Initialize()
{
LogController.Log("+Initialize State: InGame.Idle");
if (_inGameHandler.GameModel.gameType != GameType.TutorialGame)
{
_inGameHandler.RollDiceButtonActive.SetValueAndForceNotify(true);
}
}
public void Dispose()
{
LogController.Log("-Dispose State: InGame.Idle");
}
public class Factory : PlaceholderFactory<IdleState>
{
}
}
and IStateEntity
public interface IStateEntity
{
void Initialize();
void Dispose();
}
so as you can see thats how my states look like and here is a state manager
public class InGameStateManager : IInitializable
{
[Inject] private GameStateManager _gameStateManager;
public InGameStateEntity _currentState = InGameStateEntity.WaitForStart;
public InGameStateEntity _previousState;
private readonly InGameStateFactory _inGameStateFactory;
private IStateEntity _inGameStateEntity;
public InGameStateManager(InGameStateFactory inGameStateFactory)
{
_inGameStateFactory = inGameStateFactory;
}
public void Initialize()
{
if(_gameStateManager.CurrentState.Value !=GameStateEntity.Tutorial )
ChangeState(InGameStateEntity.Intro);
}
public void ChangeState(InGameStateEntity stateEntity)
{
//if we're in already in stateEntity dont change it
if (stateEntity == _currentState)
{
LogController.Log("Already in State: " + _currentState);
return;
}
//if we're in some state and this not the first time we're entering this phase then dispose current state and make it null
if (_inGameStateEntity != null)
{
_inGameStateEntity.Dispose();
_inGameStateEntity = null;
}
_previousState = _currentState;
_currentState = stateEntity;
//initializing stateEntity and assigning it to _InGameStateEntity which represents the current state.
Debug.Log("from state " + _previousState + " to " + stateEntity);
_inGameStateEntity = _inGameStateFactory.CreateState(stateEntity);
}
}
and InGameStateEntity is an enum which is all the states for my gameplay
public enum InGameStateEntity
{
WaitForStart,
Intro,
OpponentMove,
Idle,
RollDice,
BonusRoll,
Yahtzee,
PrimarySubmission,
CheckItem,
FinalSubmission,
CheckSectionBonus,
SaveProgress,
GameList,
Outro,
GetMoreDice,
ResetTurn
}
I didn't want to send it but whatever i really need help i'm kind of a newbie so don't judge me this is my first real project and the code for intro is so messed up lemme show you...
public class IntroState : IStateEntity
{
[Inject] private InGameIntroCanvasController.Factory _introCanvasControllerFactory;
[Inject] private InGameStateManager _inGameStateManager;
[Inject] private InGameHandler _inGameHandler;
[Inject] private DicePlaceHolderManager _dicePlaceHolderManager;
private InGameIntroCanvasController _controller;
[Inject] private PlayerPrefsManager _playerPrefsManager;
[Inject] private DiceRollController _diceRollController;
[Inject] private GlobalVariables _globalVariables;
[Inject] private SFXManager _sfxManager;
[Inject] private InGameSprites _inGameSprites;
public void Initialize()
{
LogController.Log("+Initialize State: InGame.Intro");
_dicePlaceHolderManager.Initialize();
if (_inGameHandler.IsDiceWorld.Value)
{
if (!_inGameHandler.GameModel.hasIntroShown)
{
ShowIntro();
Observable.FromCoroutine(() => IntroCountDown(3.5f)).Subscribe();
}
else
DiceWorldIntroWithoutEnumerator();
return;
}
if (_inGameHandler.GameModel.gameType == GameType.TutorialGame)
{
if (!_inGameHandler.GameModel.hasIntroShown)
{
ShowIntro();
Observable.FromCoroutine(() => IntroCountDown(3.5f)).Subscribe();
}
else
DiceWorldIntroWithoutEnumerator();
return;
}
if (!_inGameHandler.GameModel.hasIntroShown && _inGameHandler.IsPlayerTurn.Value)
{
ShowIntro();
Observable.FromCoroutine(() => IntroCountDown(4)).Subscribe();
}
else
IntroWithoutEnumerator();
}
private void ShowIntro()
{
_inGameHandler.GameModel.hasIntroShown = true;
_playerPrefsManager.GameModelData.GameModel = _inGameHandler.GameModel;
_inGameHandler.RollDiceButtonActive.SetValueAndForceNotify(false);
_controller = _introCanvasControllerFactory.Create();
string nickname = _globalVariables.UserProfile.Nickname;
int level = _globalVariables.UserProfile.Level;
_controller.playerProfile.nickName.text = nickname;
_controller.playerProfile.level.text = level.ToString();
if (_inGameHandler.IsDiceWorld.Value)
{
SetBossAvatar(_globalVariables.CurrentRegion);
}
else if (_inGameHandler.GameModel.gameType == GameType.TutorialGame)
{
SetPlayerAvatar(_inGameSprites.DogAvatar);
SetOpponentAvatar(_inGameSprites.CatAvatar);
}
nickname = _inGameHandler.GameModel.opponent.nickname;
level = _inGameHandler.GameModel.opponent.level;
_controller.opponentProfile.nickName.text = nickname == null || nickname.Equals("") ? "در حال جستجو" : nickname;
_controller.opponentProfile.level.text = (level == 0 ? 1 : level).ToString();
_controller.playerProfile.SelectedDice
.DoIntroMoveIn()
.Join(_controller.opponentProfile.SelectedDice
.DoIntroMoveIn())
.OnStart(() =>
{
Observable.FromCoroutine(() => _controller.PlayParticle(.8f)).Subscribe();
_controller.PrizePlaceholderUp(.5f);
_sfxManager.PlayInGameIntro(.5f);
});
// _controller.playerProfile.SelectedDice.ProfileMoveIn()
// .Join(_controller.opponentProfile.ProfileMoveIn())
// .OnStart(() =>
// {
//
// });
}
private void SetBossAvatar(RegionData region)
{
if(region.boss.avatar == null)
return;
_controller.opponentProfile.avatar.sprite = region.boss.avatar;
}
private void SetOpponentAvatar(Sprite sprite)
{
_controller.opponentProfile.avatar.sprite = sprite;
}
private void SetPlayerAvatar(Sprite sprite)
{
_controller.playerProfile.avatar.sprite = sprite;
}
void DiceWorldIntroWithoutEnumerator()
{
_diceRollController.ShowLastRolledDice();
_inGameStateManager.ChangeState(_inGameHandler.PlayerTurn() == false
? InGameStateEntity.OpponentMove
: InGameStateEntity.Idle);
}
void IntroWithoutEnumerator()
{
if (_inGameHandler.IsPlayerTurn.Value)
{
if (_inGameHandler.GameModel.lastUpdatedCell != null && !_inGameHandler.GameModel.hasOpponentMoveShown)
_inGameStateManager.ChangeState(InGameStateEntity.OpponentMove);
else
{
_diceRollController.ShowLastRolledDice();
_inGameStateManager.ChangeState(_inGameHandler.GameModel.isWaiting
? InGameStateEntity.GameList
: InGameStateEntity.Idle);
}
}
else
{
if (!_inGameHandler.HasOpponentMoved())
{
_inGameHandler.RollDiceButtonActive.SetValueAndForceNotify(false);
_inGameHandler.PlayButtonActive.SetValueAndForceNotify(false);
// _inGameHandler.OpponentAvailablePlaceHolders[_inGameHandler.GameModelData.lastUpdatedCell.cellId].opponentPlaceHolder.GetComponentInChildren<RTLTextMeshPro>().text = _inGameHandler.GameModelData.lastUpdatedCell.score + "";
_inGameStateManager.ChangeState(InGameStateEntity.GameList);
}
else if(_inGameHandler.GameModel.hasOpponentMoveShown)
{
_inGameHandler.RollDiceButtonActive.SetValueAndForceNotify(false);
_inGameHandler.PlayButtonActive.SetValueAndForceNotify(false);
_inGameStateManager.ChangeState(InGameStateEntity.OpponentMove);
}
}
}
IEnumerator IntroCountDown(float delay)
{
yield return new WaitForSeconds(delay);
_diceRollController.ShowLastRolledDice();
if (_inGameHandler.IsDiceWorld.Value)
{
_inGameStateManager.ChangeState(_inGameHandler.IsPlayerTurn.Value
? InGameStateEntity.Idle
: InGameStateEntity.OpponentMove);
}
else if (_inGameHandler.IsPlayerTurn.Value)
{
_inGameStateManager.ChangeState(_inGameHandler.GameModel.lastUpdatedCell != null
? InGameStateEntity.OpponentMove
: InGameStateEntity.Idle);
}
else
{
if (!_inGameHandler.HasOpponentMoved())
{
_inGameHandler.RollDiceButtonActive.SetValueAndForceNotify(false);
_inGameHandler.PlayButtonActive.SetValueAndForceNotify(false);
_inGameHandler.OpponentAvailablePlaceHolders[_inGameHandler.GameModel.lastUpdatedCell.cellId].opponentPlaceHolder.GetComponentInChildren<RTLTextMeshPro>().text = _inGameHandler.GameModel.lastUpdatedCell.score + "";
_inGameStateManager.ChangeState(InGameStateEntity.GameList);
}
else
{
_inGameHandler.RollDiceButtonActive.SetValueAndForceNotify(false);
_inGameHandler.PlayButtonActive.SetValueAndForceNotify(false);
_inGameStateManager.ChangeState(InGameStateEntity.OpponentMove);
}
}
}
public void Dispose()
{
if(_controller != null)
_controller.Destroy();
LogController.Log("-Dispose State: InGame.Intro");
}
public class Factory : PlaceholderFactory<IntroState>
{
}
}
I know that part of it is my fault for not separating the controllers from states but i really dont like states or if this is ok what is the best approach for making it reusable. just think about adding new intro i need to add bunch of if statements if based on the game type
BTW InGameHandler is a gameobject in the ingame scene and all the data i need for the game is inside that kinda silly and if you know a better way lemme know and dont be hard on me ok ? :))))
InGameStateFactory:
public class InGameStateFactory
{
[Inject] private IntroState.Factory _introStateFactory;
[Inject] private OpponentMoveState.Factory _opponentMoveStateFactory;
[Inject] private IdleState.Factory _idleStateFactory;
[Inject] private RollDiceState.Factory _rollDiceStateFactory;
[Inject] private BonusRollState.Factory _bonusRollStateFactory;
[Inject] private DicerState.Factory _yahtzeeStateFactory;
[Inject] private PrimarySubmissionState.Factory _primarySubmissionStateFactory;
[Inject] private CheckItemState.Factory _checkItemStateFactory;
[Inject] private FinalSubmissionState.Factory _finalSubmissionStateFactory;
[Inject] private CheckSectionBonusState.Factory _checkSectionBonusStateFactory;
[Inject] private SaveProgressState.Factory _saveProgressStateFactory;
[Inject] private GameListState.Factory _gameListStateFactory;
[Inject] private OutroState.Factory _outroStateFactory;
[Inject] private ResetTurnState.Factory _resetTurnFactory;
[Inject] private GetMoreDiceState.Factory _getMoreDiceFactory;
public IStateEntity CreateState(InGameStateEntity inGameStateEntity)
{
IStateEntity stateEntity = null;
switch (inGameStateEntity)
{
case InGameStateEntity.Intro:
stateEntity = _introStateFactory.Create();
break;
case InGameStateEntity.OpponentMove:
stateEntity = _opponentMoveStateFactory.Create();
break;
case InGameStateEntity.Idle:
stateEntity = _idleStateFactory.Create();
break;
case InGameStateEntity.RollDice:
stateEntity = _rollDiceStateFactory.Create();
break;
case InGameStateEntity.BonusRoll:
stateEntity = _bonusRollStateFactory.Create();
break;
case InGameStateEntity.Dicer:
stateEntity = _yahtzeeStateFactory.Create();
break;
case InGameStateEntity.PrimarySubmission:
stateEntity = _primarySubmissionStateFactory.Create();
break;
case InGameStateEntity.CheckItem:
stateEntity = _checkItemStateFactory.Create();
break;
case InGameStateEntity.FinalSubmission:
stateEntity = _finalSubmissionStateFactory.Create();
break;
case InGameStateEntity.CheckSectionBonus:
stateEntity = _checkSectionBonusStateFactory.Create();
break;
case InGameStateEntity.SaveProgress:
stateEntity = _saveProgressStateFactory.Create();
break;
case InGameStateEntity.GameList:
stateEntity = _gameListStateFactory.Create();
break;
case InGameStateEntity.Outro:
stateEntity = _outroStateFactory.Create();
break;
case InGameStateEntity.ResetTurn:
stateEntity = _resetTurnFactory.Create();
break;
case InGameStateEntity.GetMoreDice:
stateEntity = _getMoreDiceFactory.Create();
break;
default:
throw new ArgumentOutOfRangeException(nameof(inGameStateEntity), inGameStateEntity, null);
}
if (stateEntity == null)
throw Assert.CreateException("InGameState Not Found And This Code Should Not Be Reached There :/ !");
stateEntity.Initialize();
return stateEntity;
}
PS: i'm using zenject , unirx and the game is connected to a server.