|
Creating Your Own Game
The first thing you should do is read through the code for the included
games as that will give you a good idea of how the toolkit works under
normal circumstances. At some point you will probably also want to read
the documentation on the framework used to send data over the network,
known as
distributed objects.
You can get by without it but we may use a few terms in this document that
are explained in the distributed objects documentation.
As a starting point, we have also provided a game which is nothing
more than a template that you can copy and change the names to start
yourself off with all the necessary bits in place. This template game is
located in the sample directory of the games archive.
It should be noted that the architecture of the sample game is not the
only way to make games with the toolkit. However, much of this structure
is useful for nearly all simple multiplayer games and it is certainly a
good place to start until you are more familiar with the functionality
available. That said, let us look at each of the elements of the sample
game in turn:
SampleObject
This class extends
GameObject
and defines the data that will be shared between the clients and server
when playing your game. The standard game object defines a few pieces of
information which are used to manage the flow of the game.
GameObject.state
This transitions from AWAITING_PLAYERS to
IN_PLAY and then to either GAME_OVER or
CANCELLED. As the events arrive indicating a change in this
attribute, the game manager and controller call methods appropriate to
each state (which will become apparent as you read about that below).
GameObject.players
This is an array that contains the usernames of all of the players in the
game. This is generally used to ensure that events received are in fact
received from game participants and at the right times (in the case of
turn-based games, for example) and not malicious users who hack their
client and try to do funny things. The client user interface generally
also uses this to display the names of the players and configure the UI
depending on whether or not the client is a player or is just watching.
GameObject.roundId
If multiple games are played by the same participants (rather than leaving
and matchmaking a new game), as each new game begins, the round id is
incremented. This allows tracking of results across multiple games and is
useful when you want to ensure that moves are associated with the proper
round (lag can sometimes result in move submissions arriving fantastically
later than expected).
Additional information specific to your game would also be contained
in this object. For example, one might have an object representing the
board and a distributed set containing the pieces that are on the
board. Perhaps an array defining the scores for each player. Again, refer
to the included games for ideas and examples of how to structure things.
SampleManager
This class extends
GameManager
and is the main entity that lives on the server and manages your game's
state. It interacts with the players by making changes to the game object
and it manages the flow of the game. The following methods are called as
the game procedes through its normal lifetime:
startGame()
This is called automatically when all of the players have arrived in the
game "room" and it triggers the normal starting procedure.
gameWillStart()
This is the method you would override to make modifications to the game
object prior to the game being started. This might include generating a
new board and setting it in the game object or resetting the player's
scores to zero or whatever is appropriate to your game.
gameDidStart()
This is called after the game starting events have been dispatched to the
clients. If your game involved taking turns or reacting to a timer or
some other periodic in-game activity, this is where you would begin that
process. Indeed the TurnGameManager
(which we don't document here but is useful for turn-based games) makes
use of this method to start the turn-taking process.
endGame()
This method is called by your code when some game-ending condition has
taken place. Perhaps all the cards or tiles were used up, or maybe a timer
has finally expired. Whatever the case, your code calls this method which
triggers the standard game ending process.
gameWillEnd()
This is a method you can override to take care of any final processing
before the game transitions to the
GameObject.GAME_OVER
state. In general, this method is used less often than
gameDidEnd() (documented next) as that's where you would
unregister timers and do whatever other cleanup that might need to be done
after the game ends. However, for completeness and in the off chance that
something needs to be done before the game actually ends, this method exists.
gameDidEnd()
This method is called after the event has been dispatched to the client
letting them know that the game is over. This is a good place to compute
final scores, assign a winner and generally wrap the game up.
It should be noted that it is possible for gameWillEnd()
and gameDidEnd() to be called even though your code did not
call endGame() explicitly. If all players involved in the
game leave the game room, the game will be cancelled (it will transition
to the CANCELLED state instead of the GAME_OVER
state) and the normal game-ending callbacks will be called to clean up
after the game. Your game ending code should check the
GameObject.state when being called and avoid doing things
like computing scores and whatnot if the game was cancelled.
resetGame()
In the event that you wish to restart a game without actually ending it
and triggering the standard ending procedures, it is possible to "reset" a
game which transitions through the start-up process again
(gameWillStart() and gameDidStart() will be
called as in a normal startup, but gameWillEnd() and
gameDidEnd() will not be called).
gameWillReset()
This is the method you would override to clear anything out that needed to
be cleared out before your game was restarted if you want to make use of
the game "reset" mechanism.
SampleController
This class extends
GameController
and manages the flow of the game on the client. It acts as the controller
in the standard model/view/controller paradigm where the model in this
case is the SampleObject and the view will be explained
momentarily. As the controller, it reacts to changes in the model and to
user input. Like in the manager, there are calldown methods provided to
allow the controller to take action when the game state changes:
gameDidStart()
This is called when the client receives notification from the server that
the game has started. This is the place where the user interface would be
enabled and the client would prepare for play.
gameDidEnd()
This is called when the client receives notification from the server that
the game has ended. Here the user interface would likely be disabled and
any appropriate game over messages (like the winner, etc.) would be
displayed.
gameWasCancelled()
If the game was cancelled rather than ended through normal play, this
method will be called. The client may wish to display a special cancelled
message and otherwise do the same cleanup it would do during normal game
ending.
resetGame()
This is a less commonly used method designed to allow the client to behave
fluidly in the face of network latency. Frequently, when a game uses
resetting, the client knows that the game will reset (perhaps the user
made the request to reset the game) before the server does and it is
useful to temporarily disable the user interface and otherwise behave as
if the game has ended until a new game start notification is received from
the server (which will result in gameDidStart being called
again). In such cases, the client can call resetGame() which
will result in a call to gameWillReset() which they can
override and clear things out in preparation for the new game.
Note: the resetGame() method does not actually send
a request to the server to reset the game. It is assumed that the server
either knows the game will reset through the normal operation of the game
or that the client explicitly requested a reset (through some
game-specific mechanism) prior to calling reset game. For example, perhaps
a client submits a request for each move, then it computes the available
moves for the next turn and if the player has run out of moves, the game
is reset (rather than ended) and they start over. The client would submit
a move to the server in the normal course of play, and the game manager
would know after it received that move that the player had no moves left
and that the game needed to be reset, so it would call
resetGame() on the server. Meanwhile, the client, after
submitting its move, also determines that there are no moves left to play,
so it calls resetGame() on the client which results in the
interface being reset immediately rather than waiting for a round-trip to
the server to hear what it already knows.
gameWillReset()
This is the calldown method that a game controller would override to reset
things in preparation for the game to restart.
The controller also handles input from the user which is generally
first processed by the view and converted into actions that are meaningful
in the context of the game. For example, the view might allow the player
to place pieces on a board, in which case it would process mouse movement
and mouse click events and eventually communicate to the controller a
request like "place piece P at coordinates C".
Most likely the view will simply maintain a reference to the
controller and call methods on it, but a mechanism is also provided to
package up those requests and deliver them as events on the AWT thread to
be handled by the controller in the same stream as the network events
(which are also dispatched on the AWT thread). This is particularly useful
if the event comes as a result of, say, a timer expiring rather than some
underlying AWT event like a mouse click. To find out more about this
mechanism take a look at
Controller
and specifically
handleAction().
SamplePanel
This panel contains the various interface elements used by the game. It
usually doesn't do much except combine all the needed elements into a
single display that can be easily instantiated by the controller.
SampleBoardView
This interface element does the main work of displaying the game and
collecting user input and communicating it in a meaningful way to the
controller. In the sample game, there's not much to do, but look at the
code for the other included games to see the sorts of things done by the
view.
The board view, like any other user interface element that wishes to
display distributed object state associated with the game "room",
implements
PlaceView.
By implementing this interface, it will automatically be notified when the
client has "entered" the game room and later when it has left. That is
accomplished with the following methods:
willEnterPlace()
This is called once we have subscribed to the game object (it is passed as
a PlaceObject reference but it is indeed your game object and
can be casted appropriately), and is a good place to add listeners to the
game object and initialize the user interface based on information
therein.
Note: if the view wants to respond to changes in the game
state, there are a couple of options. It might add itself as an
AttributeChangeListener
and respond to changes to the
GameObject.state
attribute, or the game controller can call down to the view to let it know
when the game starts or ends or whatever it needs to communicate. It is
pretty likely that the view will need to listen to the game object to hear
about game-specific changes, so additional handling of changes to the
state attribute are pretty easy to incorporate.
didLeavePlace()
This is called when the client has left the game room (generally a player
is not forced out of the room when the game ends, so this will generally
happen when the player clicks a "Back to lobby" button or something
similar and the client requests to leave the game room and head back to
the lobby room). Here any listeners added to the game object should be
removed and any other cleanup that is desired can be performed.
sample.xml
In addition to your game code, you will need to create a game definition
file which is what the Game Gardens system will use to match-make and
start your game. Here is the sample configuration:
<?xml version="1.0" standalone="yes"?>
<game>
<!-- the string identifier for this game; this is used to name our jar -->
<!-- file and to name other internal bits -->
<ident>sample</ident>
<!-- The controller and manager used for our game. -->
<controller>com.whomever.sample.client.SampleController</controller>
<manager>com.whomever.sample.server.SampleManager</manager>
<!-- Herein we define how the game is matchmade and configured. -->
<match type="table">
<!-- Properties configure the match maker, in this case: table. -->
<min_seats>2</min_seats>
<max_seats>4</max_seats>
<start_seats>2</start_seats>
</match>
<!-- Parameters define values that the user can customize when -->
<!-- creating a game and which are passed on to the game itself -->
<!-- to customize the gameplay. -->
<params>
<range ident="board_size" minimum="16" maximum="48" start="32"/>
<choice ident="rules" choices="standard,hand_of_three" start="standard"/>
<toggle ident="monkeys" start="false"/>
</params>
</game>
It is mainly self-explanatory with the items in bold being the things
that absolutely must be customized. The match-making configuration also
requires a bit more explanation. Each entry in the
<params> section provides a configurable parameter to
the person creating your game. Three types of parameters are currently
provided:
range: allows an integer value to be chosen from a
specified range.
choice: allows a single choice to be selected from a list
of choices.
toggle: a simple on/off boolean toggle.
The values chosen by the player during the match-making phase are
communicated to the game code via the ToyBoxGameConfig
class. Here's an excerpt from SkirmishManager to show how
this is used:
// documentation inherited
protected void gameWillStart ()
{
super.gameWillStart();
// get a casted reference to our game configuration
_skonfig = (ToyBoxGameConfig)_config;
// generate the game board
int size = (Integer)_skonfig.params.get("board_size");
int featureDensity = (Integer)_skonfig.params.get("feature_density");
_skobj.setBoard(SkirmishBoard.generateBoard(
size, size, featureDensity));
// start the vessels in the center of the "board"
int dx = size/2-3, dy = size/2-3;
// ...
}
As you can see, the configuration values will never be null. They will
either be the default value provided in your game configuration or some
customized value provided by the user when configuring your game. This
allows you to avoid duplicating the default values from your game
configuration in your game manager.
sample.properties
The Narya system provides a mechanism for localizing your game that is
based on Sun's localization facilities. It is not a requirement that you
use this system except to provide translations for your game configuration
parameters.
This is accomplished by adding entries to the properties file:
rsrc/i18n/sample.properties. The configuration shown above
would use the following translations:
m.range_board_size = Board size:
m.choice_rules = Rules:
m.choice_standard = Standard
m.choice_hand_of_three = Hand of three
m.toggle_monkeys = Include Monkeys?
A forthcoming article on how to actually use the localization services
will explain where to put localized versions of your properties files and
how to access those translations from within the game. The sample games
make use of the localization services so in the meanwhile that's a good
place to look.
Running your game
If you develop your game in the same directory as the sample games, you
can use the provided scripts to run your game during testing. In the
following examples sample should be replaced with the
identifier you choose for your game.
# Running the server
ant server
# Running the client (must be done after the server is started and you can
# run as many as you like as long as they have different usernames)
ant -Dusername=george client
Uploading to Game Gardens
Once you have something up and running that you want to share with the
world, you can upload your game project on the
create a game page. It should
be pretty self-explanatory but you need to provide a name, some
description, your game.xml file and your game's jar file and
you should be up and running. If you have problems getting your game
running on the site even though it works when you run it in the
development environment, check the forums or shoot an email to
gardens@threerings.net and
we'll try to work out the kinks.
Happy Gardening
That's about the size of it. Be sure to check out the
message boards if you have questions or want to
talk about game ideas and implementation details.
|