Sharedwww / talks / 2006-05-09-sage-digipen / tutorial / pudding-game_skel-5.pyOpen in CoCalc
Author: William A. Stein
1
#! /usr/bin/python -O
2
3
# Game Skeleton
4
# Copyright (C) 2003-2004 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 5
21
# Adding jumping
22
23
# New stuff is in the Controler and Character class
24
25
# A bunch of import
26
import sys, os, os.path, math
27
import soya
28
import soya.widget as widget
29
import soya.sdlconst as sdlconst
30
31
import soya.pudding as pudding
32
import soya.pudding.ext.fpslabel
33
import soya.pudding.ext.meter
34
35
# Inits Soya
36
soya.init()
37
soya.path.append(os.path.join(os.path.dirname(sys.argv[0]), "data"))
38
39
pudding.init()
40
41
42
class Level(soya.World):
43
"""A game level.
44
Level is a subclass of soya.World."""
45
46
47
class Action:
48
"""An action that the character can do."""
49
def __init__(self, action):
50
self.action = action
51
52
# The available actions
53
ACTION_WAIT = 0
54
ACTION_ADVANCE = 1
55
ACTION_ADVANCE_LEFT = 2
56
ACTION_ADVANCE_RIGHT = 3
57
ACTION_TURN_LEFT = 4
58
ACTION_TURN_RIGHT = 5
59
ACTION_GO_BACK = 6
60
ACTION_GO_BACK_LEFT = 7
61
ACTION_GO_BACK_RIGHT = 8
62
ACTION_JUMP = 9
63
64
65
class KeyboardControler:
66
"""A controler is an object that gives orders to a character.
67
Here, we define a keyboard based controler, but there may be mouse-based or IA-based
68
controlers.
69
Notice that the unique method is called "next", which allows to use Python generator
70
as controller."""
71
def __init__(self):
72
self.left_key_down = self.right_key_down = self.up_key_down = self.down_key_down = 0
73
74
def next(self):
75
"""Returns the next action"""
76
jump = 0
77
78
# get the events that have not been captured by pudding
79
for event in soya.IDLER.events:
80
if event[0] == sdlconst.KEYDOWN:
81
if (event[1] == sdlconst.K_q) or (event[1] == sdlconst.K_ESCAPE):
82
sys.exit() # Quit the game
83
84
elif event[1] == sdlconst.K_LSHIFT:
85
# Shift key is for jumping
86
# Contrary to other action, jump is only performed once, at the beginning of
87
# the jump.
88
jump = 1
89
90
elif event[1] == sdlconst.K_LEFT: self.left_key_down = 1
91
elif event[1] == sdlconst.K_RIGHT: self.right_key_down = 1
92
elif event[1] == sdlconst.K_UP: self.up_key_down = 1
93
elif event[1] == sdlconst.K_DOWN: self.down_key_down = 1
94
95
elif event[0] == sdlconst.KEYUP:
96
if event[1] == sdlconst.K_LEFT: self.left_key_down = 0
97
elif event[1] == sdlconst.K_RIGHT: self.right_key_down = 0
98
elif event[1] == sdlconst.K_UP: self.up_key_down = 0
99
elif event[1] == sdlconst.K_DOWN: self.down_key_down = 0
100
101
if jump: return Action(ACTION_JUMP)
102
103
# People saying that Python doesn't have switch/select case are wrong...
104
# Remember this if you are coding a fighting game !
105
return Action({
106
(0, 0, 1, 0) : ACTION_ADVANCE,
107
(1, 0, 1, 0) : ACTION_ADVANCE_LEFT,
108
(0, 1, 1, 0) : ACTION_ADVANCE_RIGHT,
109
(1, 0, 0, 0) : ACTION_TURN_LEFT,
110
(0, 1, 0, 0) : ACTION_TURN_RIGHT,
111
(0, 0, 0, 1) : ACTION_GO_BACK,
112
(1, 0, 0, 1) : ACTION_GO_BACK_LEFT,
113
(0, 1, 0, 1) : ACTION_GO_BACK_RIGHT,
114
}.get((self.left_key_down, self.right_key_down, self.up_key_down, self.down_key_down), ACTION_WAIT))
115
116
117
class Character(soya.World):
118
"""A character in the game."""
119
def __init__(self, parent, controler):
120
soya.World.__init__(self, parent)
121
122
# Loads a Cal3D shape (=model)
123
balazar = soya.Cal3dShape.get("balazar")
124
125
# Creates a Cal3D volume displaying the "balazar" shape
126
# (NB Balazar is the name of a wizard).
127
self.perso = soya.Cal3dVolume(self, balazar)
128
129
# Starts playing the idling animation in loop
130
self.perso.animate_blend_cycle("attente")
131
132
# The current animation
133
self.current_animation = "attente"
134
135
# Disable raypicking on the character itself !!!
136
self.solid = 0
137
138
self.controler = controler
139
self.speed = soya.Vector(self)
140
self.rotation_speed = 0.0
141
142
# We need radius * sqrt(2)/2 < max speed (here, 0.35)
143
self.radius = 0.5
144
self.radius_y = 1.0
145
self.center = soya.Point(self, 0.0, self.radius_y, 0.0)
146
147
self.left = soya.Vector(self, -1.0, 0.0, 0.0)
148
self.right = soya.Vector(self, 1.0, 0.0, 0.0)
149
self.down = soya.Vector(self, 0.0, -1.0, 0.0)
150
self.up = soya.Vector(self, 0.0, 1.0, 0.0)
151
self.front = soya.Vector(self, 0.0, 0.0, -1.0)
152
self.back = soya.Vector(self, 0.0, 0.0, 1.0)
153
154
# True is the character is jumping, i.e. speed.y > 0.0
155
self.jumping = 0
156
157
def play_animation(self, animation):
158
if self.current_animation != animation:
159
# Stops previous animation
160
self.perso.animate_clear_cycle(self.current_animation, 0.2)
161
162
# Starts the new one
163
self.perso.animate_blend_cycle(animation, 1.0, 0.2)
164
165
self.current_animation = animation
166
167
def begin_round(self):
168
self.begin_action(self.controler.next())
169
soya.World.begin_round(self)
170
171
def begin_action(self, action):
172
# Reset
173
self.speed.x = self.speed.z = self.rotation_speed = 0.0
174
175
# If the haracter is jumping, we don't want to reset speed.y to 0.0 !!!
176
if (not self.jumping) and self.speed.y > 0.0: self.speed.y = 0.0
177
178
animation = "attente"
179
180
# Determine the character rotation
181
if action.action in (ACTION_TURN_LEFT, ACTION_ADVANCE_LEFT, ACTION_GO_BACK_LEFT):
182
self.rotation_speed = 4.0
183
animation = "tourneG"
184
elif action.action in (ACTION_TURN_RIGHT, ACTION_ADVANCE_RIGHT, ACTION_GO_BACK_RIGHT):
185
self.rotation_speed = -4.0
186
animation = "tourneD"
187
188
# Determine the character speed
189
if action.action in (ACTION_ADVANCE, ACTION_ADVANCE_LEFT, ACTION_ADVANCE_RIGHT):
190
self.speed.z = -0.25
191
animation = "marche"
192
elif action.action in (ACTION_GO_BACK, ACTION_GO_BACK_LEFT, ACTION_GO_BACK_RIGHT):
193
self.speed.z = 0.06
194
animation = "recule"
195
196
new_center = self.center + self.speed
197
context = scene.RaypickContext(new_center, max(self.radius, 0.1 + self.radius_y))
198
199
# Gets the ground, and check if the character is falling
200
r = context.raypick(new_center, self.down, 0.1 + self.radius_y, 3)
201
202
if r and not self.jumping:
203
# Puts the character on the ground
204
# If the character is jumping, we do not put him on the ground !
205
ground, ground_normal = r
206
ground.convert_to(self)
207
self.speed.y = ground.y
208
209
# Jumping is only possible if we are on ground
210
if action.action == ACTION_JUMP:
211
self.jumping = 1
212
self.speed.y = 0.5
213
214
else:
215
# No ground => start falling
216
# Test the fall with the pit behind the second house
217
self.speed.y = max(self.speed.y - 0.02, -0.25)
218
animation = "chute"
219
220
# If the vertical speed is negative, the jump is over
221
if self.speed.y < 0.0: self.jumping = 0
222
223
new_center = self.center + self.speed
224
225
# The movement (defined by the speed vector) may be impossible if the character
226
# would encounter a wall.
227
228
for vec in (self.left, self.right, self.front, self.back, self.up):
229
r = context.raypick(new_center, vec, self.radius, 3)
230
if r:
231
# The ray encounters a wall => the character cannot perform the planned movement.
232
# We compute a correction vector, and add it to the speed vector, as well as to
233
# new_center (for the following raypicks ; remember that
234
# new_center = self.center + self.speed, so if speed has changed, we must update
235
# it).
236
237
collision, wall_normal = r
238
hypo = vec.length() * self.radius - (new_center >> collision).length()
239
correction = wall_normal * hypo
240
241
# Theorical formula, but more complex and identical result
242
#angle = (180.0 - vec.angle_to(wall_normal)) / 180.0 * math.pi
243
#correction = wall_normal * hypo * math.cos(angle)
244
245
self.speed.add_vector(correction)
246
new_center.add_vector(correction)
247
248
self.play_animation(animation)
249
250
def advance_time(self, proportion):
251
soya.World.advance_time(self, proportion)
252
253
self.add_mul_vector(proportion, self.speed)
254
self.rotate_lateral(proportion * self.rotation_speed)
255
256
257
# Create the scene (a world with no parent)
258
scene = soya.World()
259
260
# Loads the level, and put it in the scene
261
level = soya.World.get("level_demo")
262
scene.add(level)
263
264
# Creates a character in the level, with a keyboard controler
265
character = Character(level, KeyboardControler())
266
character.set_xyz(216.160568237, -7.93332195282, 213.817764282)
267
268
# Creates a Tomb Raider-like camera in the scene
269
camera = soya.TravelingCamera(scene)
270
traveling = soya.ThirdPersonTraveling(character)
271
traveling.distance = 5.0
272
camera.add_traveling(traveling)
273
camera.zap()
274
camera.back = 70.0
275
276
# Creates a widget group, containing the camera and a label showing the FPS.
277
278
soya.set_root_widget(pudding.core.RootWidget())
279
soya.root_widget.add_child(camera)
280
281
pudding.ext.fpslabel.FPSLabel(soya.root_widget, position = pudding.TOP_RIGHT)
282
283
health_bar = pudding.ext.meter.MeterValueLabel(soya.root_widget, "health:",
284
left = 10, top = 10, width = 200,
285
height = 20)
286
health_bar.anchors = pudding.ANCHOR_TOP_LEFT
287
health_bar.meter.user_change = False
288
health_bar.meter.border_color = (0, 0, 0, 1)
289
health_bar.label.color = health_bar.meter.border_color
290
291
logo = pudding.control.Logo(soya.root_widget, 'little-dunk.png')
292
293
button = pudding.control.Button(soya.root_widget, 'Quit', left = 10, width = 50, height = 40)
294
button.set_pos_bottom_right(bottom = 10)
295
button.anchors = pudding.ANCHOR_BOTTOM | pudding.ANCHOR_LEFT
296
button.on_click = sys.exit
297
298
# Creates and run an "idler" (=an object that manage time and regulate FPS)
299
# By default, FPS is locked at 40.
300
pudding.idler.Idler(scene).idle()
301