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