-
Notifications
You must be signed in to change notification settings - Fork 0
/
character2.py
339 lines (251 loc) · 11.3 KB
/
character2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
import pygame.sprite
import pyganim
from abc import ABCMeta, abstractmethod
from actions import *
from colors import *
class Character:
def __init__(self):
self.width = 10
self.height = 10
self.collision_rect = pygame.Rect((100,100), (100,100))
class OverworldCharacter(pygame.sprite.Sprite):
"""
OVERWORLD CHARACTER
Representation of a character sprite in the overworld.
=========================
Initialization Parameters
=========================
sprite - pygame.Image object or string path to where the image is located
position - list of [x,y] coordinates for the character's starting position
movement_speed - how fast the sprite moves in pixels/frame
run_modifier - multiplier to calculate movement_speed when running
debug - Use debug methods
"""
def __init__(self, sprite, position, movement_speed=6, run_modifier=1.5, debug=False):
pygame.sprite.Sprite.__init__(self)
self.position = position
self.last_position = position
self.movement_speed = movement_speed
self.run_modifier = run_modifier
# set the display image of the character
if type(sprite) is str:
self.image = pygame.image.load(sprite)
else:
self.image = sprite
# position rect
self.rect = self.image.get_rect()
self.rect.topleft = self.position
# collision rect
self.collision_rect = pygame.Rect(0, 0, self.rect.width*.5, 8)
self.collision_rect.midbottom = self.rect.midbottom
self.width = self.rect.width
self.height = self.rect.height
# used for drawing the image
self.current_frame = self.image
# used to determine how to call methods at runtime
if debug:
self.state = OverworldDebugState(self)
else:
self.state = OverworldState(self)
"""
Call the draw function based on whether in debug mode or not
"""
def draw(self):
self.state.draw()
"""
Move the character in the given directions.
If a list of blockers is supplied, checks if the movement is valid.
:param: directions - list of directions (UP, DOWN, LEFT, RIGHT)
:param: blockers - list of pygame.Rect objects
:param: is_running - apply the run_modifier to movement_speed?
"""
def move(self, directions, blockers=None, is_running=False):
speed = self.movement_speed
if is_running:
speed = int(speed * self.run_modifier)
for direction in directions:
self.last_position = self.position[:]
if direction == UP:
self.position[1] -= speed
elif direction == DOWN:
self.position[1] += speed
elif direction == LEFT:
self.position[0] -= speed
elif direction == RIGHT:
self.position[0] += speed
self.rect.topleft = self.position
self.collision_rect.midbottom = self.rect.midbottom
# if provided with a list of blockers, check for a collision
if blockers and self.is_collision(blockers):
self.move_back()
def move_back(self):
self.position = self.last_position[:]
self.rect.topleft = self.position
self.collision_rect.midbottom = self.rect.midbottom
def is_collision(self, blockers):
return self.collision_rect.collidelist(blockers) != -1
class AnimatedOverworldCharacter(OverworldCharacter):
"""
ANIMATED OVERWORLD CHARACTER
Animated version of an overworld character.
=========================
Initialization Parameters
=========================
sprite_sheet - string path to the sprite sheet for the character
num_rows, num_cols - number of rows and columns of the sprite sheet. Used to slice the spritesheet into individual images.
!! Images are numbered from top left to bottom right, starting from 0
eg: num_rows = 4, num_cols = 6
0 1 2 3 4 5
6 7 8 9 10 11
12 13 14 15 16 17
18 19 20 21 22 23
position - list of [x,y] coordinates for the chacter's starting position
direction - which direction the sprite is facing (UP, DOWN, LEFT, RIGHT)
default_image - which image from the spritesheet should be used first
animation_dictionary - dictionary to explain which images from the spritesheet correspond to a given action
images should be listed in the order that they are to be displayed
{ACTION:[list of images]}
eg:
{UP:[0,1,2,3,2,1],
DOWN:[5,6,7,8,9,10],
LEFT:[11,12,13,14,13,12],
JUMPLEFT:[14,15,16,15],
SWINGSWORD:[19,20,21,22,23]}
display_time - the amount of time each frame should be displayed for in milliseconds
100 is a reasonable default
movement_speed - the speed at which the character will move in the overworld, in pixels/frame
run_modifier - modifier to calculate movement_speed when running
current_animation - which animation from the dictionary to display when the sprite is initialized
debug - determine how to draw the sprite
"""
def __init__(self, sprite_sheet, position, num_rows, num_cols, default_image=0, direction=DOWN,
animation_dictionary=None, display_time=100, movement_speed=6, run_modifier=1.5,
current_animation=None, debug=False):
# split the sprite sheet into a list of individual images
self._images = pyganim.getImagesFromSpriteSheet(sprite_sheet, rows=num_rows, cols=num_cols)
# call the constructor of the base class
super(AnimatedOverworldCharacter, self).__init__(self._images[default_image], position, movement_speed, run_modifier, debug)
self.direction = direction
self.current_animation = current_animation
self.animations = self._build_animation_dictionary(animation_dictionary, display_time)
# used to play/pause animations and keep them in sync
self.animation_conductor = pyganim.PygConductor(self.animations)
self.current_frame = self.image
"""
Move the sprite, and also set the direction of the sprite and play the animation for movement in that direction
:param: directions - list of directions (UP, DOWN, LEFT, RIGHT)
:param: blockers - list of pygame.Rect objects
:param: is_running - apply the run_modifier to movement_speed
"""
def move(self, directions, blockers=None, is_running=False):
super(AnimatedOverworldCharacter, self).move(directions, blockers, is_running)
self._determine_direction(directions)
self.current_animation = self.direction
self.animation_conductor.play()
"""
If there is no animation to play, pause the animation conductor
"""
def pause(self):
self.current_animation = None
self.animation_conductor.pause()
"""
Set the current_frame to the image that should be drawn on next update
"""
def update(self):
if self.current_animation:
self.current_frame = self.animations[self.current_animation].getCurrentFrame()
else:
self.current_frame = self.animations[self.direction].getFrame(0)
"""
Determines and sets the current direction that the sprite should be facing, given the directions that it is currently moving
:param: directions - list of directions (UP, DOWN, LEFT, RIGHT)
"""
def _determine_direction(self, directions):
if LEFT in directions or RIGHT in directions:
self.direction = list((set([LEFT, RIGHT]) & set(directions)))[0]
else:
self.direction = list((set([UP, DOWN]) & set(directions)))[0]
"""
Build a dictionary of ACTIONs and PygAnim animations.
By default, all animations are paused.
Called when the class is constructed.
eg:
{UP:<walk up animation>,
DOWN:<walk down animation>,
SWINGSWORD:<swing sword animation>}
"""
def _build_animation_dictionary(self, animation_dictionary, display_time):
animations = {}
for action, image_list in animation_dictionary.items():
images = []
for image in image_list:
images.append(self._images[image])
display_times = [display_time]*len(images)
images = list(zip(images, display_times))
animation = pyganim.PygAnimation(images)
animations[action] = animation
return animations
"""
Interface for character states.
Methods that vary depending on character state should be defined here, and implemented in each concrete state.
"""
class CharacterState(metaclass=ABCMeta):
def __init__(self, character):
self.character = character
@abstractmethod
def draw(self):
pass
# get a transparent surface the size of the character's image
def _get_blank_surface(self):
return pygame.Surface((self.character.width, self.character.height), pygame.SRCALPHA).convert_alpha()
"""
DEBUG STATE
"""
class OverworldDebugState(CharacterState):
def __init__(self, character):
super(self.__class__, self).__init__(character)
self.collision_box = self._get_collision_box()
self.collision_box_position = self._get_collision_box_position()
"""
Set the image of the character, but with a transparent blue box denoting the character's position rect
and a transparent red box denoting the collision rect.
"""
def draw(self):
temp = self._get_blank_surface()
temp.fill(BLUE)
temp.set_alpha(128)
temp.blit(self.character.current_frame, (0,0))
temp.blit(self.collision_box, self.collision_box_position)
self.character.image = temp
"""
Return a transparent red surface to represent the collision rect for the character
:return: collision_box - pygame.Surface
"""
def _get_collision_box(self):
collision_box = pygame.Surface((self.character.collision_rect.width, self.character.collision_rect.height), pygame.SRCALPHA)
collision_box.convert_alpha()
collision_box.fill(RED)
collision_box.set_alpha(128)
return collision_box
"""
Return the (x,y) coordinates of the topleft corner of the collision box.
Position is relative to the temp surface that collision_box will be displayed on.
:return: (x,y) - tuple of ints
"""
def _get_collision_box_position(self):
x = (self.character.width - self.character.collision_rect.width)/2
y = self.character.height - self.character.collision_rect.height
return (x, y)
"""
ANIMATED STATE
"""
class OverworldState(CharacterState):
def __init__(self, character):
super(self.__class__, self).__init__(character)
"""
Set the image of the charcter to be the next frame in the current animation.
"""
def draw(self):
temp = self._get_blank_surface()
temp.blit(self.character.current_frame, (0,0))
self.character.image = temp