I'm going to try to explain my idea but fundamentally it all comes down to using the state pattern. The state pattern lets you change the behavior of an object. Let me show you an example in Java.
interface MouseMouseControl really doesn't do much and just passes along the information to the Mouse object. The Mouse object overrides clickCard() and does the really "work." To select a target for Giant Growth you would override Mouse.clickCard() to only allow the player to target a creature. The pseudo-code would look like this.
{
void clickCard(Card c, Zone z);
}
class MouseControl
{
private Mouse m;
public void setMouse (Mouse mouse)
{
m = mouse;
}
public void clickCard(Card c, Zone z)
{
m.clickCard(c, z);
}
}
class GiantGrowth implements MouseIn MTG Forge I don't have a Mouse interface, I call it Input, and MouseClick is renamed InputControl. (Really Input is just an Adapter class, an interface with methods that don't do anything.) The Input class also has methods to click on a player, so you can target a player. The user interface only uses InputControl and doesn't care which specific Input object is currently being used.
{
public void clickCard(Card c, Zone z)
{
if(Zone.equals("Battlefield") && c.isCreature())
{
//process and go to the next Input
//if you read below, this is where you would call stop()
}
}//clickCard()
}
The Input and InputControl classes can become very complicated especially when you consider the rare situations where Inputs can be "stacked" such as if you have a Keiga, the Tide Star in play and you play another one. Both cards go to the graveyard because they are legendary and the player will get to gain control of two creatures. Currently MTG Forge does not "stack" Inputs and doesn't correctly handle the above example with Keiga, the Tide Star.
Hopefully now you understand the power of the state pattern. In MTG Forge all of the phases are implemented as Inputs. Input objects are used for mulligans at the beginning of the game and to pay for mana costs. When I programmed MTG Forge I took many shortcuts in order to get finished, but this is one area which works beautifully and is one of the cornerstones of MTG Forge's success.
p.s.
Below is the Input class from MTG Forge which has many more methods. While handling mouse inputs is nice, really I want to tie the text of a card such as "Target Creature" or "Target Player" with the mouse so the Input class shows text to the user using showMessage(). Whenever InputControl switches to a another Input, it calls the showMessage() method first, so the user will know what is going on.
The Input class also handles if the user clicks on one of the two buttons. Typically the buttons show the text "OK" and "Cancel" so the Input methods are named selectButtonOK() and selectButtonCancel(). The ok and cancel buttons can display different text such as "yes" or "no" and one or both buttons can be disabled.
Whenever an Input object gets done executing, usually when the user clicked on a creature, the stop() method lets InputControl know that it can go on to the next Input. The next Input is typically an Input that represents a phase such as Main 1 or Declare Attackers. When a spell or ability is on the stack, an Input is also used in that situation.
For a card like Giant Growth the user chooses a target creature then pays for the card. In the targeting code, which is an Input, after the user has click on a creature the Input would call the method stopSetNext(Input in) which stops the current Input and allows the argument to be next. So in the Input used to choose the target creature, after the user has clicked on a creature it would called the method stopSetNext(new PayManaCostInput()) so the user can pay the mana cost.
I know this is a lot of information but if you are serious about programming a game like Magic, you really need to use the state pattern to handle the mouse. More information about MTG Forge Input's can be found on the forum here.
public class Input
{
//showMessage() is always the first method called
public void showMessage() {AllZone.Display.showMessage("Blank Input");}
public void selectCard(Card c, PlayerZone zone) {}
public void selectPlayer(String player) {}
public void selectButtonOK() {}
public void selectButtonCancel() {}
public void stop()
{
//this lets the InputControl know that it can go on
//to the next Input
//this method works like exit()
}
public void stopPayCost(SpellAbility sa)
{
AllZone.GameAction.playSpellAbility(sa);
}
//exits the "current" Input and sets the next Input
public void stopSetNext(Input in) {stop(); AllZone.InputControl.setInput(in);}
}
the state pattern isn't really complete without inheritance:
ReplyDeleteclass MouseController implements Mouse
this way, you can just give a mouse (which is the thought behind an interface), and it remains transparent to the class that needs a Mouse (which is the thought behind the state pattern)
Hey Forge, can I make a request? In the next version, can you release Fireblast(Visions), Goblin Grenade(Fallen Empires) and Rathi Dragon(Tempest) cards? Thanks a lot for develop this program!
ReplyDeleteI forgot, Flame Rift and Incinerate as well! Thanks!
ReplyDelete"The state pattern isn't really complete without inheritance"
ReplyDeleteWell at least you seem to understand what I'm talking about, but yes MouseController could implement the Mouse interface. Personally I don't see any pros or cons either way, but it is always good to know of alternatives.
This is one of my longer articles that took longer to write. I try to limit myself to 150-300 words.
ReplyDeleteok, I have looked at the source; InputControl is only used once. I thought about a more classic example.
ReplyDeleteSay you have a panel and some mouse listener.
panel.addMouseListener(firstListener);
at some point, you want a different behavior
panel.removeMouseListener(firstListener);
panel.addMouseListener(secondListener);
however, you don't want that for some reason. say, the change has to stay transparent from the panel. here comes the state pattern
panel.addMouseListener(control);
control.setMouseListener(firstListener);
//and when you need the change
control.setMouseListener(secondListener);
it's necessary here for the controller to implement mouse listener
it comes even better when a control has more than one state object. you can nest controls into one another to get the effect you want if the control implements the same interface it takes as state
Good example Silly Freak. I know that this article probably excluded 90% of my readers but I like being technical sometimes.
ReplyDeleteAnd this article isn't really technical since "really technical" would mean more lines of (boring) source code.
ReplyDeleteForge, thanks for the link to the forums, and the further link in the forum to sourcemaking.com. That was a new site to me and I found lots of good stuff about Design Patterns. And also links to other programming-related items, which sucked me in for over 3 hours!
ReplyDeleteWell Design Patterns are great so I try to use them as much as possible.
ReplyDelete