I think you may be getting a little bogged down in the English meaning of state, when compared to the State Pattern (or Finite-State Machine, which is really a diagrammatic representation of a State Pattern). Both are appropriate here, but they shouldn't be confused.
The State Pattern is something which should, given various common stimuli, operate on the game state differently at different times in the game. So, as you've rightly concluded, the lobby stage (which I'm assuming to be while people are joining a game) is a State in that context.
The game state is a list of players, pieces, cards, dice, whatever else, and all the information required to simulate them. You might want to call that the context.
Each State should receive a UserClicked message from the application which receives the context and coordinates or area clicked and should operate on those accordingly. The information that you are trying to give ownership of to the State object does not belong there, it belongs in the game itself, alongside the State.
The state may also have a UserPressedKey method, a TimerTicked method or any other kind of stimulus on which it should act. But each of these should act on the game state rather than being the game state.
One important method of the State will be ScreenRefresh, which will draw the context for the user.
Here is a very rough example:
abstract class ApplicationState {
void Begin(GameContext context);
void UserClicked(GameContext context, int x, int y);
void UserPressedKey(GameContext context, char key);
void TimerTicked(GameContext context);
void ScreenRefresh(GameContext context);
protected void OnStateChanged(ApplicationState newState) {
// inform interested parties that state has changed,
// using the observer pattern
}
}
class LobbyState : ApplicationState
{
void Begin(GameContext context) {
context.Players = new User[context.NoOfPlayers];
}
void UserClicked(GameContext context, int x, int y) {
// Find a displayed player box which contains click coords
int index = -1;
for (i=0; i < context.Players.Length; i++) {
if (PlayerBox[i].Contains(x, y)) {
index = i;
}
}
if (index > -1) {
if (context.Player[index] == null) {
context.Player[index] = context.ActivePlayer;
} else if (context.Player[index] == context.ActivePlayer()) {
context.Player[index] = null;
} else {
ErrorSound.Play();
}
} else if (CloseIcon.Contains(x, y)) {
OnStateChanged(new ExittingState());
} else {
ErrorSound.Play();
}
}
void UserPressedKey(GameContext context, char key) {
if (key = 'J' or key = 'j') {
if (! context.AddPlayerRandomly(context.ActiveUser)) {
ErrorSound.Play();
}
if (context.GameIsNowFull()) {
OnStateChanged(new InitializingState());
}
} else if key = Esc {
if (!context.RemovePlayer(context.ActiveUser)) {
ErrorSound.Play();
}
} else {
ErrorSound.Play();
}
}
void TimerTicked(GameContext context) {
// there is no use for a game timer while
// we're trying to fill the game, but you
// want one to tick anyway, because the
// application doesn't know which state
// it is in.
}
void ScreenRefresh(GameContext context) {
DrawPlayerBoxes(context.Screen, context.Players);
DrawExitIcon(context.Screen);
}
}
class InitializingState : ApplicationState {
private double waitSpinnerAngle = 0;
void Begin(GameContext context) {
context.RandomizePlayOrder();
context.ShuffleCards();
// all other game initialization rules here
OnContextChanged(new PlayerUpState(context.Players[0]));
}
void UserClicked(GameContext context, int x, int y) {
// Not responding to user input for a moment
ErrorSound.Play();
}
void UserPressedKey(GameContext context, char key) {
// Not responding to user input for a moment
ErrorSound.Play();
}
void TimerTicked(GameContext context) {
waitSpinnerAngle += 0.05;
if (waitSpinnerAngle > 1) waitSpinnerAngle = 0;
}
void ScreenRefresh(GameContext context) {
DrawWaitSpinner(waitSpinnerAngle);
}
}
See where I'm going with this? The Application itself is then as simple as setting the initial State, hooking a listener into the OnStateChanged event and calling Begin. When the listener hears that the event is called, unhook your old State, hook in the new one, and call Begin again.
Everything else is triggered by events from the mouse, keyboard or timer, passed directly to the current State without knowledge of which State the game is currently in. If you are running this game across a network then you will also need an event for changes of state received from other players.
Everything in your State and Context is now very unit-testable and separation of concerns are observed. Although, you may want to refactor that LobbyState#UserClicked method a bit, among other things.
Remember, this is just a free example and you get what you paid for it. Don't try to apply it directly to your game. Just use it to understand how the State Pattern should work in the context of a game.
Such a language extension would be significantly more complicated and invasive than you seem to think. You can't just add
if the life-time of a variable of a stack-bound type ends, call Dispose
on the object it refers to
to the relevant section of the language spec and be done. I'll ignore the problem of temporary values (new Resource().doSomething()
) which can be solved by slightly more general wording, this is not the most serious issue. For example, this code would be broken (and this sort of thing probably becomes impossible to do in general):
File openSavegame(string id) {
string path = ... id ...;
File f = new File(path);
// do something, perhaps logging
return f;
} // f goes out of scope, caller receives a closed file
Now you need user-defined copy constructors (or move constructors) and start invoking them everywhere. Not only does this carry performance implications, it also makes these things effectively value types, whereas almost all other objects are reference types. In Java's case, this is a radical deviation from how objects work. In C# less so (already has struct
s, but no user-defined copy constructors for them AFAIK), but it still makes these RAII objects more special. Alternatively, a limited version of linear types (cf. Rust) may also solve the problem, at the cost of prohibiting aliasing including parameter passing (unless you want to introduce even more complexity by adopting Rust-like borrowed references and a borrow checker).
It can be done technically, but you end up with a category of things which are very different from everything else in the language. This is almost always a bad idea, with consequences for implementers (more edge cases, more time/cost in every department) and users (more concepts to learn, more possibility of bugs). It's not worth the added convenience.
Best Answer
One approach which can be helpful is to expose wrapper objects to the outside world, but don't hold any strong references to them internally. Either have the internal objects keep weak references to the wrappers, or else have each wrapper hold the only reference anywhere in the world to a finalizable object which in turn holds a reference to the "real" object [I don't like having any outside-world-facing objects implement
Finalize
, since objects have no control over who can call things likeGC.SuppressFinalize()
on them]. When all outside-world references to the wrapper object have disappeared, any weak references to the wrapper object will be invalidated [and can be recognized as such], and any finalizable object to which the wrapper held the only reference will run itsFinalize
method.For event handlers which are guaranteed to be triggered on some period basis (e.g. timer ticks) I like the
WeakReference
approach. There's no need to use a finalizer to ensure the timer gets cleaned up; instead, the timer-tick event can notice that the outside-world link has been abandoned and clean itself up. Such an approach may also be workable for things whose only resources are subscriptions to rarely-fired events from long-lived (or static) objects, if those objects have aCheckSubscription
event which fires periodically when a subscriber is added (the rate at which those events fire should depend upon the number of subscriptions). Event subscriptions pose a real problem is when an unbounded number of objects may subscribe to an event from a single instance of a long-lived object and be abandoned without unsubscribing; in that case, the number of abandoned-but-uncollectable subscriptions may grow without bound. If 100 instances of a class subscribe to an event from a long-lived object and are subsequently abandoned, and nothing else ever subscribes to that event, memory used by those subscriptions may never get cleaned up, but the quantity of wasted memory would be limited to those 100 subscriptions. Adding more subscriptions would at some point cause theCheckSubscription
event to fire, which would in turn cause all the abandoned objects' subscriptions to get canceled.PS--Another advantage of using weak references is that one can request that a
WeakReference
remain valid until the object to which it refers is 100% absolutely and irretrievably gone. It's possible for an object to appear abandoned and have itsFinalize
method scheduled, but then get resurrected and returned to active use even before itsFinalize
method actually executes; this can happen completely outside the object's control. If code holds a long weak reference to the public wrapper object, and if it makes sure the reference itself remains rooted, it can hold off on performing any cleanup until resurrection becomes impossible.