class TwoWayImage(pygame.sprite.Sprite): """Sprite which moves in two directions and loops; used to make background/foreground.""" def __init__(self, speed, image_dir, rearrange_coeff, image_type): super(TwoWayImage, self).__init__() self.speed = speed self.image_dir = image_dir # Total number of images; multiplied for rearrange offset self.rearrange_coeff = rearrange_coeff self.image_type = image_type if self.image_type == "foreground": self.image = cache_image.get(self.image_dir, convert_alpha=False) else: self.image = cache_image.get(self.image_dir) self.rect = self.image.get_rect() self.alpha_timer = Timer() self.alpha = 255 self.is_dying = False, if self.image_type == "foreground": self.hitmask = pygame.mask.from_surface(self.image) def rearrange(self): """Reset the sprite if it goes offscreen.""" # If screen_width isn't divisible by speed, the offset will vary: natural_offset = self.rect.x + SCREEN_WIDTH self.rect.x = (self.rearrange_coeff * SCREEN_WIDTH) + natural_offset def kill(self): """Remove foreground terrain for boss battles by fading its alpha.""" if self.alpha_timer.elapsed_time() > 60: self.alpha -= 3 self.image.set_alpha(self.alpha) self.alpha_timer.reset() def update(self): """Update image movement, rearranging or killing alpha as necessary.""" if self.alpha <= 0: all_sprites_list.remove(self) if self.image_type == "foreground": foreground_list.remove(self) if self.is_dying == True: self.kill() self.rect.x -= self.speed if self.rect.x < (0 - self.rect.width): self.rearrange()
class HeadLaserBeam(pygame.sprite.Sprite): def __init__(self, head): super(HeadLaserBeam, self).__init__() self.head = head # Bad variable name TODO self.beam_size_timer = Timer() self.beam_height = 1 self.beam_incrementer = 1 self.update_beam_image(self.beam_height) self.rect.centery = self.head.rect.centery + 10 def update_beam_height(self): """Pulse height, and remove from sprite lists for 1.5 seconds when height=0.""" max_beam_height = 15 beam_downtime = 1500 # Time where there is no beam, aka no damage taken by player if self.beam_height <= 0: enemy_projectile_list.remove(self) if self.beam_size_timer.elapsed_time() > beam_downtime: enemy_projectile_list.add(self) self.beam_height = 1 self.beam_incrementer = 1 elif self.beam_size_timer.elapsed_time() > 60: enemy_projectile_list.add(self) if self.beam_height >= max_beam_height: self.beam_incrementer *= -1 self.beam_height += self.beam_incrementer self.beam_size_timer.reset() def update_beam_image(self, beam_height): self.image = pygame.Surface([640, beam_height]) self.image.fill(BLUE) self.rect = self.image.get_rect() def kill(self): all_sprites_list.remove(self) enemy_projectile_list.remove(self) def update(self): self.update_beam_height() self.update_beam_image(self.beam_height) self.rect.centery = self.head.rect.centery + 10
class Bot(GameObject): """Bot Basic tier 1 unit. """ fire_rate = 3.0 fire_dist = 20 movement_speed = 60.0 def __init__(self, position): super(Bot, self).__init__('assets/circle.png', position) self._fire_timer = Timer(Bot.fire_rate) def _fire(self, position): diff_x = position[0] - self.position[0] diff_y = position[1] - self.position[1] aim = helpers.normalize((diff_x, diff_y)) l = Bot.fire_dist fire_pos = (self.position[0] + aim[0] * l, self.position[1] + aim[1] * l) return Pulse(position=fire_pos, direction=aim) def fire(self, position): if self._fire_timer.check(): self._fire_timer.reset() return self._fire(position) return [] def update(self, delta, **kwargs): self._fire_timer.update(delta) dist = Bot.movement_speed * delta self.move(dist, 0) targets = [(400, 400), (200, 200), (300, 300)] if targets: target = random.choice(targets) shot = self.fire(target) if shot: return [shot] return [] def __str__(self): return 'Bot'
class FadeOut(pygame.sprite.Sprite): def __init__(self, surface): super(FadeOut, self).__init__() self.surface = surface # Alpha value for transparency: self.alpha = 255 # Incrementer will change the 'direction' and speed of the fade: self.alpha_incrementer = -25 # Inc_delay = frames that pass before next alpha update: self.alpha_inc_delay = 60 # Flag for completion: self.is_complete = False self.image = pygame.Surface((800, 600)) self.rect = self.image.get_rect() self.image.fill((0, 0, 0)) self.image.set_alpha(255) self.fade_timer = Timer() def update(self): self.image.set_alpha(self.alpha) self.surface.blit(self.image, (0, 0)) if not self.is_complete: if self.alpha < 0: self.alpha = 0 self.is_complete = True if self.fade_timer.elapsed_time() > self.alpha_inc_delay: self.alpha += self.alpha_incrementer self.fade_timer.reset()
class BossWarning(pygame.sprite.Sprite): """Flash a 'WARNING!' message in the center of the SCREEN.""" def __init__(self, txt_img, xpos_center, ypos_center, kill_time): super(BossWarning, self).__init__() self.kill_time = kill_time self.image = cache_image.get("screen_messages/boss_warning" + "_F1.png") self.image.set_colorkey(BLACK) self.rect = self.image.get_rect() self.rect.centerx = xpos_center self.rect.centery = ypos_center self.anim_timer = Timer() self.kill_timer = Timer() self.frame_idx = 2 def update(self): """Flash the sign by switching images and destroy it after a set time.""" if self.kill_timer.elapsed_time() > self.kill_time: self.kill() try: flash_time = 360 if self.anim_timer.elapsed_time() > flash_time: self.anim_timer.reset() self.image = cache_image.get("screen_messages/boss_warning_F{}.png" .format(self.frame_idx)) self.image.set_colorkey(BLACK) self.frame_idx += 1 except pygame.error: self.frame_idx = 1 def kill(self): all_sprites_list.remove(self)
class Boss2(EnemyLinear): def __init__(self, dict_enemy): """Boss 2 housing class. Controls all body parts.""" super(Boss2, self).__init__(dict_enemy) self.image_dir = self.params["image"] self.has_entered = False self.is_attacking = False self.is_dying = False self.is_dead = False self.next_attack_timer = Timer() self.attack_scheduler = ActionScheduler() self.image = pygame.transform.scale2x(self.image) # Debug only self.hitmask = pygame.mask.from_surface(self.image) self.initialize_body_parts() def initialize_body_parts(self): """Set up head and claw body parts as separate sprites.""" from models.bosses.boss_2_head import Boss2Head head = Boss2Head(self) self.head = head def add_body_parts(self): """Add body parts to necessary lists.""" all_sprites_list.add(self.head) def enter(self): if self.has_entered: return if self.rect.centerx <= 550 and self.rect.centery <= SCREEN_HEIGHT / 2: self.has_entered = True self.next_attack_timer.reset() self.head.attack_scheduler.reset() self.attack_scheduler.reset() if not self.has_entered: self.add_body_parts() if self.rect.centerx >= 550: self.rect.x -= 1 if self.rect.centery >= SCREEN_HEIGHT / 2: self.rect.y -= 1 def reset_attack_parameters(self): """Reset all parameters for boss's (and body parts') attacks.""" self.next_attack_timer.reset() self.attack_scheduler.reset() self.head.reset_attack_parameters() def laser_head_attack(self, time_offset): attack_times = [750, 10000, 10750] attack_speeds = [6, 0, -6] attack_times = [(x + time_offset) for x in attack_times] self.attack_scheduler.update(attack_times) if not self.attack_scheduler.is_done_action: idx = self.attack_scheduler.action_idx current_speed = attack_speeds[idx] self.rect.x += current_speed if idx == 1: self.head.laser_attack(time_offset + 750) else: self.reset_attack_parameters() self.is_attacking = self.attack_scheduler.is_doing_action def update(self): self.enter() if self.has_entered and not self.is_dying: if self.next_attack_timer.elapsed_time() > 2500: self.laser_head_attack(2500)
class FallingBlock(EnemyLinear): """Environment Block that will fall unless supported by a floor block.""" def __init__(self, dict_enemy): super(FallingBlock, self).__init__(dict_enemy) # NOTE: falling_block_list is only used for collision logic # FallingBlock()s are added to enemy_sprite_list and use that logic falling_block_list.add(self) self.fall_timer = Timer() self.fall_speed = 3 self.is_falling = False def check_collision(self, sprite_list): """Check for vertical collision with a sprite list, skipping itself.""" for block in sprite_list: # Continue falling if self = the only block in the list: if self == block: if len(sprite_list) == 1: self.is_falling = True self.fall_timer.reset() continue # 15 = the 'edge cushion' needed to keep the block from falling: x_range = range(block.rect.x - self.rect.width + 15, block.rect.x + block.rect.width - 15) y_range = range(block.rect.y, block.rect.y + block.rect.height) if (self.rect.x in x_range and self.rect.y + self.rect.height in y_range): self.rect.y = (block.rect.y - self.rect.height + (self.fall_speed / 2) ) self.is_falling = False else: if not self.is_falling: self.is_falling = True self.fall_timer.reset() def fall(self, fall_speed): """Move the block downward, with a slight pause at the top for visual effect.""" if self.fall_timer.elapsed_time() > 60: self.rect.y += (fall_speed * 4) else: self.rect.y += fall_speed def update(self): """Kill if necessary; check collisions; update fall speed.""" if (self.rect.centerx < -700 or self.rect.centerx > SCREEN_WIDTH + 700 or self.rect.centery < -200 or self.rect.centery > SCREEN_HEIGHT + 200): self.kill() self.check_collision(falling_block_list) if self.is_falling: self.fall(self.fall_speed) self.rect.x -= 3
class ActionScheduler(): """ Allows for scheduling of actions via an array of times, an action index, a timer, and 4 booleans (Note: all booleans won't always be used on any given instance. They exist to cover a range of functionality.) This times array... action_times = [0, 1000, 2000, 3000] ...means that an action occurs at 1-second, 2-seconds, and 3-seconds. These elements are considered thresholds for 'sub_actions', and the entire array constitutes one total 'action.' For example, in enemy pathing, these could be changes of direction. So each change in direction would be a 'sub_action', whereas the enemy's entire 'path_times' array would be one total 'action.' 'action_idx' increments at each new time element in the times array. 'is_doing_action' = True while the timer < last time in the times array. (Note that there is no 'is_doing_subaction', because 'action_idx' won't increment until the timer hits the next 'subaction' threshold; it will remain the same for the duration of a 'subaction', so any behavior logic can simply reference that index.) 'is_done_action' = True only when timer > last time in times array. 'is done_subaction' will be True for only one 'update' cycle per 'subaction' completed. This sounds confusing, because it is...This was used to solve a problematic edge condition that arises when elapsed time > total_time and 'is_done_subaction' is set to True: it never gets set back to false. We solve that with some if-else logic and the self.kill() method: self.kill() and 'is_dead' are used only for actions that occur ONCE per 'subaction' threshold. Example: Condition: elapsed time = 1001 action_times[-1] = 1000 What happens: the first time through self.update(), 'is_done_action' and 'is_done_subaction' are set to True the NEXT time through self.update(), because 'is_done_action' is True, we go through our is_looped or kill conditions from there, if 'is_dead' == True, update returns None. """ def __init__(self, is_looped=None): self.is_doing_action = False self.is_done_action = False self.is_done_subaction = False self.is_dead = False self.action_idx = 0 self.timer = Timer() self.is_looped = is_looped def reset(self): """Loop an action by resetting booleans, index, and timer variables.""" self.is_doing_action = False self.is_done_action = False self.action_idx = 0 self.timer.reset() def kill(self): """Used to properly assign 'is_done_subaction' to False when a total action is done.""" self.is_doing_action = False self.is_done_action = True self.is_done_subaction = False self.is_dead = True def execute_final_action(self): """Determine whether to loop the action again or kill it.""" if self.is_looped: self.reset() else: self.kill() def update(self, action_times): """ Check if 'is_dead' or 'is_done_action', otherwise proceed normally: Flip 'is_doing_action' to True. Increment 'action_idx' when elapsed time reaches next 'subaction' threshold. If timer > total time, flip 'is_doing_action' = False, and 'is_done_action' = True. """ if self.is_dead: return self.is_doing_action = True self.is_done_subaction = False if self.is_done_action: self.execute_final_action() elif self.timer.elapsed_time() > action_times[-1]: self.action_idx = -1 self.is_done_action = True self.is_done_subaction = True elif self.timer.elapsed_time() > action_times[self.action_idx]: self.is_done_subaction = True self.action_idx += 1
class FourWayImage(pygame.sprite.Sprite): """Sprite that can move in four directions on a loop; used to create background/foreground.""" def __init__(self, params, image_dir, image_type): super(FourWayImage, self).__init__() self.params = params self.image_type = image_type self.directions = self.params["directions"] self.speed = self.params["speed"] self.image_dir = image_dir if self.image_type == "foreground": self.image = cache_image.get(self.image_dir, convert_alpha=False) else: self.image = cache_image.get(self.image_dir) self.image = pygame.transform.scale2x(self.image) self.rect = self.image.get_rect() if self.image_type == "foreground": self.hitmask = pygame.mask.from_surface(self.image) # Used for re-arranging later: self.initial_x_pos = 0 self.initial_y_pos = 0 # Used to ensure accurate movement to screen edges: self.width_check = 0 self.height_check = 0 self.path_pointer = 0 # For perfect diagonal movement, y and x must move at different speeds, # so we get a fraction here as a coefficient for later: self.diagonal_speed_coeff = Fraction(SCREEN_HEIGHT, SCREEN_WIDTH) * self.speed self.diagonal_counter = 1 self.is_dying = False self.alpha = 255 self.alpha_timer = Timer() def path(self, direction, coefficient): """ Move the sprite in various directions. Diagonal movement was very tricky, as the screen is not square, so 1 : 1 movement doesn't produce desired results. Neither does decimal movement, as trying to move by decimals of a pixel results in de-syncing the background. Instead we have to produce whole-number pixel movement based on a ratio we calculate using the speed, screen height, and screen width: ********************************************************************* EXAMPLE: SCREEN_HEIGHT / SCREEN_WIDTH = (3/4) speed: 5 coefficient = 5 * (3/4) = (15/4) diagonal_counter = 1 ticker will increment up to coefficient's denominator, y will increment by the denominator amount. when the ticker == the denominator, y += total moved space, aka: ((denominator - 1) * denominator) and diagonal_counter will reset to 1: Ticker Moved_Space 1 4 2 8 3 12 4 15 4 updates = 15 moved pixels = our original coefficient! (Some additional if-else logic had to be implemented for coefficients that were < 1 (i.e., if speed == 1 or 2), so as to not get negative movement. """ if direction == "right": self.rect.x -= self.speed self.width_check += self.speed elif direction == "down": self.rect.y -= self.speed self.height_check += self.speed elif direction == "up": self.rect.y += self.speed self.height_check += self.speed elif direction in ["upright", "downright"]: # For diagonal movement, we have to use our coefficient: if self.diagonal_counter < coefficient.denominator: if direction == "upright": # Special conditions if coefficient < 1: if coefficient < 1: self.rect.y += 1 else: self.rect.y += coefficient.denominator elif direction == "downright": if coefficient < 1: self.rect.y -= 1 else: self.rect.y -= coefficient.denominator self.diagonal_counter += 1 else: if coefficient < 1: next_move = 0 else: moved_space = (coefficient.denominator - 1) * coefficient.denominator next_move = coefficient.numerator - moved_space if direction == "upright": self.rect.y += next_move elif direction == "downright": self.rect.y -= next_move self.diagonal_counter = 1 self.rect.x -= self.speed self.width_check += self.speed self.height_check += float(self.diagonal_speed_coeff) def kill(self): if self.alpha_timer.elapsed_time() > 60: self.alpha_timer.reset() self.alpha -= 3 self.image.set_alpha(self.alpha) def update(self): """Update based on 'directions' parameter; loop when finished.""" if self.alpha <= 0: all_sprites_list.remove(self) if self.image_type == "foreground": foreground_list.remove(self) if self.is_dying: self.kill() # Continuous x-loop for foreground only: if self.image_type == "foreground" and self.rect.x < (self.initial_x_pos - 1600): self.rect.x = 0 try: current_direction = self.directions[self.path_pointer] # Reset flags if they reach screen dimensions (more precise than a timer): if self.image_type == "background": if self.width_check >= SCREEN_WIDTH or self.height_check >= SCREEN_HEIGHT: # TODO: Fix looped clipping: self.width_check = 0 self.height_check = 0 self.path_pointer += 1 else: # Foreground moves at 2x the speed of background, thus double the checks: if self.width_check >= SCREEN_WIDTH * 2 or self.height_check >= SCREEN_HEIGHT * 2: self.width_check = 0 self.height_check = 0 self.path_pointer += 1 self.path(current_direction, self.diagonal_speed_coeff) except IndexError: self.rearrange() def rearrange(self): """Reset images for looping.""" self.rect.x = self.initial_x_pos - self.speed self.rect.y = self.initial_y_pos self.path_pointer = 0
def main(cfg): cprint("-- preparing..") max_iter = cfg['solver']['max_iter'] summary_iter = cfg['solver']['summary_iter'] save_iter = cfg['solver']['save_iter'] ckpt_dir = os.path.join(cfg['path']['ckpt_dir'], cfg.cfg_name) ckpt_file = os.path.join(ckpt_dir, cfg.cfg_name) output_path = '../train_inter_results/' mkdir_p(output_path) tf.logging.info("-- constructing network..") with tf.Graph().as_default(): with tf.device('/cpu:0'): with tf.name_scope('data_provider'): sample = { 'sample_radii': cfg.radii, 'cls': cfg.cls, 'x_min': cfg.min_x, 'y_min': cfg.min_y, 'z_min': cfg.min_z, 'x_max': cfg.max_x, 'y_max': cfg.max_y, 'z_max': cfg.max_z, 'num_points': cfg.num_points, 'CENTER_PERTURB': cfg.CENTER_PERTURB, 'CENTER_PERTURB_Z': cfg.CENTER_PERTURB_Z, 'SAMPLE_Z_MIN': cfg.sample_z_min, 'SAMPLE_Z_MAX': cfg.sample_z_max, 'QUANT_POINTS': cfg.QUANT_POINTS, 'QUANT_LEVEL': cfg.QUANT_LEVEL } dataset = ObjectProvider( edict({ 'batch_size': cfg.solver.batch_size, 'dataset': 'kitti', 'split': 'train', 'is_training': True, 'num_epochs': None, 'sample': sample })) dataset.data_size = 100000 # FIXME. random number global_step = tf.get_variable( 'global_step', [], initializer=tf.constant_initializer(0), trainable=False) learning_rate = _configure_learning_rate(cfg, dataset.data_size, global_step) bn_decay = get_bn_decay(cfg, dataset.data_size, global_step) optimizer = _configure_optimizer(cfg, learning_rate) tf.summary.scalar('learning_rate', learning_rate) # Calculate the gradients for each model tower. towers_ph_points = [] towers_ph_obj = [] towers_ph_is_training = [] tower_grads = [] tower_losses = [] device_scopes = [] scope_name = 'rpn' with tf.variable_scope(scope_name): for gid in range(cfg.num_gpus): with tf.name_scope('gpu%d' % gid) as scope: with tf.device('/gpu:%d' % gid): with tf.name_scope("train_input"): ph_points = tf.placeholder(tf.float32, shape=(None, cfg.num_points, 3)) ph_obj = tf.placeholder(tf.float32, shape=(None, )) ph_is_training = tf.placeholder(tf.bool, shape=()) net = Net(ph_points=ph_points, is_training=ph_is_training, bn_decay=bn_decay, cfg=cfg) net.losses(target_objs=ph_obj, cut_off=cfg.iou_cutoff, gl=global_step) all_losses = tf.get_collection(tf.GraphKeys.LOSSES, scope) sum_loss = tf.add_n(all_losses) for loss in all_losses: tf.summary.scalar(loss.op.name, loss) tf.summary.scalar("sum_loss_tower", sum_loss) # Reuse variables for the next tower. tf.get_variable_scope().reuse_variables() # Calculate the gradients for the batch of data grads = optimizer.compute_gradients(sum_loss) # Keep track of the gradients across all towers. tower_grads.append(grads) tower_losses.append(sum_loss) device_scopes.append(scope) # Collect all placeholders towers_ph_points.append(ph_points) towers_ph_obj.append(ph_obj) towers_ph_is_training.append(ph_is_training) total_loss = tf.add_n(tower_losses, name='total_loss') grads = _average_gradients(tower_grads) apply_gradient_ops = optimizer.apply_gradients(grads, global_step=global_step) # Add histograms for trainable variables. for var in tf.trainable_variables(): tf.summary.histogram(var.op.name, var) # Track the moving averages of all trainable variables. # if cfg.solver.moving_average_decay: with tf.name_scope('expMovingAverage'): variable_averages = tf.train.ExponentialMovingAverage( 0.005, global_step) # cfg.solver.moving_average_decay, global_step) averages_op = variable_averages.apply(tf.trainable_variables()) # else: # averages_op = None # Group all updates to into a single train op. train_op = tf.group(apply_gradient_ops, averages_op) train_tensor = control_flow_ops.with_dependencies([train_op], total_loss, name='train_op') # Create a saver saver = tf.train.Saver(tf.global_variables(), max_to_keep=20) init = tf.global_variables_initializer() # =================================================================== # # Kicks off the training. # =================================================================== #\ # GPU configuration if cfg.num_gpus == 0: config = tf.ConfigProto(device_count={'GPU': 0}) else: config = tf.ConfigProto(allow_soft_placement=True, log_device_placement=False) with tf.Session(config=config) as sess: dataset.set_session(sess) # initialization / session / writer / saver print('initializing a network may take minutes...') sess.run(init) tf.train.start_queue_runners(sess=sess) train_writer, eval_writer = _set_filewriters(ckpt_dir, sess) merged = tf.summary.merge_all() ckpt_dir = os.path.join(cfg.path.ckpt_dir, cfg.cfg_name) weight_file = tf.train.latest_checkpoint(ckpt_dir) if weight_file is not None: saver.restore(sess, weight_file) tf.logging.info('%s loaded' % weight_file) else: tf.logging.info( 'Training from the scratch (no pre-trained weight_filets)..' ) train_timer = Timer() print('start training...') stat_correct = [] def inference(feed_dict): loc, conf_iou = sess.run([net.pred_locs, net.pred_conf_iou], feed_dict) return loc, conf_iou for step in range(max_iter): train_timer.tic() feed_dict = {} for i in range(cfg.num_gpus): b_points, b_objs, b_locs = dataset.get_batch() feed_dict[towers_ph_points[i]] = b_points feed_dict[towers_ph_obj[i]] = b_objs feed_dict[towers_ph_is_training[i]] = True gl, loss, _ = sess.run([global_step, total_loss, train_tensor], feed_dict=feed_dict) if gl % 100 == 0: cprint("gl: {} loss: {:.3f}".format(gl, loss)) train_timer.toc() if gl % summary_iter == 0: if gl % (summary_iter * 10) == 0: # Summary with run meta data run_options = tf.RunOptions( trace_level=tf.RunOptions.FULL_TRACE) run_metadata = tf.RunMetadata() summary_str, loss, _ = sess.run( [merged, total_loss, train_tensor], feed_dict=feed_dict, options=run_options, run_metadata=run_metadata) train_writer.add_run_metadata(run_metadata, 'step_{}'.format(gl), gl) train_writer.add_summary(summary_str, gl) else: # Summary summary_str = sess.run(merged, feed_dict=feed_dict) train_writer.add_summary(summary_str, gl) log_str = ( '{} Epoch: {:3d}, Step: {:4d}, Learning rate: {:.4e}, Loss: {:5.3f}\n' '{:14s} Speed: {:.3f}s/iter, Remain: {}').format( datetime.datetime.now().strftime('%m/%d %H:%M:%S'), int(cfg.solver.batch_size * gl / dataset.data_size), int(gl), round(learning_rate.eval(session=sess), 6), loss, '', train_timer.average_time, train_timer.remain(step, max_iter)) print(log_str) train_timer.reset() if gl % save_iter == 0: print('{} Saving checkpoint file to: {}'.format( datetime.datetime.now().strftime('%m/%d %H:%M:%S'), ckpt_dir)) saver.save(sess, ckpt_file, global_step=global_step) evaluate_iter = 100000
class AnimationScheduler(): """Class to handle various types of sprite animations.""" def __init__(self, is_looped, is_seesaw=False): self.is_looped = is_looped self.is_seesaw = is_seesaw self.is_done_animating = False self.frame_idx = 0 self.timer = Timer() if self.is_seesaw: self.frame_incrementer = 1 def reset(self): """Reset boolean, frame index, and timer.""" self.is_done_animating = False self.frame_idx = 0 self.timer.reset() def looped_update(self, anim_times, static_time_threshold): """Update looped animation based on a schedule of times or a single static threshold.""" if anim_times is not None: if self.timer.elapsed_time() > anim_times[self.frame_idx]: self.frame_idx += 1 self.timer.reset() elif self.timer.elapsed_time() > static_time_threshold: self.frame_idx += 1 self.timer.reset() def seesaw_update(self, anim_times, static_time_threshold): """Update an animation that loops by playing itself backwards and forwards.""" try: if self.timer.elapsed_time() > static_time_threshold: self.frame_idx += self.frame_incrementer self.timer.reset() except pygame.error: self.frame_incrementer *= -1 def normal_update(self, anim_times): """Update linear animation that plays one time before flipping 'is_done_animating'.""" if self.is_done_animating: return # Necessary edge condition: elif len(anim_times) < 2: if self.timer.elapsed_time() > anim_times[self.frame_idx]: self.frame_idx += 1 self.is_done_animating = True elif self.frame_idx == len(anim_times) - 1 and self.timer.elapsed_time() > anim_times[-1]: self.is_done_animating = True # This needs to be separated from the above if-statement to preserve the last frame of # animation. If we tried to flip 'is_done_animating' AND increment frame_idx at the same # time, we don't get to see the last frame of the animation. Separating here ensures that: elif self.frame_idx == len(anim_times) - 2 and self.timer.elapsed_time() > anim_times[-2]: self.frame_idx += 1 elif self.timer.elapsed_time() > anim_times[self.frame_idx]: self.frame_idx += 1 self.timer.reset() def get_next_frame(self, image_dir, convert_alpha=True): """Return next frame in the animation.""" # Using aTry-Except block allows me to easily add new frame assets to a # looped animation without updating any code whatsoever! try: image = cache_image.get(image_dir + "_F{}.png".format(self.frame_idx), convert_alpha) except pygame.error: if self.is_looped: self.reset() image = cache_image.get(image_dir + "_F{}.png".format(self.frame_idx), convert_alpha) elif self.is_seesaw: self.frame_incrementer *= -1 self.frame_idx += self.frame_incrementer image = cache_image.get(image_dir + "_F{}.png".format(self.frame_idx), convert_alpha) return image def update(self, anim_times=None, static_time_threshold=120): """Update based on type of animation.""" if self.is_looped: self.looped_update(anim_times, static_time_threshold) elif self.is_seesaw: self.seesaw_update(anim_times, static_time_threshold) else: self.normal_update(anim_times)
class Level(object): """Deploy waves of enemies based on a dictionary of parameters.""" def __init__(self, dict_level): self.params = dict_level self.wave_timer = Timer() self.wave_idx = 1 self.level_scheduler = ActionScheduler() self.wave_scheduler = ActionScheduler() self.level_number = self.params["level_number"] self.is_complete = False self.initialize_level_assets() self.boss = self.initialize_boss() # Wave parameters + deployment times self.deployment_times = {0: 0} self.waves = {0: 0} # self.initialize_waves() self.last_level = key_or_none("last", self.params) def initialize_level_assets(self): """Initialize background and foreground art for the level.""" # Boolean which determines if level is sidescrolling or multi-directional self.has_directional_bg = key_or_none("has_directional_bg", self.params) if self.has_directional_bg is None: self.background = TwoWayBackground(self.params["bg_params"]) self.foreground = TwoWayForeground(self.params["fg_params"]) else: self.background = FourWayBackground(self.params["bg_params"]) self.foreground = FourWayForeground(self.params["fg_params"]) def initialize_boss(self): """Initialize the level's boss, based on params.""" params_dict = self.params["boss"] klass = params_dict["class"] params = params_dict["params"] boss = klass(params) return boss def initialize_waves(self): """Unpack wave and deployment time parameters into separate dicts.""" # NOTE: DO NOT TOUCH THESE BLOCKS!!! # Loops for collecting Deployment Times: deployment_times = self.params["deployment_times"] for wave_num in deployment_times: try: for subwave_times in deployment_times[wave_num]: self.deployment_times[len(self.deployment_times)] = subwave_times except TypeError: self.deployment_times[len(self.deployment_times)] = deployment_times[wave_num] # Multiply times by 1000 to account for milliseconds: for wave_num in self.deployment_times: self.deployment_times[wave_num] *= 1000 # Loop for collecting Waves: waves = self.params["waves"] for wave_num in waves: if isinstance(waves[wave_num][0], list): for wave_params in waves[wave_num]: self.waves[len(self.waves)] = wave_params else: self.waves[len(self.waves)] = waves[wave_num] def load_wave(self, params_wave): """ Return a wave of enemies based on the parameters dictionary, accounting for positional offsets, vertical flips, and horizontal flips. """ # Get parameters from the dict: next_wave = [] num_enemy = params_wave[0] klass = params_wave[1] params_enemy = params_wave[2] offsets = params_wave[3] vertical_flips = params_wave[4] horizontal_flips = params_wave[5] # Special offsets applied to FallingBlocks to spawn simultaneously: if klass == FallingBlock: for i in range(num_enemy): enemy = klass(params_enemy) if offsets is not None: enemy.rect.centerx += offsets[i][0] enemy.rect.centery += offsets[i][1] next_wave.append(enemy) else: # Apply any x- or y- shifts: for i in range(0, num_enemy): enemy = klass(params_enemy) if offsets is not None: enemy.rect.centerx += offsets[0] enemy.rect.centery += offsets[1] next_wave.append(enemy) # Apply flips ([i - 1] because params are not 0-indexed): if horizontal_flips is not None: for i in horizontal_flips: next_wave[i - 1].flip_path_horizontal() if vertical_flips is not None: for i in vertical_flips: next_wave[i - 1].flip_path_vertical() all_sprites_list.add(next_wave) enemy_sprite_list.add(next_wave) def display_boss_warning(self): """Flash a warning message that the boss is approaching.""" warning = BossWarning("screen_messages/boss_warning", SCREEN_WIDTH / 2, 200, 6000) all_sprites_list.add(warning) message = TextToScreen("screen_messages/boss_approaching", SCREEN_WIDTH / 2, 300, 6000) all_sprites_list.add(message) def release_wave(self, wave, offset): if self.wave_scheduler.is_done_action: self.wave_scheduler.reset() self.wave_timer.reset() self.wave_idx += 1 return wave_times = [((x * 1000) + offset) for x in wave["wave_times"]] enemy_params = wave["enemies"] self.wave_scheduler.update(wave_times) wave_idx = self.wave_scheduler.action_idx - 1 #TODO: Temporary hack, should fix if wave_idx == -2: wave_idx = -1 if self.wave_scheduler.is_done_subaction or self.wave_scheduler.is_done_action: self.load_wave(enemy_params[wave_idx]) def run(self): """Load all waves and deploy them based on a timed schedule.""" # try: # Level completion conditions: if self.boss.is_dead == True: self.is_complete = True last_wave = self.params["waves"][self.wave_idx - 1] current_wave = self.params["waves"][self.wave_idx] start_time = (current_wave["deployment_time"] - last_wave["deployment_time"]) * 1000 if self.wave_timer.elapsed_time() > start_time: if current_wave["wave_times"] == "BOSS": all_sprites_list.add(self.boss) boss_list.add(self.boss) elif current_wave["wave_times"] == "WARNING": self.wave_timer.reset() self.wave_idx += 1 self.display_boss_warning() self.foreground.kill() else: self.release_wave(current_wave, start_time)