Sharedwww / talks / 2006-05-09-sage-digipen / tutorial / tofudemo.pyOpen in CoCalc
Author: William A. Stein
1
#! /usr/bin/python -O
2
3
# Game Skeleton
4
# Copyright (C) 2003-2005 Jean-Baptiste LAMY
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
20
# Soya gaming tutorial, lesson 6
21
# Network gaming with Tofu !
22
23
# This tutorial is the same than game_skel-5.py, but with networking support.
24
# Tofu also save players automatically, so if you play, then disconnect, and then connect
25
# again, you'll restart at the same position you were when you disconnected.
26
27
# To run it:
28
# 1 - Execute tofudemo_create_level.py, in order to create and save the level
29
# (this script is very similar to game_skel-1.py)
30
# 2 - Run:
31
# python ./run_demo.py --single <login> for single player game
32
# python ./run_demo.py --server for server
33
# python ./run_demo.py --client <host> <login> for client
34
35
# A bunch of import
36
import sys, os, os.path, math
37
import soya
38
import soya.widget as widget
39
import soya.sdlconst as sdlconst
40
import tofu, tofu.pickle_sec, soya.tofu4soya
41
42
# Tofu provide some base classes that you must extend.
43
# To use Tofu with Soya, soya.tofu4soya provides these base classes with already Soya-ish
44
# support.
45
46
# The first class is the Level class. soya.tofu4soya. Level already inherits from
47
# soya.World.
48
49
50
class Level(soya.tofu4soya.Level):
51
"""A game level."""
52
53
54
# A Player represent a human player.
55
56
class Player(soya.tofu4soya.Player):
57
58
# Player.__init__ is called when a NEW player is created (NOT when an existent player
59
# logon !).
60
# filename and password are the player's login and password. An additional string data
61
# can be passed by the client, here we ignore them.
62
63
# Player.__init__ must create at least one mobile for the player, then add this mobile
64
# in a level and finally add the mobile in the player. Mobiles are a generic concept
65
# that includes characters (see below).
66
67
def __init__(self, filename, password, client_side_data = ""):
68
soya.tofu4soya.Player.__init__(self, filename, password)
69
70
level = tofu.Level.get("level_tofudemo")
71
character = Character()
72
character.set_xyz(216.160568237, -3.0, 213.817764282)
73
level.add_mobile(character)
74
self .add_mobile(character)
75
76
77
# An Action is an action a mobile can accomplish.
78
# Here, we identify actions with the following constants:
79
80
ACTION_WAIT = 0
81
ACTION_ADVANCE = 1
82
ACTION_ADVANCE_LEFT = 2
83
ACTION_ADVANCE_RIGHT = 3
84
ACTION_TURN_LEFT = 4
85
ACTION_TURN_RIGHT = 5
86
ACTION_GO_BACK = 6
87
ACTION_GO_BACK_LEFT = 7
88
ACTION_GO_BACK_RIGHT = 8
89
ACTION_JUMP = 9
90
91
92
import struct
93
94
class Action(soya.tofu4soya.Action):
95
"""An action that the character can do."""
96
def __init__(self, action):
97
self.action = action
98
99
# Optimized version, optional (default to serialization)
100
101
#def dump (self): return struct.pack("B", self.action)
102
#def undump(data): return Action(struct.unpack("B", data)[0])
103
#undump = staticmethod(undump)
104
105
# A state is the state of a mobile, e.g. values for the set of the attributes of the mobile
106
# that evolve over the time.
107
# soya.tofu4soya.CoordSystState provide a state object for CoordSyst, which include
108
# the position, rotation and scaling attributes. as it extend CoordSyst, CoordSystState
109
# have all CoordSyst method like set_xyz, add_vector, rotate_*, scale,...
110
111
# Here, we also add an animation attribute, since the character's animation evolves
112
# over time.
113
114
class State(soya.tofu4soya.CoordSystState):
115
"""A state of the character position."""
116
117
def __init__(self, mobile = None):
118
soya.tofu4soya.CoordSystState.__init__(self, mobile)
119
120
self.animation = "attente"
121
122
# is_crucial must returns true if the state is crucial.
123
# Non-crucial states can be dropped, either for optimization purpose or because of
124
# network protocol (currently we use TCP, but in the future we may use UDP to send
125
# non-crucial states).
126
# Here we have no crucial state.
127
128
def is_crucial(self): return 0
129
130
# Optimized version, optional (default to serialization)
131
132
#def dump (self): return struct.pack("i10p19f", self.round, self.animation, *self.matrix)
133
#def undump(data):
134
# self = State()
135
# data = struct.unpack("i10p19f", data)
136
# self.round = data[0]
137
# self.animation = data[1]
138
# self.matrix = data[2:]
139
# return self
140
#undump = staticmethod(undump)
141
142
143
# The controller is responsible for reading inputs and generating Actions on the client
144
# side.
145
# We extend soya.tofu4soya.LocalController; "local" means that the player is controlled
146
# locally, contrary to the RemoteController.
147
148
class KeyboardController(soya.tofu4soya.LocalController):
149
def __init__(self, mobile):
150
soya.tofu4soya.LocalController.__init__(self, mobile)
151
152
self.left_key_down = self.right_key_down = self.up_key_down = self.down_key_down = 0
153
self.current_action = ACTION_WAIT
154
155
def begin_round(self):
156
"""Returns the next action"""
157
158
jump = 0
159
160
for event in soya.process_event():
161
if event[0] == sdlconst.KEYDOWN:
162
if (event[1] == sdlconst.K_q) or (event[1] == sdlconst.K_ESCAPE):
163
tofu.GAME_INTERFACE.end_game() # Quit the game
164
165
elif event[1] == sdlconst.K_LSHIFT:
166
# Shift key is for jumping
167
# Contrary to other action, jump is only performed once, at the beginning of
168
# the jump.
169
jump = 1
170
171
elif event[1] == sdlconst.K_LEFT: self.left_key_down = 1
172
elif event[1] == sdlconst.K_RIGHT: self.right_key_down = 1
173
elif event[1] == sdlconst.K_UP: self.up_key_down = 1
174
elif event[1] == sdlconst.K_DOWN: self.down_key_down = 1
175
176
elif event[0] == sdlconst.KEYUP:
177
if event[1] == sdlconst.K_LEFT: self.left_key_down = 0
178
elif event[1] == sdlconst.K_RIGHT: self.right_key_down = 0
179
elif event[1] == sdlconst.K_UP: self.up_key_down = 0
180
elif event[1] == sdlconst.K_DOWN: self.down_key_down = 0
181
182
if jump: action = ACTION_JUMP
183
else:
184
# People saying that Python doesn't have switch/select case are wrong...
185
# Remember this if you are coding a fighting game !
186
action = {
187
(0, 0, 1, 0) : ACTION_ADVANCE,
188
(1, 0, 1, 0) : ACTION_ADVANCE_LEFT,
189
(0, 1, 1, 0) : ACTION_ADVANCE_RIGHT,
190
(1, 0, 0, 0) : ACTION_TURN_LEFT,
191
(0, 1, 0, 0) : ACTION_TURN_RIGHT,
192
(0, 0, 0, 1) : ACTION_GO_BACK,
193
(1, 0, 0, 1) : ACTION_GO_BACK_LEFT,
194
(0, 1, 0, 1) : ACTION_GO_BACK_RIGHT,
195
}.get((self.left_key_down, self.right_key_down, self.up_key_down, self.down_key_down), ACTION_WAIT)
196
197
if action != self.current_action:
198
self.current_action = action
199
self.mobile.doer.do_action(Action(action))
200
201
# A mobile is anything that can move and evolve in a level. This include player characters
202
# but also computer-controlled objects (also named bots).
203
204
# Here we have a single class of Mobile: character.
205
206
# soya.tofu4soya.Mobile already inherits from World.
207
208
class Character(soya.tofu4soya.Mobile):
209
"""A character in the game."""
210
def __init__(self):
211
soya.tofu4soya.Mobile.__init__(self)
212
213
# Loads a Cal3D shape (=model)
214
balazar = soya.Cal3dShape.get("balazar")
215
216
# Creates a Cal3D volume displaying the "balazar" shape
217
# (NB Balazar is the name of a wizard).
218
self.perso = soya.Cal3dVolume(self, balazar)
219
220
# Starts playing the idling animation in loop
221
self.perso.animate_blend_cycle("attente")
222
223
# The current animation
224
self.current_animation = ""
225
226
self.current_action = ACTION_WAIT
227
228
# Disable raypicking on the character itself !!!
229
self.solid = 0
230
231
self.speed = soya.Vector(self)
232
self.rotation_speed = 0.0
233
234
# We need radius * sqrt(2)/2 > max speed (here, 0.35)
235
self.radius = 0.5
236
self.radius_y = 1.0
237
self.center = soya.Point(self, 0.0, self.radius_y, 0.0)
238
239
self.left = soya.Vector(self, -1.0, 0.0, 0.0)
240
self.right = soya.Vector(self, 1.0, 0.0, 0.0)
241
self.down = soya.Vector(self, 0.0, -1.0, 0.0)
242
self.up = soya.Vector(self, 0.0, 1.0, 0.0)
243
self.front = soya.Vector(self, 0.0, 0.0, -1.0)
244
self.back = soya.Vector(self, 0.0, 0.0, 1.0)
245
246
# True is the character is jumping, i.e. speed.y > 0.0
247
self.jumping = 0
248
249
# loaded is called when the mobile is loaded from a file.
250
# Here, we reset the current animation, because currently Soya doesn't save Cal3DVolume
251
# current's animation yet.
252
253
def loaded(self):
254
soya.tofu4soya.Mobile.loaded(self)
255
self.current_animation = ""
256
257
# do_action is called when the mobile executes the given action. It is usually called
258
# on the server side.
259
# It must return the new State of the Mobile, after the action is executed.
260
261
def do_action(self, action):
262
263
# Create a new State for self. By default, the state is at the same position,
264
# orientation and scaling that self.
265
266
state = State(self)
267
268
# If an action is given, we interpret it by moving the state according to the action.
269
# We also set the state's animation.
270
271
if action:
272
self.current_action = action.action
273
274
275
if self.current_action in (ACTION_TURN_LEFT, ACTION_ADVANCE_LEFT, ACTION_GO_BACK_LEFT):
276
state.rotate_lateral( 4.0)
277
state.animation = "tourneG"
278
elif self.current_action in (ACTION_TURN_RIGHT, ACTION_ADVANCE_RIGHT, ACTION_GO_BACK_RIGHT):
279
state.rotate_lateral(-4.0)
280
state.animation = "tourneD"
281
282
if self.current_action in (ACTION_ADVANCE, ACTION_ADVANCE_LEFT, ACTION_ADVANCE_RIGHT):
283
state.shift(0.0, 0.0, -0.25)
284
state.animation = "marche"
285
elif self.current_action in (ACTION_GO_BACK, ACTION_GO_BACK_LEFT, ACTION_GO_BACK_RIGHT):
286
state.shift(0.0, 0.0, 0.06)
287
state.animation = "recule"
288
289
# Now, we perform collision detection.
290
# state_center is roughly the center of the character at the new state's position.
291
# Then we create a raypicking context.
292
# Detection collision is similar to the previous game_skel, except that "state"
293
# replaces "new_center"
294
295
state_center = soya.Point(state, 0.0, self.radius_y, 0.0)
296
context = scene.RaypickContext(state_center, max(self.radius, 0.1 + self.radius_y))
297
298
# Gets the ground, and check if the character is falling
299
300
r = context.raypick(state_center, self.down, 0.1 + self.radius_y, 1, 1)
301
302
if r and not self.jumping:
303
304
# Puts the character on the ground
305
# If the character is jumping, we do not put him on the ground !
306
307
ground, ground_normal = r
308
ground.convert_to(self)
309
self.speed.y = ground.y
310
311
# Jumping is only possible if we are on ground
312
313
if action and (action.action == ACTION_JUMP):
314
self.jumping = 1
315
self.speed.y = 0.5
316
317
else:
318
319
# No ground => start falling
320
321
self.speed.y = max(self.speed.y - 0.02, -0.25)
322
state.animation = "chute"
323
324
if self.speed.y < 0.0: self.jumping = 0
325
326
# Add the current vertical speed to the state.
327
328
state.y += self.speed.y
329
330
# Check for walls.
331
332
for vec in (self.left, self.right, self.front, self.back, self.up):
333
r = context.raypick(state_center, vec, self.radius, 1, 1)
334
if r:
335
# The ray encounters a wall => the character cannot perform the planned movement.
336
# We compute a correction vector, and add it to the state.
337
338
collision, wall_normal = r
339
hypo = vec.length() * self.radius - (state_center >> collision).length()
340
correction = wall_normal * hypo
341
342
# Theorical formula, but more complex and identical result
343
#angle = (180.0 - vec.angle_to(wall_normal)) / 180.0 * math.pi
344
#correction = wall_normal * hypo * math.cos(angle)
345
346
state.add_vector(correction)
347
348
# Returns the resulting state.
349
350
self.doer.action_done(state)
351
352
# set_state is called when the mobile's state change, due to the execution of an
353
# action. It is called BOTH server-side and client-side.
354
355
def set_state(self, state):
356
# The super implementation take care of the position, rotation and scaling stuff.
357
358
soya.tofu4soya.Mobile.set_state(self, state)
359
360
# Play the new animation.
361
if self.current_animation != state.animation:
362
# Stops previous animation
363
if self.current_animation: self.perso.animate_clear_cycle(self.current_animation, 0.2)
364
365
# Starts the new one
366
self.perso.animate_blend_cycle(state.animation, 1.0, 0.2)
367
368
self.current_animation = state.animation
369
370
# control_owned is called on the client-side when the player gets the control of the
371
# mobile (i.e. the mobile is not a bot).
372
373
def control_owned(self):
374
soya.tofu4soya.Mobile.control_owned(self)
375
376
# Use our KeyboardController instead of Tofu's default LocalController.
377
378
self.controller = KeyboardController(self)
379
380
# Create a camera traveling, in order to make the camera look toward this character.
381
382
traveling = soya.ThirdPersonTraveling(self)
383
traveling.distance = 5.0
384
tofu.GAME_INTERFACE.camera.add_traveling(traveling)
385
tofu.GAME_INTERFACE.camera.zap()
386
387
388
# GameInterface is the interface of the game.
389
390
class GameInterface(soya.tofu4soya.GameInterface):
391
def __init__(self):
392
soya.tofu4soya.GameInterface.__init__(self)
393
394
soya.init()
395
396
self.player_character = None
397
398
# Creates a traveling camera in the scene, with a default look-toward-nothing
399
# traveling.
400
401
self.camera = soya.TravelingCamera(scene)
402
self.camera.back = 70.0
403
self.camera.add_traveling(soya.FixTraveling(soya.Point(), soya.Vector(None, 0.0, 0.0, -1.0)))
404
405
soya.set_root_widget(soya.widget.Group())
406
soya.root_widget.add(self.camera)
407
408
# ready is called when the client has contacted the server and anything is ready.
409
# In particular, we can now call self.notifier.login_player to logon the server.
410
# Additional arguments to self.notifier.login_player will be pickled and sent to the
411
# server, and then made available to the player (see client_side_data above).
412
413
def ready(self, notifier):
414
soya.tofu4soya.GameInterface.ready(self, notifier)
415
416
login, password = sys.argv[-1], "test"
417
self.notifier.login_player(login, password)
418
419
420
# Define data path (=where to find models, textures, ...)
421
422
HERE = os.path.dirname(sys.argv[0])
423
soya.path.append(os.path.join(HERE, "data"))
424
tofu.path.append(os.path.join(HERE, "data"))
425
426
# Create the scene (a world with no parent)
427
428
scene = soya.World()
429
430
# Inits Tofu with our classes.
431
432
soya.tofu4soya.init(GameInterface, Player, Level, Action, State, Character)
433
434
# Use cPickle for serializing local file (faster), and Cerealizer for network.
435
436
tofu.enable_pickle (1, 0)
437
tofu.enable_cerealizer(0, 1)
438
439
# Make our classes safe for Cerealizer
440
441
import cerealizer, soya.cerealizer4soya
442
cerealizer.register_class(Action)
443
cerealizer.register_class(State)
444
cerealizer.register_class(KeyboardController)
445
cerealizer.register_class(Character)
446
cerealizer.register_class(Level)
447
448
449
450
# To use Jelly (buggy :-( )
451
# soya.tofu4soya.allow_jelly()
452
# tofu.allow_jelly()
453
# tofu.allow_jelly(
454
# Action,
455
# Character,
456
# KeyboardController,
457
# Level,
458
# State,
459
# )
460
461
462
# For security reason, there is a maximum to the size of the transmitted serialized
463
# object, which default to 99999. You may need to increase this value, e.g. up to
464
# 1MB (this is not needed for the demo, thus the following code is commented).
465
466
#import tofu.client
467
#tofu.client.MAX_LENGTH = 1000000
468
469
470
# This function makes all Soya pickleable classes safe for pickle_sec.
471
472
473
if __name__ == "__main__":
474
print """Don't run me, run run_tofudemo.py instead !!!"""
475