#! /usr/bin/python -O12# Game Skeleton3# Copyright (C) 2003-2005 Jean-Baptiste LAMY4#5# This program is free software; you can redistribute it and/or modify6# it under the terms of the GNU General Public License as published by7# the Free Software Foundation; either version 2 of the License, or8# (at your option) any later version.9#10# This program is distributed in the hope that it will be useful,11# but WITHOUT ANY WARRANTY; without even the implied warranty of12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13# GNU General Public License for more details.14#15# You should have received a copy of the GNU General Public License16# along with this program; if not, write to the Free Software17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA1819# Soya gaming tutorial, lesson 620# Network gaming with Tofu !2122# This tutorial is the same than game_skel-5.py, but with networking support.23# Tofu also save players automatically, so if you play, then disconnect, and then connect24# again, you'll restart at the same position you were when you disconnected.2526# To run it:27# 1 - Execute tofudemo_create_level.py, in order to create and save the level28# (this script is very similar to game_skel-1.py)29# 2 - Run:30# python ./run_demo.py --single <login> for single player game31# python ./run_demo.py --server for server32# python ./run_demo.py --client <host> <login> for client3334# A bunch of import35import sys, os, os.path, math36import soya37import soya.widget as widget38import soya.sdlconst as sdlconst39import tofu, tofu.pickle_sec, soya.tofu4soya4041# Tofu provide some base classes that you must extend.42# To use Tofu with Soya, soya.tofu4soya provides these base classes with already Soya-ish43# support.4445# The first class is the Level class. soya.tofu4soya. Level already inherits from46# soya.World.474849class Level(soya.tofu4soya.Level):50"""A game level."""515253# A Player represent a human player.5455class Player(soya.tofu4soya.Player):5657# Player.__init__ is called when a NEW player is created (NOT when an existent player58# logon !).59# filename and password are the player's login and password. An additional string data60# can be passed by the client, here we ignore them.6162# Player.__init__ must create at least one mobile for the player, then add this mobile63# in a level and finally add the mobile in the player. Mobiles are a generic concept64# that includes characters (see below).6566def __init__(self, filename, password, client_side_data = ""):67soya.tofu4soya.Player.__init__(self, filename, password)6869level = tofu.Level.get("level_tofudemo")70character = Character()71character.set_xyz(216.160568237, -3.0, 213.817764282)72level.add_mobile(character)73self .add_mobile(character)747576# An Action is an action a mobile can accomplish.77# Here, we identify actions with the following constants:7879ACTION_WAIT = 080ACTION_ADVANCE = 181ACTION_ADVANCE_LEFT = 282ACTION_ADVANCE_RIGHT = 383ACTION_TURN_LEFT = 484ACTION_TURN_RIGHT = 585ACTION_GO_BACK = 686ACTION_GO_BACK_LEFT = 787ACTION_GO_BACK_RIGHT = 888ACTION_JUMP = 9899091import struct9293class Action(soya.tofu4soya.Action):94"""An action that the character can do."""95def __init__(self, action):96self.action = action9798# Optimized version, optional (default to serialization)99100#def dump (self): return struct.pack("B", self.action)101#def undump(data): return Action(struct.unpack("B", data)[0])102#undump = staticmethod(undump)103104# A state is the state of a mobile, e.g. values for the set of the attributes of the mobile105# that evolve over the time.106# soya.tofu4soya.CoordSystState provide a state object for CoordSyst, which include107# the position, rotation and scaling attributes. as it extend CoordSyst, CoordSystState108# have all CoordSyst method like set_xyz, add_vector, rotate_*, scale,...109110# Here, we also add an animation attribute, since the character's animation evolves111# over time.112113class State(soya.tofu4soya.CoordSystState):114"""A state of the character position."""115116def __init__(self, mobile = None):117soya.tofu4soya.CoordSystState.__init__(self, mobile)118119self.animation = "attente"120121# is_crucial must returns true if the state is crucial.122# Non-crucial states can be dropped, either for optimization purpose or because of123# network protocol (currently we use TCP, but in the future we may use UDP to send124# non-crucial states).125# Here we have no crucial state.126127def is_crucial(self): return 0128129# Optimized version, optional (default to serialization)130131#def dump (self): return struct.pack("i10p19f", self.round, self.animation, *self.matrix)132#def undump(data):133# self = State()134# data = struct.unpack("i10p19f", data)135# self.round = data[0]136# self.animation = data[1]137# self.matrix = data[2:]138# return self139#undump = staticmethod(undump)140141142# The controller is responsible for reading inputs and generating Actions on the client143# side.144# We extend soya.tofu4soya.LocalController; "local" means that the player is controlled145# locally, contrary to the RemoteController.146147class KeyboardController(soya.tofu4soya.LocalController):148def __init__(self, mobile):149soya.tofu4soya.LocalController.__init__(self, mobile)150151self.left_key_down = self.right_key_down = self.up_key_down = self.down_key_down = 0152self.current_action = ACTION_WAIT153154def begin_round(self):155"""Returns the next action"""156157jump = 0158159for event in soya.process_event():160if event[0] == sdlconst.KEYDOWN:161if (event[1] == sdlconst.K_q) or (event[1] == sdlconst.K_ESCAPE):162tofu.GAME_INTERFACE.end_game() # Quit the game163164elif event[1] == sdlconst.K_LSHIFT:165# Shift key is for jumping166# Contrary to other action, jump is only performed once, at the beginning of167# the jump.168jump = 1169170elif event[1] == sdlconst.K_LEFT: self.left_key_down = 1171elif event[1] == sdlconst.K_RIGHT: self.right_key_down = 1172elif event[1] == sdlconst.K_UP: self.up_key_down = 1173elif event[1] == sdlconst.K_DOWN: self.down_key_down = 1174175elif event[0] == sdlconst.KEYUP:176if event[1] == sdlconst.K_LEFT: self.left_key_down = 0177elif event[1] == sdlconst.K_RIGHT: self.right_key_down = 0178elif event[1] == sdlconst.K_UP: self.up_key_down = 0179elif event[1] == sdlconst.K_DOWN: self.down_key_down = 0180181if jump: action = ACTION_JUMP182else:183# People saying that Python doesn't have switch/select case are wrong...184# Remember this if you are coding a fighting game !185action = {186(0, 0, 1, 0) : ACTION_ADVANCE,187(1, 0, 1, 0) : ACTION_ADVANCE_LEFT,188(0, 1, 1, 0) : ACTION_ADVANCE_RIGHT,189(1, 0, 0, 0) : ACTION_TURN_LEFT,190(0, 1, 0, 0) : ACTION_TURN_RIGHT,191(0, 0, 0, 1) : ACTION_GO_BACK,192(1, 0, 0, 1) : ACTION_GO_BACK_LEFT,193(0, 1, 0, 1) : ACTION_GO_BACK_RIGHT,194}.get((self.left_key_down, self.right_key_down, self.up_key_down, self.down_key_down), ACTION_WAIT)195196if action != self.current_action:197self.current_action = action198self.mobile.doer.do_action(Action(action))199200# A mobile is anything that can move and evolve in a level. This include player characters201# but also computer-controlled objects (also named bots).202203# Here we have a single class of Mobile: character.204205# soya.tofu4soya.Mobile already inherits from World.206207class Character(soya.tofu4soya.Mobile):208"""A character in the game."""209def __init__(self):210soya.tofu4soya.Mobile.__init__(self)211212# Loads a Cal3D shape (=model)213balazar = soya.Cal3dShape.get("balazar")214215# Creates a Cal3D volume displaying the "balazar" shape216# (NB Balazar is the name of a wizard).217self.perso = soya.Cal3dVolume(self, balazar)218219# Starts playing the idling animation in loop220self.perso.animate_blend_cycle("attente")221222# The current animation223self.current_animation = ""224225self.current_action = ACTION_WAIT226227# Disable raypicking on the character itself !!!228self.solid = 0229230self.speed = soya.Vector(self)231self.rotation_speed = 0.0232233# We need radius * sqrt(2)/2 > max speed (here, 0.35)234self.radius = 0.5235self.radius_y = 1.0236self.center = soya.Point(self, 0.0, self.radius_y, 0.0)237238self.left = soya.Vector(self, -1.0, 0.0, 0.0)239self.right = soya.Vector(self, 1.0, 0.0, 0.0)240self.down = soya.Vector(self, 0.0, -1.0, 0.0)241self.up = soya.Vector(self, 0.0, 1.0, 0.0)242self.front = soya.Vector(self, 0.0, 0.0, -1.0)243self.back = soya.Vector(self, 0.0, 0.0, 1.0)244245# True is the character is jumping, i.e. speed.y > 0.0246self.jumping = 0247248# loaded is called when the mobile is loaded from a file.249# Here, we reset the current animation, because currently Soya doesn't save Cal3DVolume250# current's animation yet.251252def loaded(self):253soya.tofu4soya.Mobile.loaded(self)254self.current_animation = ""255256# do_action is called when the mobile executes the given action. It is usually called257# on the server side.258# It must return the new State of the Mobile, after the action is executed.259260def do_action(self, action):261262# Create a new State for self. By default, the state is at the same position,263# orientation and scaling that self.264265state = State(self)266267# If an action is given, we interpret it by moving the state according to the action.268# We also set the state's animation.269270if action:271self.current_action = action.action272273274if self.current_action in (ACTION_TURN_LEFT, ACTION_ADVANCE_LEFT, ACTION_GO_BACK_LEFT):275state.rotate_lateral( 4.0)276state.animation = "tourneG"277elif self.current_action in (ACTION_TURN_RIGHT, ACTION_ADVANCE_RIGHT, ACTION_GO_BACK_RIGHT):278state.rotate_lateral(-4.0)279state.animation = "tourneD"280281if self.current_action in (ACTION_ADVANCE, ACTION_ADVANCE_LEFT, ACTION_ADVANCE_RIGHT):282state.shift(0.0, 0.0, -0.25)283state.animation = "marche"284elif self.current_action in (ACTION_GO_BACK, ACTION_GO_BACK_LEFT, ACTION_GO_BACK_RIGHT):285state.shift(0.0, 0.0, 0.06)286state.animation = "recule"287288# Now, we perform collision detection.289# state_center is roughly the center of the character at the new state's position.290# Then we create a raypicking context.291# Detection collision is similar to the previous game_skel, except that "state"292# replaces "new_center"293294state_center = soya.Point(state, 0.0, self.radius_y, 0.0)295context = scene.RaypickContext(state_center, max(self.radius, 0.1 + self.radius_y))296297# Gets the ground, and check if the character is falling298299r = context.raypick(state_center, self.down, 0.1 + self.radius_y, 1, 1)300301if r and not self.jumping:302303# Puts the character on the ground304# If the character is jumping, we do not put him on the ground !305306ground, ground_normal = r307ground.convert_to(self)308self.speed.y = ground.y309310# Jumping is only possible if we are on ground311312if action and (action.action == ACTION_JUMP):313self.jumping = 1314self.speed.y = 0.5315316else:317318# No ground => start falling319320self.speed.y = max(self.speed.y - 0.02, -0.25)321state.animation = "chute"322323if self.speed.y < 0.0: self.jumping = 0324325# Add the current vertical speed to the state.326327state.y += self.speed.y328329# Check for walls.330331for vec in (self.left, self.right, self.front, self.back, self.up):332r = context.raypick(state_center, vec, self.radius, 1, 1)333if r:334# The ray encounters a wall => the character cannot perform the planned movement.335# We compute a correction vector, and add it to the state.336337collision, wall_normal = r338hypo = vec.length() * self.radius - (state_center >> collision).length()339correction = wall_normal * hypo340341# Theorical formula, but more complex and identical result342#angle = (180.0 - vec.angle_to(wall_normal)) / 180.0 * math.pi343#correction = wall_normal * hypo * math.cos(angle)344345state.add_vector(correction)346347# Returns the resulting state.348349self.doer.action_done(state)350351# set_state is called when the mobile's state change, due to the execution of an352# action. It is called BOTH server-side and client-side.353354def set_state(self, state):355# The super implementation take care of the position, rotation and scaling stuff.356357soya.tofu4soya.Mobile.set_state(self, state)358359# Play the new animation.360if self.current_animation != state.animation:361# Stops previous animation362if self.current_animation: self.perso.animate_clear_cycle(self.current_animation, 0.2)363364# Starts the new one365self.perso.animate_blend_cycle(state.animation, 1.0, 0.2)366367self.current_animation = state.animation368369# control_owned is called on the client-side when the player gets the control of the370# mobile (i.e. the mobile is not a bot).371372def control_owned(self):373soya.tofu4soya.Mobile.control_owned(self)374375# Use our KeyboardController instead of Tofu's default LocalController.376377self.controller = KeyboardController(self)378379# Create a camera traveling, in order to make the camera look toward this character.380381traveling = soya.ThirdPersonTraveling(self)382traveling.distance = 5.0383tofu.GAME_INTERFACE.camera.add_traveling(traveling)384tofu.GAME_INTERFACE.camera.zap()385386387# GameInterface is the interface of the game.388389class GameInterface(soya.tofu4soya.GameInterface):390def __init__(self):391soya.tofu4soya.GameInterface.__init__(self)392393soya.init()394395self.player_character = None396397# Creates a traveling camera in the scene, with a default look-toward-nothing398# traveling.399400self.camera = soya.TravelingCamera(scene)401self.camera.back = 70.0402self.camera.add_traveling(soya.FixTraveling(soya.Point(), soya.Vector(None, 0.0, 0.0, -1.0)))403404soya.set_root_widget(soya.widget.Group())405soya.root_widget.add(self.camera)406407# ready is called when the client has contacted the server and anything is ready.408# In particular, we can now call self.notifier.login_player to logon the server.409# Additional arguments to self.notifier.login_player will be pickled and sent to the410# server, and then made available to the player (see client_side_data above).411412def ready(self, notifier):413soya.tofu4soya.GameInterface.ready(self, notifier)414415login, password = sys.argv[-1], "test"416self.notifier.login_player(login, password)417418419# Define data path (=where to find models, textures, ...)420421HERE = os.path.dirname(sys.argv[0])422soya.path.append(os.path.join(HERE, "data"))423tofu.path.append(os.path.join(HERE, "data"))424425# Create the scene (a world with no parent)426427scene = soya.World()428429# Inits Tofu with our classes.430431soya.tofu4soya.init(GameInterface, Player, Level, Action, State, Character)432433# Use cPickle for serializing local file (faster), and Cerealizer for network.434435tofu.enable_pickle (1, 0)436tofu.enable_cerealizer(0, 1)437438# Make our classes safe for Cerealizer439440import cerealizer, soya.cerealizer4soya441cerealizer.register_class(Action)442cerealizer.register_class(State)443cerealizer.register_class(KeyboardController)444cerealizer.register_class(Character)445cerealizer.register_class(Level)446447448449# To use Jelly (buggy :-( )450# soya.tofu4soya.allow_jelly()451# tofu.allow_jelly()452# tofu.allow_jelly(453# Action,454# Character,455# KeyboardController,456# Level,457# State,458# )459460461# For security reason, there is a maximum to the size of the transmitted serialized462# object, which default to 99999. You may need to increase this value, e.g. up to463# 1MB (this is not needed for the demo, thus the following code is commented).464465#import tofu.client466#tofu.client.MAX_LENGTH = 1000000467468469# This function makes all Soya pickleable classes safe for pickle_sec.470471472if __name__ == "__main__":473print """Don't run me, run run_tofudemo.py instead !!!"""474475