Sharedwww / talks / 2006-05-09-sage-digipen / tutorial / game_skel-3.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 3
21
# Adding character-level interaction (collision, ...)
22
23
# New stuff is in the 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
class Level(soya.World):
40
"""A game level.
41
Level is a subclass of soya.World."""
42
43
44
class Action:
45
"""An action that the character can do."""
46
def __init__(self, action):
47
self.action = action
48
49
# The available actions
50
ACTION_WAIT = 0
51
ACTION_ADVANCE = 1
52
ACTION_ADVANCE_LEFT = 2
53
ACTION_ADVANCE_RIGHT = 3
54
ACTION_TURN_LEFT = 4
55
ACTION_TURN_RIGHT = 5
56
ACTION_GO_BACK = 6
57
ACTION_GO_BACK_LEFT = 7
58
ACTION_GO_BACK_RIGHT = 8
59
60
61
class KeyboardController:
62
"""A controller is an object that gives orders to a character.
63
Here, we define a keyboard based controller, but there may be mouse-based or IA-based
64
controllers.
65
Notice that the unique method is called "next", which allows to use Python generator
66
as controller."""
67
def __init__(self):
68
self.left_key_down = self.right_key_down = self.up_key_down = self.down_key_down = 0
69
70
def next(self):
71
"""Returns the next action"""
72
for event in soya.process_event():
73
if event[0] == sdlconst.KEYDOWN:
74
if (event[1] == sdlconst.K_q) or (event[1] == sdlconst.K_ESCAPE):
75
sys.exit() # Quit the game
76
77
elif event[1] == sdlconst.K_LEFT: self.left_key_down = 1
78
elif event[1] == sdlconst.K_RIGHT: self.right_key_down = 1
79
elif event[1] == sdlconst.K_UP: self.up_key_down = 1
80
elif event[1] == sdlconst.K_DOWN: self.down_key_down = 1
81
82
elif event[0] == sdlconst.KEYUP:
83
if event[1] == sdlconst.K_LEFT: self.left_key_down = 0
84
elif event[1] == sdlconst.K_RIGHT: self.right_key_down = 0
85
elif event[1] == sdlconst.K_UP: self.up_key_down = 0
86
elif event[1] == sdlconst.K_DOWN: self.down_key_down = 0
87
88
# People saying that Python doesn't have switch/select case are wrong...
89
# Remember this if you are coding a fighting game !
90
return Action({
91
(0, 0, 1, 0) : ACTION_ADVANCE,
92
(1, 0, 1, 0) : ACTION_ADVANCE_LEFT,
93
(0, 1, 1, 0) : ACTION_ADVANCE_RIGHT,
94
(1, 0, 0, 0) : ACTION_TURN_LEFT,
95
(0, 1, 0, 0) : ACTION_TURN_RIGHT,
96
(0, 0, 0, 1) : ACTION_GO_BACK,
97
(1, 0, 0, 1) : ACTION_GO_BACK_LEFT,
98
(0, 1, 0, 1) : ACTION_GO_BACK_RIGHT,
99
}.get((self.left_key_down, self.right_key_down, self.up_key_down, self.down_key_down), ACTION_WAIT))
100
101
102
class Character(soya.World):
103
"""A character in the game."""
104
def __init__(self, parent, controller):
105
soya.World.__init__(self, parent)
106
self.set_shape(soya.Shape.get("cube"))
107
108
# Disable raypicking on the character itself !!!
109
self.solid = 0
110
111
self.controller = controller
112
self.speed = soya.Vector(self)
113
self.rotation_speed = 0.0
114
115
# This variable are used for collision detection.
116
# radius is the radius of an approximative bounding sphere around the character,
117
# and radius_y is the same but in Y (vertical) direction only (a character is
118
# usually higher that large ;-).
119
# We need radius * sqrt(2)/2 < max speed (here, 0.35)
120
self.radius = 0.5
121
self.radius_y = 1.0
122
123
# Center is a center point of the character (center of the previously mentioned
124
# bounding sphere)
125
self.center = soya.Point(self, 0.0, self.radius_y, 0.0)
126
127
# These vectors are the -X, X, -Y, Y, -Z, Z direction of the character.
128
self.left = soya.Vector(self, -1.0, 0.0, 0.0)
129
self.right = soya.Vector(self, 1.0, 0.0, 0.0)
130
self.down = soya.Vector(self, 0.0, -1.0, 0.0)
131
self.up = soya.Vector(self, 0.0, 1.0, 0.0)
132
self.front = soya.Vector(self, 0.0, 0.0, -1.0)
133
self.back = soya.Vector(self, 0.0, 0.0, 1.0)
134
135
def begin_round(self):
136
self.begin_action(self.controller.next())
137
soya.World.begin_round(self)
138
139
def begin_action(self, action):
140
"""This method begins the action ACTION. It DOES NOT perform the action
141
(see advance_time for that). But it does "decode" the action, and NOW it check for any
142
character-level collision that may occur. It also check if the character is on the
143
ground, and if not it make him stat falling."""
144
# Reset
145
self.rotation_speed = self.speed.x = self.speed.z = 0.0
146
if self.speed.y > 0.0: self.speed.y = 0.0
147
# If self.speed.y < 0.0, the character is falling, and we want him to continue
148
# to fall, and even fall faster and faster, so we don't reset self.speed.y to 0.0.
149
150
# Determine the character rotation
151
if action.action in (ACTION_TURN_LEFT, ACTION_ADVANCE_LEFT, ACTION_GO_BACK_LEFT):
152
self.rotation_speed = 5.0
153
elif action.action in (ACTION_TURN_RIGHT, ACTION_ADVANCE_RIGHT, ACTION_GO_BACK_RIGHT):
154
self.rotation_speed = -5.0
155
156
# Determine the character speed
157
if action.action in (ACTION_ADVANCE, ACTION_ADVANCE_LEFT, ACTION_ADVANCE_RIGHT):
158
self.speed.z = -0.35
159
elif action.action in (ACTION_GO_BACK, ACTION_GO_BACK_LEFT, ACTION_GO_BACK_RIGHT):
160
self.speed.z = 0.2
161
162
# Computes the new center of the character, after the move
163
# (this is a Point + Vector addition, i.e. a translation)
164
new_center = self.center + self.speed
165
166
# Creates a raypicking context, from a center and a radius.
167
# Raypicking contexts allows faster raypicking if you perform several raypicking
168
# in the same region, by selecting in advance all the objects susceptible to
169
# collide with the ray.
170
context = scene.RaypickContext(new_center, max(self.radius, 0.1 + self.radius_y))
171
172
# Gets the ground, and check if the character is falling
173
# To do so, we perform a raypicking in the down direction.
174
# If successful, the raypick method returns a
175
# (collision point, collision normal) tuple.
176
r = context.raypick(new_center, self.down, 0.1 + self.radius_y, 1, 1)
177
if r:
178
# Character's feet are on or below the ground, so we puts the character
179
# on the ground.
180
ground, ground_normal = r
181
ground.convert_to(self)
182
self.speed.y = ground.y
183
else:
184
# No ground => start falling
185
# Test the fall with the pit behind the second house
186
# We increase self.speed.y from 0.0 to -0.5 with a step of -0.05
187
self.speed.y = max(self.speed.y - 0.05, -0.5)
188
189
# The movement (defined by the speed vector) may be impossible if the character
190
# would encounter a wall.
191
192
# Character-level collision
193
for vec in (self.left, self.right, self.front, self.back, self.up):
194
r = context.raypick(new_center, vec, self.radius, 1, 1)
195
if r:
196
# The ray encounters a wall => the character cannot perform the planned movement.
197
# We compute a correction vector, and add it to the speed vector, as well as to
198
# new_center (for the following raypicks ; remember that
199
# new_center = self.center + self.speed, so if speed has changed, we must update
200
# it).
201
202
collision, wall_normal = r
203
hypo = vec.length() * self.radius - new_center.distance_to(collision)
204
correction = wall_normal * hypo
205
206
# Theorical formula, but more complex and identical result
207
#angle = (180.0 - vec.angle_to(wall_normal)) / 180.0 * math.pi
208
#correction = wall_normal * hypo * math.cos(angle)
209
210
# Adds the correction vector to the speed vector and to new_center
211
# (Point + Vector addition, i.e. translation)
212
self.speed += correction
213
new_center += correction
214
215
def advance_time(self, proportion):
216
soya.World.advance_time(self, proportion)
217
218
self.add_mul_vector(proportion, self.speed)
219
self.rotate_lateral(proportion * self.rotation_speed)
220
221
222
# Create the scene (a world with no parent)
223
scene = soya.World()
224
225
# Loads the level, and put it in the scene
226
level = soya.World.get("level_demo")
227
scene.add(level)
228
229
# Creates a character in the level, with a keyboard controller
230
character = Character(level, KeyboardController())
231
character.set_xyz(216.160568237, -7.93332195282, 213.817764282)
232
233
# Creates a Tomb Raider-like camera in the scene
234
camera = soya.TravelingCamera(scene)
235
traveling = soya.ThirdPersonTraveling(character)
236
traveling.distance = 5.0
237
camera.add_traveling(traveling)
238
camera.zap()
239
camera.back = 70.0
240
241
# Creates a widget group, containing the camera and a label showing the FPS.
242
soya.set_root_widget(widget.Group())
243
soya.root_widget.add(camera)
244
soya.root_widget.add(widget.FPSLabel())
245
246
# Creates and run an "idler" (=an object that manage time and regulate FPS)
247
# By default, FPS is locked at 40.
248
soya.Idler(scene).idle()
249