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