编辑“︁
How to write games using Arianne in Python
”︁
跳转到导航
跳转到搜索
警告:
您没有登录。如果您进行任何编辑,您的IP地址会公开展示。如果您
登录
或
创建账号
,您的编辑会以您的用户名署名,此外还有其他益处。
反垃圾检查。
不要
加入这个!
<div style="border: 2px solid red; padding: 1em"> '''Note: ''' The page explains how to write games in Python using Arianne. Python support in Arianne, however, is dormant. You may want to have a look at [[How to write games using Arianne]] for more recent documentation. </div> =How to write a game using Arianne= =Introduction= This document is a tutorial that will help you develop multiplayer online games using the Arianne system with python and pygame. Python is a powerful but simple scripting language, and pygame is an SDL binding for Python. Although Marauroa (the Arianne game server) is made in Java and Ariannexp (the Arianne client library) is made in C we are actually going to explain how to develop the game using Python. Python is so simple that hopefully by the end of this document everyone should be able to give making a game a try. Writing a game is a process that involves several stages: * Specification * Design * Implementation * Evaluation * Deployment Arianne is a multiplayer online games framework and engine to develop turn based and real time games. It provides a simple way of creating games on a portable and robust server architecture. Marauroa, the server, is coded in Java and uses Python for your game description. It also provides a MySQL backend and uses a UDP transport channel to communicate with dozens of players. Our reference clients are coded using Java and the C language in order to achieve maximum portability. The Arianne engine is designed so that you can concentrate on designing the actual game and ignore all the detailed implementation aspects of a complex system, such as those in the multiplayer online game content server. You therefore need not be concerned with issues of Thread, Database and Network handling. Arianne has been in development since 1999 and has evolved from a tiny application written in pseudo-C++ to a powerful, expandable but simple server framework, running on the Java platform, and a client framework, written in bare C to allow total portability of arianne's clients. Arianne's server is totally client agnostic for maximum flexibility. Since the beginning, the key concept at the heart of Arianne's development has been KISS: Keep it simple, stupid! Arianne has always been an Open source project, written and released under the GNU GPL license. We believe the right way is the Open Source way and we want you to have the power to change, edit and configure whatever you want, both in the clients and server. Arianne always welcomes your contributions and modifications to the code to create the best possible open source reference platform for game content providers. All our efforts are supported by Arianne's server: Marauroa. Marauroa is written completely in Java using a multithreaded server architecture with a UDP oriented network protocol, a MySQL based persistence engine and a flexible game system. Marauroa is totally game agnostic and makes very little assumptions about what you are trying to make thus allowing great freedom when creating games. The game system is totally expandable and modifiable to suit your game's needs. It is able to run Python scripts defining the game's rules hence providing a simple way of specifying your games behavior. Marauroa is based on a design philosophy we called Action/Perception. A Perception is a collection of data sent each turn to the clients explaining to them what they currently perceive in the game environment. Actions are sent from clients to the server and are used to ask the server to perform an action for them. You should always grab the latest release of Arianne from http://arianne.sourceforge.net as we are constantly fixing bugs and improving features. ==Mapacman game example== In this document I will show, as an example of the development of a game, the development of the mapacman game. I will give more guidelines and templates to help you start your own game at the end of the document but please first read the mapacman example. It is essential to see a real working example of a game to fully understand the Arianne system. =Prerequisites= Developing a multiplayer online game is a very complex task. In fact I would say that it is the most complex software development task that you can imagine. Arianne lowers that complexity level several orders of magnitude! However, there are still several things you need to have a knowledge of to successfully understand this game design example: * Python: Everything in this example is coded using Python. * Client/Server concepts: These will help you understand why things are done the way they are. * Game design: Having a knowledge of game design will simplify the understanding of the overall document. Just make sure you know the basics, i.e. you are able to write python code ( everyone should be able to ) and that you know a little about game design procedures. Remember that Arianne provides a client/server framework which needs modifing to suit your game thus you need to think about both the server and client sides: * Server: this is similar to the referee of all the online players. It tells every player what he/she sees and what he/she can do. It also determines the result of actions that players perform. * Client: these are similar to a TV in that they use information from the server to provide a view in to the game. The client also takes input from the player and sends that to the server. There are no other prerequisites to using Arianne technology and you should find nearly everything else you need will be explained in this document. So let's get started! =Specification= Before starting to do anything, think about the game you want to implement. Decide what your game is about, what features it will have, what will make it different to other similar games, what technologies will be used in it, and so on... Try to plan your design well first to avoid problems later. For our example we are going to write a game called mapacman. ==Basic Description== mapacman is a multiplayer online game based on the old favorite, pacman. The aim of the game is to eat as many little balls as possible as each ball increases the score of the player. There are also special balls that give extra powers to the player, mainly allowing it to eat ghosts. Ghosts are non-player characters controlled by the game that can intercept players and eat them. mapacman stands for <i>multiplayer arianne pacman</i>. A first draft of the game looks like this: [[Image:mapacman_draft.png]] ==Multiplayer fun== The multiplayer approach is based on competitiveness between players: scores increase with each ball eaten and drop when the player is eaten by a ghost. However, players can't kill or block each other, though everyone does play in the same maze (players can pass each other unhindered). Imagine a big maze filled with dozens of frantic little pacmans ( customised of course ) running away from ghosts while eating as many dots as possible to achieve the highest score. Another possible feature would be to expand the game so that the highest scoring player could possibly run another role in the game! Maybe they would be allowed to modify the map or implement a nice new feature. The point is that the highest scoring player could modify the game experience. ==Why pacman?== Pacman is dated and definately something of the past, but the game can potentially expose several issues that are similar to those in a RPG game: * There are lots of objects in the world * Objects have triggers associated with them * There is character development in the game * The World is persistent * AI (of the Ghosts) is complex as it would be in a RPG game * Everything is conceptually simple however * Lag does matter a lot ==Details== Once we know what will appear in the game we can make seperate, detailed descriptions of each entity of the game. If this sounds to you like OOP methodologies, then pat yourself on the back, as you are right! Arianne uses an Object based design approach. In the game we have the following entities: * Players * Ghosts * Balls * Super Balls * Maze ===Players=== These are the core entities of the game and look like pacman from the original game (a yellow circle with a mouth to eat-eat-eat!). They accrue points as a result of eating balls. Their score increases by one unit each time the player eats a ball, and drops by around 50% each time the pacman player is eaten by a ghost. [[Image:player_draft.png]] ===Ghosts=== Ghosts add interest to the game. Ghosts chase players around the Maze and as in the original pacman they have different behaviours. There are chaser ghosts, blocker ghosts and camper ghosts. Their number is not static like in the original pacman, but they increase or decrease depending on the size of the maze and the number of players. [[Image:ghost_draft.png]] ===Maze=== The maze is a closed labyrinth, with corridors of different lengths that usually have exits to other corridors. You can never get stuck in the maze; there should always be one way forward. The Maze is full of balls that are eaten by players. There are also super balls which are a limited number of special balls that allow players to eat ghosts. The Maze has a few special locations called respawn points where players randomly enter the game. [[Image:wall_draft.png]] ===Balls=== A ball is a dot on the maze that increases the score of the player when the player eats it. The ball then disappears for a period of time to prevent a player camping over that point (Camping is waiting in one place for items to reappear and thus often frowned upon). ===Superballs=== A Super ball is a special dot in the maze that gives the player the power to eat ghosts for a short period of time. Each time the player eats a ghost their score is increased further. As with a common ball, the Super ball disappears for a period of time after it has been eaten. ==Goal== Every game has a goal. The goal of mapacman is to get the highest score of all the players by eating lots of balls and not being eaten. ==Game overall look== We need to know what the game will look like before moving forward. We use a standard pacman game as a reference: [[Image:20040608_mapacman.jpg]] =Design= ==Technology used== To implement mapacman we are going to use Arianne, our multiplayer online game engine. So before designing the game we must understand the main concepts and ideas of Arianne. We need to make the design fit some simple rules to get the easiest design with the best possible performance. Arianne uses the UDP transport protocol, which is the fastest, lowest ping transport available. However, it comes at the cost of the application not being able to detect lost packets. If your connection is really bad you will often become out of sync with the server as so much data sent will be lost. However, there is no transport method will help you with this type of connection! Arianne's system is based on a Perception/Action/Perception scheme. This involves the server sending the clients a Perception, which is a list of RPObjects (the objects in Arianne are of type RPObject) with the modifications, additions and removals that happened in that turn. The clients take the Perception and process it to update their view of the game environment and then if they want to perform an action they send an RPAction (actions in Arianne as of type RPAction) back to the server. On the next turn the server sends a new perception message that will contain the result of the action (i.e. the changes to the objects affected by the action). This scheme has several advantages: * Works perfectly with turn based and real time based games * There is a coherent state of the game at each point in time * Players with a low ping time don't get an insane advantage * Turn time can be modified to improve bandwidth/performance * Support for several orders of magnitude more players than other systems. On the other hand it suffers from an obvious set of disadvantages: * Results are only made valid when the turn actually happens (i.e. an actions result will only appear in the next turn) * Not the best/easiest way to make a First Person Shooter type game However, this simple system is powerful enough to code nearly all games easily: both real-time and turn based games. [[Image:PerceptionActionPerception.png]] One of the main issues in the game design is choosing a turn time for the server. It should be based on the type of game we are making. For example, a real time strategy game will need turn times of around 300 ms, while a turn based strategy game will work fine with 1000-1500 ms of turn time. Turn based games save a lot of bandwidth compared to non-turn based however note that the lower the turn time, the higher the bandwidth usage. Also remember that, the lower the turn time, the higher the CPU usage. Perceptions are made up of a list of RPObjects. An RPObject is built up of several Attributes that are of the form: <br> attribute=value The attributes allow the storage of strings, ints and floats in the object. An RPObject is also built up of Slots. A Slot is a container of objects, much like a pocket, a bag, a box or a hand. The point is that if our objects need to have objects inside them, or attached to them, you need to use Slots. [[Image:RPObjectER.png]] All the dynamic changes to the world are made using Actions. An RPAction is also made up of attributes. You must redefine the default attributes of the action object so that the action becomes specific to your game. Every player is stored in a relational database using the MySQL database system. You don't need to know how this is done but you can trust me that it works! Everything is stored in the database thus making the whole world permanent. It is up to you to decide to which degree things need be stored in the database and when and how often they should be committed (stored). The database is the main bottleneck of Arianne at the time of writing. ==Entities design== Now that we know that the Arianne engine uses RPObjects and RPActions, we envision the game in a way that it is made up of these elements. Our ''Player'' will be an RPObject with the following attributes: * id: <br> The unique identification of the player * name: <br> The name of the player * x: <br> horizontal position of the player in the maze * y: <br> vertical position of the player in the maze * dir: <br> direction that player follows * score: <br> the score of the player The ''Maze'' is part of the Map and it is not sent on each perception, but on the initial connection to the system. The Maze is made of ''Walls'', each wall having: * id: <br> an identification to this block, but it is not part of the dynamic system, that is, it is not part of the RPZone. * x: <br> horizontal position of the block in the maze * y: <br> vertical position of the block in the maze There are also respawing points which are the points on the map where pacman players can appear into the game. They are marked with a <b>+</b> sign. Note that the Maze is static in this design, neither the walls nor respawn points are expected to change during the duration of the game. This is why the map is not part of the Perception that is sent to each player each turn. The ''Dots'' in the maze are part of the dynamic system (as they change pretty often) and are as such defined as a RPObject: * id <br> The unique identification of the dot * x <br> horizontal position of the dot in the maze * y <br> vertical position of the dot in the maze * !timeout <br> amount of time that must elapse before it is restored back in to the world The !timeout attribute has a special mark, the exclamation mark: <b>!</b>. The exclamation mark before an attribute's name means that the attribute is hidden to the players and that it is only considered on the server side. No player should know about when an item is going to reappear, hence the time is hidden. This avoids camper players knowing the fastest respawning dots to sit on. The ''superdots'' follow a similar definition to that of the dot: * id <br> The unique identification of the superdots * x <br> horizontal position of the superdots in the maze * y <br> vertical position of the superdots in the maze * !effectime <br> lapsus of time that the effect of the superdot is noticed on the players. * !timeout <br> amount of time that must elapse before it is restored back in to the world Finally ''Ghosts'' are a special kind of AI-controlled player that move around the maze trying to eat human players. They have: * id: <br> The unique identification of the ghost * name: <br> Each ghost has an unique name * x: <br> horizontal position of the ghost in the maze * y: <br> vertical position of the ghost in the maze * dir: <br> direction that the ghost follows ==Logic design== In pacman the players and ghosts are active objects and thus are the only entities that can create Actions. We define the actions for our players: * ''Turn'': Turn is the action used to make the ghost or player change direction, but only if there isn't a wall in the new location that the player wishes to move to. For example: <pre> ***** ..C.. ***** </pre> The player won't be allowed to move UP or DOWN, but it could move LEFT or RIGHT. It has the following attributes: * id: the unique id of the action so that we can know if it was successful or not. * turn: a char containing where the player wishes to move to: N, W, S and E As you probably have realised, N means going up on the map, W means going to the left, S means going down and E means going the right. However, when you request to move in a direction that makes a 90º degree angle with the current one and there is a wall in the way, the action is not discarded. It is actually conserved until it is cancelled by a new order, that specifies a move in a different direction, or until the turn can be properly executed. See below for an example. <pre> ********** .C->...... *******.** *.* *.* </pre> If the player presses down in the location shown above, it can not immediately move down but instead the action is stored and the player will take the hallway heading to south when it finally arrives there. This way we minimize the effect of lag on the game. And, believe it or not, that's all! As you have seen, it is actually a very simple game especially if we ignore the little trick to handle lag explained above. However, this is not the end of it. The main problem is in coding the logic of the game. Remember that actions happens synchronously, each turn, independently of the player input. For example: <pre> ******* .C->... ******* </pre> Our pacman will continue to move along the hallway, unless we change the direction of it to the left ( East ). In order to achieve this syncronous behavior we design all the logic to happen at synchronous periods of time. So each Turn we execute the server logic that consists mainly of: <pre> for each player do pos=Try to move to the next position following the current direction if pos is not Wall then Move to next position if pos is Dot then Increase player score Remove Dot at pos Add to restore later list this Dot else if pos is ~SuperDot Player becomes Hunter Remove Dot at pos Add to restore later list this Dot Add to remove hunter list this Player end if end if end for for each item in Remove Hunter List do Decreate item timeout if timeout is 0 then Remove Hunter status of item end if end for for each item in Restore Later List do Decreate item timeout if timeout is 0 then Add Dot at pos to World end if end for </pre> ==AI Logic design== Another important logic programming task is created the AI for the Ghosts that move around the maze. Their objectives are to chase and eat players and to run away from players that can eat them (after the player has consumed a superball). I should point out though that we are not going to implement different behaviours for each ghost as mapacman is meant to be a very simple and easy to understand game. An initial approach to the logic could look like this: <pre> for each Ghost do if ghost doesn't have target then get target follow target end for </pre> =Implementation= Now for the million dollar question: How do we actually implement the logic? Hopefully the answer is easy enough to be understood quickly and will be outlined in the following sections. Remember that Arianne is a very high level application and thus you don't need to mess around with the database handling, network connections, object serialization, version controlling and so on. First we are going to think about the server implementation and later about the client. ==Server== To implement the server side of your game you have two options: to use pure <b>Java</b> or to use <b>Jython</b> (http://www.jython.org/). I will explain the implmentation using Jython. This is an arbitrary choice as they are almost the same and they only differ in the way we create the classes with them. ===Getting ready=== First lets create a file named <i>mapacman_script.py</i>. Ths is the file that will contain all the Python source code that will describe the game logic. Here is the empty framework I created for it: <pre> from marauroa.game.python import * class mapacmanRP(PythonRP) def __init__(self, zone): self._zone=zone def execute(self, id, action): return 0 def nextTurn(self): pass def onInit(self, object): return 0 def onExit(self, objectid): return 0 def onTimeout(self, objectid): return 0 class mapacmanZone(PythonZoneRP): def __init__(self, zone): self._zone=zone def onInit(self): pass def onFinish(self): pass def serializeMap(self, objectid): return java.util.LinkedList() class mapacmanAI(PythonAIRP): def __init__(self, zone, sched): self._zone=zone self._sched=sched def onCompute(self, timelimit): pass </pre> This bare skeleton does nothing but will allow us to start adding functionality to the game. The main classes are inherited from marauroa.game.python.PythonRP, marauroa.game.python.~PythonZone and marauroa.game.python.PythonAIRP. These are the classes that contain the python language wrappers to the marauroa API. In this way the server can handle the Python code from Java as if it were a normal Java class. We will describe what each of the methods mean shortly. In order to inform the server of the Python game logic save the <i>mapacman_script.py</i> file to the location where the marauroa-<version>.jar or class files are. Then modify the following parameters of <i>marauroa.ini</i>: <pre> [1] rp_RPRuleProcessorClass=marauroa.game.python.PythonRPRuleProcessor rp_RPZoneClass=marauroa.game.python.PythonRPZone rp_RPAIClass=marauroa.game.python.PythonRPAIManager [2] python_script=mapacman_script.py [3] python_script_rules_class=mapacmanRP python_script_zone_class=mapacmanZone python_script_ai_class=mapacmanAI </pre> These configuration lines change the behaviour of the marauroa server by telling it to use the Python classes as the RPZone and RPRuleProcessor classes, hence changing the game rules that the server will use to the ones we are about to define. [1] <i>rp_RPRuleProcessorClass</i> is the attribute that contains the name of the class that will be used to handle all the game logic. As our game is written in Python we use the default Python rules handler. <i>rp_RPZoneClass</i> is the attribute that contain the name of the zone class that will contain all the game objects. Think of the zone as an object container. Finally, <i>rp_RPAIClass</i> is the class which contains the AI code. [2] <i>python_script</i> is the name of the file that contains the Python script and it must be in the same folder as the jar file of marauroa. [3] <i>python_script_rules_class</i> is the name of the python class that we are creating that implements the java PythonRP superclass. Thus this is class that is called from Java to provide the RP rules. <br> <i>python_script_zone_class</i> is the name of the class that is called from Java to initialize the zone from Python.<br> Finally, <i>python_script_ai_class</i> is the name of the class that will handle all the AI. Before we can start our game we also need to set up the database. Marauroa uses a JDBC database. The configuration options for the database are also in marauroa.ini. The first item is *marauroa_DATABASE*, this attribute sets the type of database that you will use: * <i>MemoryPlayerDatabase</i>: this type of database is pre built in memory and to modify it you need to recompile the server and manually edit the sources to add new accounts. Obviously when the application is shut down everything that has been modified is discarded. * <i>JDBCPlayerDatabase</i>: this is a MySQL database. It won't run as-is on other SQL compliant databases because it uses MySQL only features such as special table types to get transactions and the auto_increment column type to generate primary keys. I <b>highly recommend</b> using MySQL as your database engine and choosing JDBCPlayerDatabase as the marauroa_DATABASE. The next section in the configuration file describes the connection string to the database: * <i>jdbc_url=jdbc:mysql://127.0.0.1/marauroa</i>: This attribute tells marauroa the address of the database _( for example 127.0.0.1)_ and the name of the database e.g. _(marauroa)_. * <i>jdbc_class=com.mysql.jdbc.Driver</i>: this is the class that describes the driver used for this JDBC connection. * <i>jdbc_user=marauroa_dbuser</i>: this option is username of the database account for marauroa * <i>jdbc_pwd=marauroa_dbpwd</i>: this option is the password of the database account for marauroa NOTE: that to set up the database in the first place you need to enter MySQL as administrator and run: <pre> create database marauroa; grant all on marauroa.* to marauroa_dbuser@<serverip> identified by 'marauroa_dbpwd'; </pre> Excellent! We have set up the server now so lets run marauroad to see what happens: <pre> java -classpath marauroa.jar marauroa.marauroad -l </pre> You should see the following output: <pre> Marauroa - an open source multiplayer online framework for game development - Running on version 0.40 (C) 2003-2004 Miguel Angel Blanch Lardin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 2004-05-25 13:31:37.961 > RPServerManager::run 2004-05-25 13:31:37.961 D RPServerManager::run Turn time elapsed: 0 2004-05-25 13:31:38.555 D RPServerManager::run Turn time elapsed: 0 2004-05-25 13:31:39.165 D RPServerManager::run Turn time elapsed: 0 2004-05-25 13:31:39.758 D RPServerManager::run Turn time elapsed: 0 2004-05-25 13:31:40.368 D RPServerManager::run Turn time elapsed: 0 2004-05-25 13:31:40.961 D RPServerManager::run Turn time elapsed: 0 </pre> ===Server: Game rules logic=== Before we procced to a detailed analysis of the game logic we need to define the types of the objects that will be used in our game. If you do not define the specific type of a variable it will default to a String. As strings are large objects this is not advisable. If you do define specific types you can improve bandwidth usage by up to a factor of 5:1. We do this initialisation in the constructor of PythonZone and hence define a method in ~PythonZone like: <pre> def createRPClasses(self): STRING=RPClass.STRING SHORT_STRING=RPClass.STRING INT=RPClass.INT SHORT=RPClass.SHORT BYTE=RPClass.BYTE FLAG=RPClass.FLAG HIDDEN=RPClass.HIDDEN objclass=RPClass("position") objclass.add("x",BYTE) objclass.add("y",BYTE) objclass=RPClass("player") objclass.isA("position") objclass.add("name",SHORT_STRING) objclass.add("dir",SHORT_STRING) objclass.add("score",INT) objclass.add("super",BYTE) objclass.add("!vdir",STRING,HIDDEN) objclass.add("!hdir",STRING,HIDDEN) objclass=RPClass("ghost") objclass.isA("player") objclass.add("!target",INT,HIDDEN) objclass.add("!decision",INT,HIDDEN) objclass.add("?kill",FLAG) objclass=RPClass("block") objclass.isA("position") objclass=RPClass("ball") objclass.isA("position") objclass.add("!score",INT,HIDDEN) objclass.add("!respawn",INT,HIDDEN) objclass=RPClass("superball") objclass.isA("ball") objclass.add("!timeout",INT,HIDDEN) </pre> Note how we define the data types and specify their visibility. As you can see, Marauroa supports inheritance on data definitions using the <i>isA("name")</i> method of RPClass (see how all classes are inherited from position and for example ghost is inherited from player). Usage of inheritance is optional but the advantages are enormous so don't pass it by! :-) First, we are going to add the code for the <i>execute</i> method of the mapacmanRP class. This method is called by the scheduler to execute actions for the current turn. <pre> def execute(self, id, action): [1] player=self._zone.get(id) [2] action_code=action.get("type") [3] if action_code=="turn": result=self.turn(player,action.get("dir")) [4] elif action_code=="chat": result=self.chat(player,action.get("content")) else: [5] print "action not registered" [6] print "Player doing ", action.toString()," with result ", result return result </pre> Our first important step at [1] and [2] is to retrieve the player from the world and then get the type of action he/she is trying to do. For example: <pre> Our player Billy, whose id is 2, sent an action to the server: action={type=chat,content=Hi World} Our server will call execute with params: %%% execute(2,action) And our script will choose the chat method. Nice and simple, isn't it? </pre> In parts [3], [4] and [5] we execute the actual action code that is related to the type of action being performed. Lets have a look at the code for each action in the mapacmanRP class. <pre> def turn(self, player, direction): result=failed if direction=='N' or direction=='W' or direction=='S' or direction=='E': if self.canMove(player,direction): player.put("dir",direction) self._zone.modify(player) result=success return result def canMove(self, player, dir): """ This methods try to move the player and return the new position """ x=player.getInt("x") y=player.getInt("y") if dir=='N' and (y-1)>=0 and self._map.get(x,y-1)<>'*': return 1 elif dir=='W' and (x-1)>=0 and self._map.get(x-1,y)<>'*': return 1 elif dir=='S' and (y+1)<self._map.sizey() and self._map.get(x,y+1)<>'*': return 1 elif dir=='E' and (x+1)<self._map.sizex() and self._map.get(x+1,y)<>'*': return 1 else: return 0 </pre> As you can see, in this implementation the turn method still doesn't handle the issue we pointed out in the design stage, that is, allowing the pacman to move on in the current direction and then perform any turn requested at a earlier time when a corner is reached. To take this feature in to account we modify our implementation as follows: <pre> def turn(self, player, direction): result=failed directionsH=['E','W'] directionsV=['N','S'] if _directions.count(direction)==1 and not self._canMove(player,direction): if directionsH.count(direction)==1: player.put("!hdir",direction) if directionsV.count(direction)==1: player.put("!vdir",direction) if _directions.count(direction)==1 and self._canMove(player,direction): player.put("dir",direction) self._zone.modify(player) result=success return result </pre> Our first modification is to store the new direction. If it is a horizontal one then we store it in !hdir and a vertical direction is stored in !vdir (notice that we store this data in hidden attributes). We also renamed the canMove method to _canMove. The new canMove implements the logic to decide if we need to move perpendicular to the current direction of the player movement. <pre> def canMove(self, player): dir=player.get("dir") if (dir=='E' or dir=='W') and player.has("!vdir"): vdir=player.get("!vdir") if self._canMove(player,vdir): player.put("dir",vdir) player.remove("!vdir") if (dir=='N' or dir=='S') and player.has("!hdir"): hdir=player.get("!hdir") if self._canMove(player,hdir): player.put("dir",hdir) player.remove("!hdir") return self._canMove(player,player.get("dir")) </pre> canMove is called each turn to determine if the client can move. As you can see in the code, first we decide if we need to change the direction of the player and then, using the old code, we decide if we can move in that direction or not. The chat action is relatively simple to understand: <pre> def chat(self, player, content): player.put("?text",content) self._zone.modify(player) return success </pre> The main procedure of a game turn is as follows: <pre> # get action params # do action logic # modify player and/or world # notify zone that you modified them </pre> One thing to note is that you need to manually notify everything about the modifications you make each turn. This speeds up the creation of the perceptions and also controls more accurately when and how players perceive things. Now let's handle the player login, logout and timeout events. The reason for handling these events is to allow your application to control what needs to be done in these special cases. For example, you may need to add attributes on login, or remove other attributes, or mark the characters and so on. <br> The events available are: * <i>onInit</i>: this is the login event and happens when the player is inserted into the world. There is a period of time between login and game start that corresponds to the time frame between one synchronization perception and the next. This way we ensure that player has a coherent state of the world before really joining the game. * <i>onExit</i>: this is the logout event and happens when the player requests to logout. Your only task is to modify the object so that it is subsequently stored in the database correctly. * <i>onTimeout</i> : this is the timeout event that happens when a player loses connection to the server. It is called after a period of inactivity, usually around 1 minute. Usually the simplest option is to map this method to onExit(). <pre> def onInit(self, object): """ Do what you need to initialize this player """ pos=self._map.getRandomRespawn() object.put("x",pos[0]) object.put("y",pos[1]) self._zone.add(object) self._online_players.append(object) return 1 </pre> The onInit method of the mapacmanRP class inserts the player into the world and gives them a position using a random respawn point that we defined on the map. It also inserts the player in to a list of online players, so we can more easily iterate through who is online at any point in time. <pre> def onExit(self, objectid): """ Do what you need to remove this player """ for x in self._online_players: if x.getInt("id")==objectid.getObjectID(): self._online_players.remove(x) break self._zone.remove(objectid) return 1 </pre> The onExit method of the mapacmanRP class is the opposite of onInit. It removes the player from the world and from our data structure. Note: the search for the player '''must''' be done using the RPObject.ID, not the object name. <pre> def onTimeout(self, objectid): return onExit(self,objectid) </pre> In our example we are not interested in handling the timeout of the player in any special way; we simply remove it from the world. You may want to make the player stay in the world for a period of time after this so they cannot exploit the annoying when-I-am-going-to-die-I-quickly-logout trick! One important thing to note is that in this case we don't call the modify method of the zone; this is because it is automatically called when we add or remove objects from the world. Now for the mother of all the methods in mapacmanRP class: the nextTurn method: <pre> def nextTurn(self): [1] for object in self._removed_elements: if object['timeout']==0: self._addBall(object) else: object['timeout']=object['timeout']-1 [2] for object in self._super_players: if object['timeout']==0: object['object'].remove("super") self._super_players.remove(object) else: object['timeout']=object['timeout']-1 object['object'].put("super",object['timeout']) [3] self._zone.modify(object['object']) self._foreachPlayer() </pre> [1] takes care of the dots that have been eaten by the pacmans. After their timeout value has reach 0 they are restored back to world. This means that the game is almost endless. [2] is in charge of removing the special Super (Ghost eater) state from players that have elapsed the full time after eating a superball. Notice that we modify the object directly so that when the zone is modified and the new perception is sent to the client, it can see that the object was modified. [3] part is the call to the zone modify function to tell the zone that this object has changed. The following method is also called each turn from nextTurn: <pre> def _foreachPlayer(self): for player in self._online_players: if(self.canMove(player,player.get("dir"))): print 'You move in %s direction' % player.get("dir") self._movePlayer(player) self._ghostCollisions(player) else: print 'You CAN\'T move in %s direction' % player.get("dir") </pre> The above code tries to move each online player in the direction chosen and if the move is possible then calls the movePlayer method. We also create some output to help debug the server. <pre> def _movePlayer(self,player): pos=self.move(player) if self._map.hasZoneRPObject(pos): object_in_pos=self._map.getZoneRPObject(pos) if object_in_pos.get("type")=="ball": self._removeBall(object_in_pos,pos) # Increment the score of the player player.add("score",1) elif object_in_pos.get("type")=="superball": self._removeBall(object_in_pos,pos) # Notify to remove the attribute on timeout timeout=object_in_pos.getInt("!timeout") player.put("super",timeout) element={'timeout':timeout,'object':player} self._super_players.append(element) self._zone.modify(player) </pre> If the _movePlayer method decides that the player can move in a particular direction (i.e. there is not a wall in the way) it then checks what is found in the new position. If, in the new position there is an object of type dot ( ball ) then we remove it and insert it in the list of elements to be restored later (as seen in section [1] of nextTurn). If the object is a Superdot ( Superball ) we remove it, as before, but also add the super attribute to the player information. The super attribute will be removed at a later time (see section [2] of nextTurn). The following two methods are simply helper functions for the nextTurn logic: <pre> def _removeBall(self, ball, pos): self._zone.remove(ball.getID()) self._map.removeZoneRPObject(pos) element={'timeout':ball.getInt("!respawn"),'object':ball} self._removed_elements.append(element) def _addBall(self, element): self._map.addZoneRPObject(element['object']) self._removed_elements.remove(element) </pre> The _ghostCollisions method handles the event of ghosts eating players (effectively by colliding with them): <pre> def _ghostCollisions(self, player): pos=(player.getInt("x"),player.getInt("y")) for ghost in self.getGhosts(pos): if player.has("super"): # TODO: Eat the ghost pass else: print "Ghost killed player ",player.get("id") ghost.add("score",1) ghost.put("?kill","") if ghost.has("!target"): ghost.remove("!target") self._killedFlagGhosts.append(ghost) self._zone.modify(ghost) pos=self._map.getRandomRespawn() player.put("x",pos[0]) player.put("y",pos[1]) player.put("score",player.getInt("score")/2) self._zone.modify(player) </pre> We tag the particular ghost with the ?kill attribute to indicate that it has eaten a player and we increase its score by one. We also modify the position and the score of the player that just got killed. Once a ghost kills a player we tell it to look for a new target by removing its target attribute. Note that in this implementation the player will respawn immediately and you may want to add a respawn delay (10-15 seconds maybe) to avoid this. We now only need to define the way in which the game map is seen but before going in to that I will show you the helper methods in mapacmanZone that where used to create the objects: <pre> def createPlayer(self, name): """ This function create a player """ object=self._zone.create() object.put("type","player"); object.put("name",name) object.put("x",0) object.put("y",0) object.put("dir",randomDirection()) object.put("score",0) return object; </pre> This method creates a player and gives it a random direction using randomDirection. Note that we use zone.create( ) to get a new RPObject with a valid id. Remember that each object '''must''' have a unique id for the whole session and the only way to make sure of this is to use the RPZone create( ) method. <pre> def createGhost(self, name): """ This function create a ghost """ object=self._zone.create() object.put("type","ghost"); object.put("name",name) object.put("x",0) object.put("y",0) object.put("score",0) object.put("dir",randomDirection()) return object; </pre> <pre> def createBall(self, x,y): """ This function create a Ball object that when eats by player increments its score. """ object=self._zone.create() object.put("type","ball"); object.put("x",x) object.put("y",y) object.put("!score",1) object.put("!respawn",60) return object; </pre> <pre> def createSuperBall(self, x,y): """ This function create a SuperBall object that when eats by player make it to be able to eat and destroy the ghosts """ object=self.createBall(x,y) object.put("type","superball"); object.put("!timeout",15) return object; </pre> Note that a super ball is just a special type of ball, so we reuse the definition of ball but then change the type name and timeout. Ok, now let's see what the map looks like in mapacman. The map class doesn't need to follow any special mapacman guidelines so we can simply design it however we want. Just one thing to note: mapacmanPython has a method named serializeMap, that will be implemented here. serializeMap gives the client a list, on login, of the static objects that make up the map, for example the walls. Here is the class: <pre> class mapacmanRPMap: def __init__(self, pythonRP, filename): f=open(filename,'r') line=f.readline() self._respawnPoints=[] self._last_respawnPoints=0 self._objects_grid={} self._grid=[] self._zone=pythonRP.getZone() i=0 while line<>'': j=0 for char in line[:-1]: if char=='.': self.addZoneRPObject(pythonRP.createBall(j,i)) elif char=='0': self.addZoneRPObject(pythonRP.createSuperBall(j,i)) elif char=='+' pos = (j,i) self._respawnPoints.append(pos) j=j+1 self._grid.append(line[:-1]) line=f.readline() i=i+1 </pre> This is a relatively complex method of the class and its task is to load a text file that contains the map definition into the data structure that hosts the map. A map file is drawn as follows: <pre> ************* *+...***...+* *.**.***.**.* *.*.......*.* *...*****...* ***...*...*** *...*...*...* *.***.*.***.* *+....*....+* ************* </pre> The mapacmanRPMap method reads each line in to a list of strings in Python and also creates a list of dynamic objects like dots and superdots. We also look for respawn points on each line and add them to the list of objects. Remember respawn points are the points chosen by the designer at which players enter the world. Think of them as the phones in The Matrix! :-) The following methods are some helper functions to test, get, add and remove static and dynamic objects in the world. <pre> def get(self,x,y): return (self._grid[y])[x] def hasZoneRPObject(self, pos): return self._objects_grid.has_key(pos) def getZoneRPObject(self,pos): return self._objects_grid[pos] def addZoneRPObject(self,object): x=object.getInt("x") y=object.getInt("y") self._objects_grid[(x,y)]=object self._zone.add(object) def removeZoneRPObject(self,pos): del self._objects_grid[pos] </pre> The next two methods are more helper functions to make the rest of the logic independent of the map representation: <pre> def sizey(self): return len(self._grid) def sizex(self): return len(self._grid[0]) </pre> The next method chooses random respawn points (as we want our player to reappear at a randomly chosen respawn point): <pre> def getRandomRespawn(self): self._last_respawnPoints=(self._last_respawnPoints+1)%(len(self._respawnPoints)) return self._respawnPoints[self._last_respawnPoints] </pre> The serializeMap function is quite important and worth a special mention: <pre> def serializeMap(self): def createBlock(pos): object=RPObject() object.put("x",pos[0]) object.put("y",pos[1]) object.put("type","block") return object listObjects=LinkedList() y=0 for line in self._grid: x=0 for char in line: if char=='*': listObjects.add(createBlock((x,y))) x=x+1 y=y+1 return listObjects </pre> The idea behind serializeMap is to tell the client what the static part of the game is like. Think of the static part as a background for a 2D scroller. These objects don't need to be sent on each perception because they never change. To help myself I create a createBlock function that, given a position, returns an RPObject containing the information for the static object at that point as these static objects don't have an id. They do not have a unique id as these objects are just static in the game and they don't really do anything to the game logic. As you can see from the code that the algorithm simply runs through the whole list of strings and for each Wall it creates a Block object and adds it to a list. When we are done we just return the list and the server will send the map to the clients. ===Server: Game world implementation=== The RPZone of mapacman is very light and simple so we don't want to load or store anything in the database. <br> You may want to store and load ghosts instead of resetting them each time but this is not necessary. <pre> class mapacmanZone(PythonZone): def __init__(self, zone): self._zone=zone def onInit(self): return 1 def onFinish(self): return 1 </pre> As you can see the zone class is completely empty. ===Server: Game AI implementation=== The AI implementation is in charge of moving pacmans around the map and handling the ghosts. Everything must be done inside the compute method. Frist we create two helper methods to access the PythonAI from PythonRP: <pre> def getPythonAI(): return variable_PythonAI def setPythonAI(pythonAI): global variable_PythonAI variable_PythonAI=None if variable_PythonAI is None: variable_PythonAI=pythonAI </pre> PythonAI is called before PythonRP so we set pythonAI in PythonAI's constructor and in PythonRP we set the pythonRP attribute in PythonAI. What does this mean? It means that we can reuse the PythonRP methods from PythonAI. This works because the initialization order of the classes is always the same: # RPZone # RPAI # RPRuleProcessor The following method is called on initialisation by PythonRP: <pre> class RealPythonAI(PythonAI): def __init__(self, zone, sched): self._zone=zone self._sched=sched self.pythonRP=None self.ghosts=[] setPythonAI(self) def setPythonRP(self, pythonRP): self.pythonRP=pythonRP def createEnviroment(self): ghost=self.pythonRP.createGhost('Sticky') self.pythonRP.onInitAIGhost(ghost) self.ghosts.append(ghost) </pre> The method creates a ghost that moves around the maze to make the game a bit more interesting. The compute method computes the actual AI: <pre> def compute(self,timelimit): if len(self.pythonRP._online_players)==0: return 1 for ghost in self.ghosts: print ghost.toString() if self.pythonRP.canMove(ghost)==0: print "Can't move: Changing direction" dir=randomDirection() ghost.put("dir",dir) else: self.pythonRP.move(ghost) self.pythonRP._zone.modify(ghost) for player in self.pythonRP._online_players: self.pythonRP._ghostCollisions(player) return 1 </pre> This method is the real AI of the ghost and as you can see, at the moment, it is pretty stupid. The point of this document is not to create a long AI implementation but rather to give you an insight in to game design in general. In this simple implementation each ghost will just try to move in any direction. If it cannot move in that direction the algorithm will randomly change its direction. Once it has finished this it checks to see if the ghost collided with any players. The idea may seem a little stupid but at the time of writing it has scored 5000 kills in only 12 hours! :-) However, if desired, it is not hard to make a more complex implementation of the AI, for example: <pre> for ghost in self.ghosts: ghost.add("!decision",-1) target=None if not ghost.has("!target"): players=self.pythonRP._online_players target=players[random.randint(0,len(players)-1)] ghost.put("!target",target.get("id")) else: try: target=self.pythonRP._zone.get(RPObject.ID(ghost.getInt("!target"))) except: ghost.remove("!target") if target is not None and ghost.getInt("!decision")<=0: ghost.put("!decision",5) difx=target.getInt("x")-ghost.getInt("x") dify=target.getInt("y")-ghost.getInt("y") if difx>0: ghost.put("!hdir","E") elif difx<0: ghost.put("!hdir","W") if dify>0: ghost.put("!vdir","S") elif dify<0: ghost.put("!vdir","N") if self.pythonRP.canMove(ghost)==0: dir=randomDirection() ghost.put("dir",dir) else: self.pythonRP.move(ghost) self.pythonRP._zone.modify(ghost) </pre> Basically this algorithm randomly chooses a target and tries to get the ghost near to it by moving in the horizontal or vertical directions. To avoid the ghost getting stuck because of the primitive AI, this decision is made only once every 5 turns, and if the ghost does get stuck then a new direction is chosen for it randomly. This implementation is just a bit more complex but proves that a little more work goes a long way as the ghost becomes a killing machine with this code! :-D ==Client== Implementation of the client side is not as easy as that of the server side however it is still relatively simple. The main flow of the client is: <pre> login choose Character map=get Map while not exit do if has Perception get Perception apply Perception end if draw map draw perception if has input from user get input from user send action end if end while </pre> To help your understanding of this example, you should read the pyarianne API definition before continuing. To implement the client we are going to use pyarianne and pygame. pyarianne is the Python arianne client library and pygame is a set of python modules for game creation. Lets first create our main game loop and later define the rest of the helper functions. Keep in mind that I am not writing how to design a great client, rather, I will describe the design of a basic client that just does the minimum to work properly. That is it will have no GUI and no extras! :-) <pre> presentation=GamePresentation() logic=GameLogic(presentation) presentation.init() logic.init("127.0.0.1",32140) if logic.login("miguel","qwerty"): character=presentation.chooseCharacter() if logic.chooseCharacter(character): while not presentation.exitRequested(): logic.run() logic. logout() else: print 'ERROR(2): '+pyarianne.errorReason() else: print 'ERROR(1): '+pyarianne.errorReason() presentation.quit() </pre> I have designed the client so hat the presentation and logic code of the game are completely seperate ( yes, I am a maniac ). This will mean replacing pygame for another engine like a Soya or pyopengl should be an easy task. The presentation class is in charge of putting things on the screen and handling the user input. It does not use the pyarianne library and is thus independant of Arianne but totally dependant on pygame. On the other hand, the Logic class is the one that talks to pyarianne and thus is totally dependant on Arianne while being independent of the technology used in the presentation class ( i.e. pygame in our code ). ===Client: Game logic=== First, lets have a look at the logic class which is the one that talks with the arianne server via the pyarianne library: <pre> class GameLogic: def __init__(self, presentation): self._world=pyarianne.World() self._presentation=presentation listener=mapacmanListener(self._presentation) self._perceptionHandler=pyarianne.PerceptionHandler(listener) </pre> The GameLogic class is the constructor of the game logic and its only important job is to create the listener that will be the glue between perception handling and the presentation layer. This glue will be called the mapacmanListener. The point of the listener is to process things from the perception (refer to pyarianne documentation for more info). We need to redefine the methods that we will use, in our case: * onClear * onAdded * onModifiedAdded * onDeleted The rest of the methods are simply of no interest to the mapacman game. Note that we wish to pass specific tasks to the presentation class instead of doing them directly ourselves. <pre> class mapacmanListener(pyarianne.PerceptionListener): def __init__(self, presentation): pyarianne.PerceptionListener.__init__(self) self._presentation=presentation </pre> <pre> def onClear(self): self._presentation.mapDots.empty() self._presentation.mapPlayers.empty() </pre> Here we redefine onClear because it is the method that is called on a sync perception and clears everything and thus gets us ready to start adding objects. This method should really only be called once, but it may be called more times if a player looses synchronization with the server due to a bad connection. <pre> def onAdded(self, object): if object.get('type')=='ball': pos=(object.getInt("x"),object.getInt("y")) self._presentation.mapDots.add(Dot(object.getInt("id"),pos)) elif object.get('type')=='player': pos=(object.getInt("x"),object.getInt("y")) self._presentation.mapPlayers.add(Player(object.getInt("id"),pos)) </pre> The onAdded method is used to add new objects, like dots that reappear or new players that enter the game. It will also be called just after onClear. <pre> def onModifiedAdded(self, object): for item in self._presentation.mapPlayers.sprites(): id=object.getInt("id") if id==item.getID(): pos=(object.getInt("x"),object.getInt("y")) item.setPosition(pos) break </pre> The onModifiedAdded is called each time an object is modified. In mapacman that is every time one object chats or moves. Note that the object is already in the world and we are only making modifications to its attributes. <pre> def onDeleted(self, object): for item in self._presentation.mapPlayers.sprites(): id=object.getInt("id") if id==item.getID(): item.kill() for item in self._presentation.mapDots.sprites(): id=object.getInt("id") if id==item.getID(): item.kill() </pre> The onDeleted method is called each time an object is removed from the world. For example when players leave and dots are eaten. Let's continue analysing the logic class. <pre> def init(self, server, port): def idleCallback(): self._presentation.idleCallback() pyarianne.setIdleMethod(idleCallback) pyarianne.connectToArianne(server, port) </pre> The init method is called with a server and a port to connect to. This is then passed to the pyarianne library to make the actual connection to the server. We also set the idleCallback method here to take advantage of a nice feature of python that allows inner functions access to variables of the methods and class where the function is defined. <pre> def login(self, username, password): result=False if pyarianne.login(username, password): self._chars=pyarianne.availableCharacters() self._listOfCharacters(self._presentation) result=True return result def _listOfCharacters(self, presentation): self._presentation._showListOfCharacters(self._chars) </pre> This method performs the actual login to the server using the username and password passed to the init function. Once you have logged in to the server successfully it will pass the list of players to the presentation layer. <pre> def chooseCharacter(self, character): result=False if pyarianne.chooseCharacter(character): listObjects=pyarianne.getRPMap() worldMap=WorldMap(listObjects) self._setRPMap(worldMap) result=True return result def _setRPMap(self,worldMap): self._presentation._addRPMapObjects(worldMap.listWalls()) </pre> The presentation layer will return one of the characters to us which we then pass to the chooseCharacter method. This method will then select the appropriate character on the server. If the client is allowed to choose this character, the server will send the client a list of Objects that make up the map. This list is then passed to the presentation layer so that the map can drawn by it. <pre> def run(self): if pyarianne.hasPerception(): perception=pyarianne.getPerception() self._perceptionHandler.applyPerception(perception, self._world) self._presentation._update() event=self._presentation.getEvent() if event=='N' or event=='S' or event=='W' or event=='E': def createTurnAction(dir): action=pyarianne.RPAction() action.put('type','turn') action.put('dir',dir) return action pyarianne.send(createTurnAction(event)) </pre> The run method handles all the logic of the game. It checks if a perception has been received and if so it applies it to the world (that process will call listener if it is needed). Note that you only need to call this method once per perception turn time ( usually about 300 ms is a good value for this ). Next we update the presentation class and then get any user input events that may be waiting in the presentation class and send them to the server. For this game the only valid user input is a move event. To send the action we create a helper method called createTurnAction. We simply pass the direction we wish to move in and the function creates an action with type=turn and our direction. The action object is then sent to the server using the pyarianne library function send(). Note that there is no chat support in this example. This functionality would require a GUI and that would just make the example harder to explain and follow. It would also add extra complexities such as the need for Unicode support. To see the chat implementation please refer to the actual source of this game available for download. <pre> def logout(self): pyarianne.logout() </pre> The logout method performs the logout from the server by calling the logout function in the pyarianne library. ===Client: Game presentation=== The client presentation layer is implemented using pygame which a good SDL wrapper graphics support library for Python. (www.pygame.org & www.libsdl.org) Pygame is very simple to use but I will include some explaination of how it works as I continue to explain the presentation layer. Pygame is very 2D oriented and hence I will code mapacman using Sprites. A sprite is a small graphic that can be moved independently around the screen, producing animated effects. Pygame provides a Sprite class and so we will subclass it to create our own sprites. The BlockSprite is a class used for static objects: <pre> class BlockSprite(pygame.sprite.Sprite): def __init__(self, resource): pygame.sprite.Sprite.__init__(self) self.image, self.rect=loadImage(resource) self.pos=None def setPosition(self, pos): self.pos=pos x=[self.pos[0]*SPRITE_WIDTH,self.pos[1]*SPRITE_HEIGHT] y=[SPRITE_WIDTH,SPRITE_HEIGHT] self.rect=[x,y] def update(self): pass </pre> This is our basic static sprite for a block like a wall or a dot. It is not animated and has no orientation. We initialise it as expected and we load the graphic resource that is passed as a parameter. We need to call setPosition explictly to tell pygame where this block is expected to be located. The update method is called by pygame to update the sprite. However, as this is a static object there should be nothing to update and hence there is nothing to be done in the update method. The following are the actual static objects inherited from BlockSprite: <pre> class Dot(BlockSprite): def __init__(self, id, pos): BlockSprite.__init__(self,'dot.png') self._id=id self.setPosition(pos) def getID(self): return self._id class Wall(BlockSprite): def __init__(self, pos): BlockSprite.__init__(self,'block.png') self.setPosition(pos) </pre> We create two subclasses of our BlockSprite sprite class, one for dots and one for walls. A nice improvement here would be to pass the wall a description of what surrounds it. This would make it possible for the wall to change depending on adjacent objects. Animated sprites have thier own class: <pre> class AnimatedSprite(pygame.sprite.Sprite): def __init__(self, resource): pygame.sprite.Sprite.__init__(self) self.images=[] self.images.append(loadImage(resource+'_N.png')) self.images.append(loadImage(resource+'_E.png')) self.images.append(loadImage(resource+'_S.png')) self.images.append(loadImage(resource+'_W.png')) self.image, self.rect=self.images[0] self.pos=None def setPosition(self, pos): self.pos=pos x=[self.pos[0]*SPRITE_WIDTH,self.pos[1]*SPRITE_HEIGHT] y=[SPRITE_WIDTH,SPRITE_HEIGHT] self.rect=[x,y] def setDirection(self, dir): if dir=='N': self.image,i=self.images[0] if dir=='E': self.image,i=self.images[1] if dir=='S': self.image,i=self.images[2] if dir=='W': self.image,i=self.images[3] def update(self): pass </pre> This class is also a subclass of Sprite but it actually loads several sprites, one for each orientation. It also has methods to set the position and the orientation of the object. The setDirection method takes a parameter given to it by mapacmanListener and makes the sprite head in that direction. There is no actual animation of the sprite because we are making a very simple client. Note that if we add animations to the sprite then we will add the code for them in the update method. The following classes are for our entities which can move: <pre> class Player(AnimatedSprite): def __init__(self, id, pos, dir): AnimatedSprite.__init__(self,'player') self._id=id self.setPosition(pos) self.setDirection(dir) def getID(self): return self._id class Ghost(AnimatedSprite): def __init__(self, id, pos, dir): AnimatedSprite.__init__(self,'ghost') self._id=id self.setPosition(pos) self.setDirection(dir) def getID(self): return self._id </pre> The player and ghost sprites are exactly the same because there is no difference between a player and a ghost front the graphics point of view. However, note that we need to store the id of the player so we can use it to recognize the player later. The presentation layer constructor is: <pre> class GamePresentation: def __init__(self): self._exit_requested=False self._event=None </pre> The constructor is very simple, we just need to set exit_requested to false so that the client doesn't exit immediately! The init method is called to initialise the presentation class: <pre> def init(self): pygame.init() [1] self.screen=pygame.display.set_mode((400, 400)) [2] self.background=pygame.Surface([400, 400]) [3] self.background.fill([0, 0, 0]) self.clock=pygame.time.Clock() [4] pygame.display.set_caption("mapacman "+VERSION+" client&;quot;) [5] image,rect=loadImage('mainscreen.png') self.screen.blit(image, [0, 0]) pygame.display.update() [6] self.mapDots=pygame.sprite.RenderUpdates() self.mapPlayers=pygame.sprite.RenderUpdates() self.mapWalls= pygame.sprite.RenderUpdates() </pre> This method contains a lot of pygame specific code. [1] the set_mode method initialises the graphic screen mode to 400x400 using the best available colour depth on the system. [2] and [3] set the background to black by creating a new surface and filling it with black. This is necessary to make the game look correct. [4] sets up the title and the icon of the window. We place the caption "mapacman 0.01 client" in the window title. [5] shows a splash screen until the game finishes loading and is ready to go. [6] creates the groups which our sprites will belong to. A group is a container for sprites. The RenderUpdates is a sprite group that can draw and clear with update rectangles. The ~RenderUpdates is derived from the ~RenderClear group and keeps track of all the areas drawn and cleared. It also cleverly handles overlapping areas between where a sprite was drawn and cleared when generating the update rectangles. <pre> def exitRequested(self): return self._exit_requested </pre> The exitRequested method returns whether the user has requested to exit or not. <pre> def quit(self): pygame.quit() </pre> The quit method must be called before exiting so that pygame can free all the resources properly and in the correct order. <pre> def chooseCharacter(self): #TODO: Make character choosal choosable return self._characters[0] def _addRPMapObjects(self, listObjects): for i in listObjects: self.mapWalls.add(i) self.screen.blit(self.background, [0, 0]) self.mapWalls.draw(self.screen) pygame.display.flip() </pre> The addRPMapObjects method adds all the blocks that should appear then blits the black background to the screen and draws the sprites in place. Finally, the back buffer is flipped on to the screen so that the changes are made visible. Now we shall look at the main loop of the presentation class: <pre> def getEvent(self): event=self._event self._event=None return event def _update(self): [1] for event in pygame.event.get(): if event.type == QUIT: self._exit_requested=True elif event.type == KEYDOWN: if event.key == K_ESCAPE: self._exit_requested=True elif event.key ==K_UP: self._event='N' elif event.key ==K_DOWN: self._event='S' elif event.key ==K_LEFT: self._event='W' elif event.key ==K_RIGHT: self._event='E' [2] self.mapWalls.update() self.mapDots.update() self.mapPlayers.update() [3] self.updated_areas=[] self.updated_areas.extend(self.mapWalls.draw(self.screen)) self.updated_areas.extend(self.mapDots.draw(self.screen)) self.updated_areas.extend(self.mapPlayers.draw(self.screen)) [4] pygame.display.update(self.updated_areas) [5] pygame.time.delay(30) [6] self.mapWalls.clear(self.screen, self.background) self.mapDots.clear(self.screen, self.background) self.mapPlayers.clear(self.screen, self.background) </pre> [1] iterates over the events that are waiting and processes the events which we are interested in. We are only interested in the UP, DOWN, LEFT, RIGHT and EXIT events, so a single variable is ok for them all. This method needs to be improved in order to allow for chatting. [2] is where we update the sprites. Note however, that in our example, this will do nothing as the update method of each sprite is empty. [3] is where the sprites are actually drawn now they have been updated. We call the draw method of the groups and they will take care of restoring backgrounds and handling overlapping areas and so on. The draw method will generate a list with the positions that need to be updated. [4] updates the positions that have changed. This way is the fastest possible way of doing an update. Any other methods won't go above 20-15 fps even on a very good machine. [5] sleeps for a few milliseconds as CPU time is valuable and we have to allow the CPU time to do other tasks. [6] is where we restore the background to where it was before the sprites were drawn. However, note that even if we restore it this doesn't update the screen as we are working in the back buffer. <pre> def _showListOfCharacters(self, characters): self._characters=characters </pre> The _showListOfCharacters method should show the list of characters available to the user but as we are still not using a GUI we simply store it so we can choose the first one in the list later. =Evaluation= To evaluate the result we are actually going to use the full implementation of the game server and client. ==Download the files from Sourceforge== Go to the following URLs and grab the latest version of each package: * Marauroa http://sourceforge.net/project/showfiles.php?group_id=1111&package_id=114051 * ariannexp http://sourceforge.net/project/showfiles.php?group_id=1111&package_id=1109 * mapacman http://sourceforge.net/project/showfiles.php?group_id=1111&package_id=120109 ==Using CVS access== In order to get Marauroa grab a copy from CVS by using: <pre> cvs -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/arianne checkout marauroa </pre> To get a CVS copy of the client mapacman use: <pre> cvs -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/arianne checkout ariannexp cvs -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/arianne checkout mapacman </pre> Remember to follow the instructions in the README of each package to know how to build and setup the server and clients. Once you have them built and working carefully choose the turn time for the game. The best turn time choice can be made by using the following conditions: * It is at least as big as the mean ping time to your server * It is long enough for the server to do all the game computation in that time * It happens often enough, but is not unnecessarily bandwidth hungry For example, 10 ms of turn time is too aggressive, as no client will issue commands as fast as every 10 ms, but 300-400 ms is a good time because it is near the limit of real-time and turn-based gaming requirement. Now simply run the server and then run a client. You should see something like this: [[Image:20040603_mapacman.jpg]] =Deployment= Deployment is the process whereby software is installed into an operational environment. In our case we have two different applications: * Client * Server Fortunately for our users, both the client and server are totally portable. In order to make the process of installation easier we need to think about what type of user will run each application. ==Client== The client is going to be used by a range of people whos computer skill vary greatly. Our install procedure should be targeted to this wide group of abilities. But how? This can be done by making the install very configurable, to accommodate IT types, but should otherwise default to a set of simple settings to accommodate the general gamer/user. ==Server== The server should only be targered at an above average user. However, there is an important reason for making it easy to install: to prevent the risk of opening holes to the system of the user by accident if he/she is not able to set up a firewall or correctly protect the database. My advice is to run a server only on Linux, and to set up a very restrictive firewall poking holes through only on the ports needed by the game. The steps to successfully install marauroa should be: * Check dependencies # Java 1.4 # MySQL * Install marauroa * Setup everything # Database # marauroa.ini * Install as a service We can create a makefile to do this task. ---- This document was originally written by Miguel Angel Blanch Lardin. It was then proof read by Stephen I. Even though the utmost care has been taken to make sure this document is correct and well written if you find any errors please feel free to modify the document and fix them. [[Category:Marauroa]]
摘要:
请注意,所有对gamedev的贡献均可能会被其他贡献者编辑、修改或删除。如果您不希望您的文字作品被随意编辑,请不要在此提交。
您同时也向我们承诺,您提交的内容为您自己所创作,或是复制自公共领域或类似自由来源(详情请见
Gamedev:著作权
)。
未经许可,请勿提交受著作权保护的作品!
取消
编辑帮助
(在新窗口中打开)
导航菜单
个人工具
未登录
讨论
贡献
创建账号
登录
命名空间
页面
讨论
不转换
不转换
简体
繁體
大陆简体
香港繁體
澳門繁體
大马简体
新加坡简体
臺灣正體
查看
阅读
编辑
查看历史
更多
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
工具
链入页面
相关更改
特殊页面
页面信息