game_world
View Source
import os, sys, math, time, importlib, json, traceback from collections import namedtuple import sdl2 import game_object, game_util_objects, game_hud, game_room import collision, vector from camera import Camera from grid import GameGrid from art import ART_DIR from charset import CHARSET_DIR from palette import PALETTE_DIR TOP_GAME_DIR = 'games/' DEFAULT_STATE_FILENAME = 'start' STATE_FILE_EXTENSION = 'gs' GAME_SCRIPTS_DIR = 'scripts/' SOUNDS_DIR = 'sounds/' # generic starter script with a GO and Player subclass STARTER_SCRIPT = """ from game_object import GameObject from game_util_objects import Player class MyGamePlayer(Player): "Generic starter player class for newly created games." art_src = 'default_player' # no "move" state art, so just use stand state for now move_state = 'stand' class MyGameObject(GameObject): "Generic starter object class for newly created games." def update(self): # write "hello" in a color that shifts over time color = self.art.palette.get_random_color_index() self.art.write_string(0, 0, 3, 2, 'hello!', color) # run parent class update GameObject.update(self) """ # Quickie class to debug render order RenderItem = namedtuple('RenderItem', ['obj', 'layer', 'sort_value']) class GameCamera(Camera): pan_friction = 0.2 use_bounds = False class GameWorld: """ Holds global state for Game Mode. Spawns, manages, and renders GameObjects. Properties serialized via WorldPropertiesObject. Global state can be controlled via a WorldGlobalsObject. """ game_title = 'Untitled Game' "Title for game, shown in window titlebar when not editing" gravity_x, gravity_y, gravity_z = 0., 0., 0. "Gravity applied to all objects who are affected by gravity." bg_color = [0., 0., 0., 1.] "OpenGL wiewport color to render behind everything else, ie the void." hud_class_name = 'GameHUD' "String name of HUD class to use" properties_object_class_name = 'WorldPropertiesObject' globals_object_class_name = 'WorldGlobalsObject' "String name of WorldGlobalsObject class to use." player_camera_lock = True "If True, camera will be locked to player's location." object_grid_snap = True # editable properties # TODO: #update_when_unfocused = False #"If True, game sim will update even when window doesn't have input focus" draw_hud = True allow_pause = True "If False, user cannot pause game sim" collision_enabled = True "If False, CollisionLord won't bother thinking about collision at all." # toggles for "show all" debug viz modes show_collision_all = False show_bounds_all = False show_origin_all = False show_all_rooms = False "If True, show all rooms not just current one." draw_debug_objects = True "If False, objects with is_debug=True won't be drawn." room_camera_changes_enabled = True "If True, snap camera to new room's associated camera marker." list_only_current_room_objects = False "If True, list UI will only show objects in current room." builtin_module_names = ['game_object', 'game_util_objects', 'game_hud', 'game_room'] builtin_base_classes = (game_object.GameObject, game_hud.GameHUD, game_room.GameRoom) def __init__(self, app): self.app = app "Application that created this world." self.game_dir = None "Currently loaded game directory." self.sounds_dir = None self.game_name = None "Game's internal name, based on its directory." self.selected_objects = [] self.hovered_objects = [] self.hovered_focus_object = None "Set by check_hovers(), to the object that will be selected if edit mode user clicks" self.last_click_on_ui = False self.last_mouse_click_x, self.last_mouse_click_y = 0, 0 self.properties = None "Our WorldPropertiesObject" self.globals = None "Our WorldGlobalsObject - not required" self.camera = GameCamera(self.app) self.grid = GameGrid(self.app) self.grid.visible = False self.player = None self.paused = False self._pause_time = 0 self.updates = 0 "Number of updates this we have performed." self.modules = {'game_object': game_object, 'game_util_objects': game_util_objects, 'game_hud': game_hud, 'game_room': game_room} self.classname_to_spawn = None self.objects = {} "Dict of objects by name:object" self.new_objects = {} "Dict of just-spawned objects, added to above on update() after spawn" self.rooms = {} "Dict of rooms by name:room" self.current_room = None self.cl = collision.CollisionLord(self) self.hud = None self.art_loaded = [] self.drag_objects = {} "Offsets for objects player is edit-dragging" self.last_state_loaded = DEFAULT_STATE_FILENAME def play_music(self, music_filename, fade_in_time=0): "Play given music file in any SDL2_mixer-supported format." music_filename = self.game_dir + SOUNDS_DIR + music_filename self.app.al.set_music(music_filename) self.app.al.start_music(music_filename) def pause_music(self): self.app.al.pause_music() def resume_music(self): self.app.al.resume_music() def stop_music(self): "Stop any currently playing music." self.app.al.stop_all_music() def is_music_playing(self): "Return True if there is music playing." return self.app.al.is_music_playing() def pick_next_object_at(self, x, y): "Return next unselected object at given point." objects = self.get_objects_at(x, y) # early out if len(objects) == 0: return None # don't bother cycling if only one object found if len(objects) == 1 and objects[0].selectable and \ not objects[0] in self.selected_objects: return objects[0] # cycle through objects at point til an unselected one is found for obj in objects: if not obj.selectable: continue if obj in self.selected_objects: continue return obj return None def get_objects_at(self, x, y, allow_locked=False): "Return list of all objects whose bounds fall within given point." objects = [] for obj in self.objects.values(): if obj.locked and not allow_locked: continue # only allow selecting of visible objects # (can still be selected via list panel) if obj.visible and obj.is_point_inside(x, y): objects.append(obj) # sort objects in Z, highest first objects.sort(key=lambda obj: obj.z, reverse=True) return objects def select_click(self): x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # remember last place we clicked self.last_mouse_click_x, self.last_mouse_click_y = x, y if self.classname_to_spawn: new_obj = self.spawn_object_of_class(self.classname_to_spawn, x, y) if self.current_room: self.current_room.add_object(new_obj) self.app.ui.message_line.post_line('Spawned %s' % new_obj.name) return objects = self.get_objects_at(x, y) next_obj = self.pick_next_object_at(x, y) if len(objects) == 0: self.deselect_all() # ctrl: unselect first selected object found under mouse elif self.app.il.ctrl_pressed: for obj in self.selected_objects: if obj in objects: self.deselect_object(obj) break # shift: add to current selection elif self.app.il.shift_pressed: self.select_object(next_obj) # no mod keys: select only object under cursor, deselect all if none elif not next_obj and len(objects) == 0: self.deselect_all() # special case: must use shift-click for objects # beneath (lower Z) topmost elif next_obj is not objects[0]: pass else: self.select_object(next_obj) # remember object offsets from cursor for dragging for obj in self.selected_objects: self.drag_objects[obj.name] = (obj.x - x, obj.y - y, obj.z - z) def clicked(self, button): # if edit UI is up, select stuff if self.app.ui.is_game_edit_ui_visible(): if button == sdl2.SDL_BUTTON_LEFT: self.select_click() # else pass clicks to any objects under mouse else: x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # 'locked" only relevant to edit mode, ignore it if in play mode objects = self.get_objects_at(x, y, allow_locked=True) for obj in objects: if obj.handle_mouse_events and \ (not obj.locked or not self.app.can_edit): obj.clicked(button, x, y) if obj.consume_mouse_events: break def select_unclick(self): # clicks on UI are consumed and flag world to not accept unclicks # (keeps unclicks after dialog dismiss from deselecting objects) if self.last_click_on_ui: self.last_click_on_ui = False # clear drag objects, since we're leaving valid drag context # fixes unwanted drag after eg ESC exiting a menu self.drag_objects.clear() return # if we're clicking to spawn something, don't drag/select if self.classname_to_spawn: return x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # remember selected objects now, they might be deselected but still # need to have their collision turned back on. selected_objects = self.selected_objects[:] if len(self.selected_objects) > 0 and not self.app.il.shift_pressed: # if mouse has traveled much since click, deselect all objects # except one mouse is over. dx = self.last_mouse_click_x - x dy = self.last_mouse_click_y - y if math.sqrt(dx ** 2 + dy ** 2) < 1.5: for obj in self.get_objects_at(x, y): if obj in self.selected_objects: self.deselect_all() self.select_object(obj) break # end drag, forget drag object offsets self.drag_objects.clear() for obj in selected_objects: obj.enable_collision() if obj.collision_shape_type == collision.CST_TILE: obj.collision.create_shapes() obj.collision.update() def unclicked(self, button): if self.app.ui.is_game_edit_ui_visible(): if button == sdl2.SDL_BUTTON_LEFT: self.select_unclick() else: x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) objects = self.get_objects_at(x, y) for obj in objects: if obj.handle_mouse_events: obj.unclicked(button, x, y) def check_hovers(self): "Update objects on their mouse hover status" x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) new_hovers = self.get_objects_at(x, y) # if this object will be selected on left click; draw bounds & label if self.app.ui.is_game_edit_ui_visible(): next_obj = self.pick_next_object_at(x, y) self.hovered_focus_object = next_obj # if in play mode, notify objects who have begun to be hovered else: for obj in new_hovers: if obj.handle_mouse_events and not obj in self.hovered_objects: obj.hovered(x, y) # check for objects un-hovered by this move for obj in self.hovered_objects: if obj.handle_mouse_events and not obj in new_hovers: obj.unhovered(x, y) self.hovered_objects = new_hovers def mouse_wheeled(self, wheel_y): x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) objects = self.get_objects_at(x, y, allow_locked=True) for obj in objects: if obj.handle_mouse_events and \ (not obj.locked or not self.app.can_edit): obj.mouse_wheeled(wheel_y) if obj.consume_mouse_events: break def mouse_moved(self, dx, dy): if self.app.ui.active_dialog: return # bail if mouse didn't move (in world space - include camera) last input if self.app.mouse_dx == 0 and self.app.mouse_dy == 0 and \ not self.camera.moved_this_frame: return # if last onclick was a UI element, don't drag if self.last_click_on_ui: return self.check_hovers() # not dragging anything? if len(self.selected_objects) == 0: return # 0-length drags cause unwanted snapping if dx == 0 and dy == 0: return # set dragged objects to mouse + offset from mouse when drag started x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) for obj_name,offset in self.drag_objects.items(): obj = self.objects[obj_name] if obj.locked: continue obj.disable_collision() obj.x = x + offset[0] obj.y = y + offset[1] obj.z = z + offset[2] if self.object_grid_snap: obj.x = round(obj.x) obj.y = round(obj.y) # if odd width/height, origin will be between quads and # edges will be off-grid; nudge so that edges are on-grid if obj.art.width % 2 != 0: obj.x += obj.art.quad_width / 2 if obj.art.height % 2 != 0: obj.y += obj.art.quad_height / 2 def select_object(self, obj, force=False): "Add given object to our list of selected objects." if not self.app.can_edit: return if obj and (obj.selectable or force) and not obj in self.selected_objects: self.selected_objects.append(obj) self.app.ui.object_selection_changed() def deselect_object(self, obj): "Remove given object from our list of selected objects." if obj in self.selected_objects: self.selected_objects.remove(obj) self.app.ui.object_selection_changed() def deselect_all(self): "Deselect all objects." self.selected_objects = [] self.app.ui.object_selection_changed() def create_new_game(self, new_game_dir, new_game_title): "Create appropriate dirs and files for a new game, return success." self.unload_game() new_dir = self.app.documents_dir + TOP_GAME_DIR + new_game_dir + '/' if os.path.exists(new_dir): self.app.log('Game dir %s already exists!' % new_game_dir) return False os.mkdir(new_dir) os.mkdir(new_dir + ART_DIR) os.mkdir(new_dir + GAME_SCRIPTS_DIR) os.mkdir(new_dir + SOUNDS_DIR) os.mkdir(new_dir + CHARSET_DIR) os.mkdir(new_dir + PALETTE_DIR) # create a generic starter script with a GO and Player subclass f = open(new_dir + GAME_SCRIPTS_DIR + new_game_dir + '.py', 'w') f.write(STARTER_SCRIPT) f.close() # load game self.set_game_dir(new_game_dir) self.properties = self.spawn_object_of_class('WorldPropertiesObject') self.objects.update(self.new_objects) self.new_objects = {} # HACK: set some property defaults, no idea why they don't take :[ self.collision_enabled = self.properties.collision_enabled = True self.game_title = self.properties.game_title = new_game_title self.save_to_file(DEFAULT_STATE_FILENAME) return True def unload_game(self): "Unload currently loaded game." for obj in self.objects.values(): obj.destroy() self.cl.reset() self.camera.reset() self.player = None self.globals = None self.properties = None if self.hud: self.hud.destroy() self.hud = None self.objects, self.new_objects = {}, {} self.rooms = {} # art_loaded is cleared when game dir is set self.selected_objects = [] self.app.al.stop_all_music() def get_first_object_of_type(self, class_name, allow_subclasses=True): "Return first object found with given class name." c = self.get_class_by_name(class_name) for obj in self.objects.values(): # use isinstance so we catch subclasses if c and allow_subclasses: if isinstance(obj, c): return obj elif type(obj).__name__ == class_name: return obj def get_all_objects_of_type(self, class_name, allow_subclasses=True): "Return list of all objects found with given class name." c = self.get_class_by_name(class_name) objects = [] for obj in self.objects.values(): if c and allow_subclasses: if isinstance(obj, c): objects.append(obj) elif type(obj).__name__ == class_name: objects.append(obj) return objects def set_for_all_objects(self, name, value): "Set given variable name to given value for all objects." for obj in self.objects.values(): setattr(obj, name, value) def edit_art_for_selected(self): if len(self.selected_objects) == 0: return self.app.exit_game_mode() for obj in self.selected_objects: for art_filename in obj.get_all_art(): self.app.load_art_for_edit(art_filename) def move_selected(self, move_x, move_y, move_z): "Shift position of selected objects by given x,y,z amount." for obj in self.selected_objects: obj.x += move_x obj.y += move_y obj.z += move_z def reset_game(self): "Reset currently loaded game to last loaded state." if self.game_dir: self.load_game_state(self.last_state_loaded) def set_game_dir(self, dir_name, reset=False): "Load game from given game directory." if self.game_dir and dir_name == self.game_dir: self.load_game_state(DEFAULT_STATE_FILENAME) return # loading a new game, wipe art list self.art_loaded = [] # check in user documents dir first game_dir = TOP_GAME_DIR + dir_name doc_game_dir = self.app.documents_dir + game_dir for d in [doc_game_dir, game_dir]: if not os.path.exists(d): continue self.game_dir = d self.game_name = dir_name if not d.endswith('/'): self.game_dir += '/' self.app.log('Game data folder is now %s' % self.game_dir) # set sounds dir before loading state; some obj inits depend on it self.sounds_dir = self.game_dir + SOUNDS_DIR if reset: # load in a default state, eg start.gs self.load_game_state(DEFAULT_STATE_FILENAME) else: # if no reset load submodules into namespace from the get-go self._import_all() self.classes = self._get_all_loaded_classes() break if not self.game_dir: self.app.log("Couldn't find game directory %s" % dir_name) def _remove_non_current_game_modules(self): """ Remove modules from previously-loaded games from both sys and GameWorld's dicts. """ modules_to_remove = [] games_dir_prefix = TOP_GAME_DIR.replace('/', '') this_game_dir_prefix = '%s.%s' % (games_dir_prefix, self.game_name) for module_name in sys.modules: # remove any module that isn't for this game or part of its path if module_name != games_dir_prefix and \ module_name != this_game_dir_prefix and \ module_name.startswith(games_dir_prefix) and \ not module_name.startswith(this_game_dir_prefix + '.'): modules_to_remove.append(module_name) for module_name in modules_to_remove: sys.modules.pop(module_name) if module_name in self.modules: self.modules.pop(module_name) def _get_game_modules_list(self): "Return list of current game's modules from its scripts/ dir" # build list of module files modules_list = self.builtin_module_names[:] # create appropriately-formatted python import path module_path_prefix = '%s.%s.%s.' % (TOP_GAME_DIR.replace('/', ''), self.game_name, GAME_SCRIPTS_DIR.replace('/', '')) for filename in os.listdir(self.game_dir + GAME_SCRIPTS_DIR): # exclude emacs temp files and special world start script if not filename.endswith('.py'): continue if filename.startswith('.#'): continue new_module_name = module_path_prefix + filename.replace('.py', '') modules_list.append(new_module_name) return modules_list def _import_all(self): """ Populate GameWorld.modules with the modules GW._get_all_loaded_classes refers to when finding classes to spawn. """ # on first load, documents dir may not be in import path if not self.app.documents_dir in sys.path: sys.path += [self.app.documents_dir] # clean modules dict before (re)loading anything self._remove_non_current_game_modules() # make copy of old modules table for import vs reload check old_modules = self.modules.copy() self.modules = {} # load/reload new modules for module_name in self._get_game_modules_list(): try: # always reload built in modules if module_name in self.builtin_module_names or \ module_name in old_modules: m = importlib.reload(old_modules[module_name]) else: m = importlib.import_module(module_name) self.modules[module_name] = m except Exception as e: self.app.log_import_exception(e, module_name) def toggle_pause(self): "Toggles game pause state." if not self.allow_pause: return self.paused = not self.paused s = 'Game %spaused.' % ['un', ''][self.paused] self.app.ui.message_line.post_line(s) def get_elapsed_time(self): """ Return total time world has been running (ie not paused) in milliseconds. """ return self.app.get_elapsed_time() - self._pause_time def enable_player_camera_lock(self): if self.player: self.camera.focus_object = self.player def disable_player_camera_lock(self): # change only if player has focus if self.player and self.camera.focus_object is self.player: self.camera.focus_object = None def toggle_player_camera_lock(self): "Toggle whether camera is locked to player's location." if self.player and self.camera.focus_object is self.player: self.disable_player_camera_lock() else: self.enable_player_camera_lock() def toggle_grid_snap(self): self.object_grid_snap = not self.object_grid_snap def handle_input(self, event, shift_pressed, alt_pressed, ctrl_pressed): # pass event's key to any objects that want to handle it if not event.type in [sdl2.SDL_KEYDOWN, sdl2.SDL_KEYUP]: return key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode() key = key.lower() args = (key, shift_pressed, alt_pressed, ctrl_pressed) for obj in self.objects.values(): if obj.handle_key_events: if event.type == sdl2.SDL_KEYDOWN: self.try_object_method(obj, obj.handle_key_down, args) elif event.type == sdl2.SDL_KEYUP: self.try_object_method(obj, obj.handle_key_up, args) # TODO: handle_ functions for other types of input def get_colliders_at_point(self, point_x, point_y, include_object_names=[], include_class_names=[], exclude_object_names=[], exclude_class_names=[]): """ Return lists of colliding objects and shapes at given point that pass given filters. Includes are processed before excludes. """ whitelist_objects = len(include_object_names) > 0 whitelist_classes = len(include_class_names) > 0 blacklist_objects = len(exclude_object_names) > 0 blacklist_classes = len(exclude_class_names) > 0 # build list of objects to check # always ignore non-colliders colliders = [] for obj in self.objects.values(): if obj.should_collide(): colliders.append(obj) check_objects = [] if whitelist_objects or whitelist_classes: # list of class names -> list of classes include_classes = [self.get_class_by_name(class_name) for class_name in include_class_names] # only given objects of given classes if whitelist_objects and whitelist_classes: for obj_name in include_object_names: obj = self.objects[obj_name] for c in include_classes: if isinstance(obj, c) and not obj in check_objects: check_objects.append(obj) # only given objects of any class elif whitelist_objects and not whitelist_classes: check_objects += [self.objects[obj_name] for obj_name in include_object_names] # all colliders of given classes elif whitelist_classes: for obj in colliders: for c in include_classes: if isinstance(obj, c) and not obj in check_objects: check_objects.append(obj) else: check_objects = colliders[:] check_objects_unfiltered = check_objects[:] if blacklist_objects or blacklist_classes: exclude_classes = [self.get_class_by_name(class_name) for class_name in exclude_class_names] for obj in check_objects_unfiltered: if obj.name in exclude_object_names: check_objects.remove(obj) continue for c in exclude_classes: if isinstance(obj, c): check_objects.remove(obj) # check all objects that passed the filter(s) and build hit list hit_objects = [] hit_shapes = [] for obj in check_objects: # check bounds if not obj.is_point_inside(point_x, point_y): continue # point overlaps with tile collision? if obj.collision_shape_type == collision.CST_TILE: tile_x, tile_y = obj.get_tile_at_point(point_x, point_y) if (tile_x, tile_y) in obj.collision.tile_shapes: hit_objects.append(obj) hit_shapes.append(obj.collision.tile_shapes[(tile_x, tile_y)]) else: # point overlaps with primitive shape(s)? for shape in obj.collision.shapes: if shape.is_point_inside(point_y, point_y): hit_objects.append(obj) hit_shapes.append(shape) return hit_objects, hit_shapes def frame_begin(self): "Run at start of game loop iteration, before input/update/render." for obj in self.objects.values(): obj.art.updated_this_tick = False obj.frame_begin() def frame_update(self): for obj in self.objects.values(): obj.frame_update() def try_object_method(self, obj, method, args=()): "Try to run given object's given method, printing error if encountered." #print('running %s.%s' % (obj.name, method.__name__)) try: method(*args) if method.__name__ == 'update': obj.last_update_failed = False except Exception as e: if method.__name__ == 'update' and obj.last_update_failed: return obj.last_update_failed = True for line in traceback.format_exc().split('\n'): if line and not 'try_object_method' in line and line.strip() != 'method()': self.app.log(line.rstrip()) s = 'Error in %s.%s! See console.' % (obj.name, method.__name__) self.app.ui.message_line.post_line(s, 10, True) def pre_update(self): "Run GO and Room pre_updates before GameWorld.update" # add newly spawned objects to table self.objects.update(self.new_objects) self.new_objects = {} # run pre_first_update / pre_update on all appropriate objects for obj in self.objects.values(): if not obj.pre_first_update_run: self.try_object_method(obj, obj.pre_first_update) obj.pre_first_update_run = True # only run pre_update if not paused elif not self.paused and (obj.is_in_current_room() or obj.update_if_outside_room): # update timers # (copy timers list in case a timer removes itself from object) for timer in list(obj.timer_functions_pre_update.values())[:]: timer.update() obj.pre_update() for room in self.rooms.values(): if not room.pre_first_update_run: room.pre_first_update() room.pre_first_update_run = True def update(self): self.mouse_moved(self.app.mouse_dx, self.app.mouse_dy) if self.properties: self.properties.update_from_world() if not self.paused: # update objects based on movement, then resolve collisions for obj in self.objects.values(): if obj.is_in_current_room() or obj.update_if_outside_room: # update timers for timer in list(obj.timer_functions_update.values())[:]: timer.update() self.try_object_method(obj, obj.update) # subclass update may not call GameObject.update, # set last update time here once we're sure it's done obj.last_update_end = self.get_elapsed_time() if self.collision_enabled: self.cl.update() for room in self.rooms.values(): room.update() # display debug text for selected object(s) for obj in self.selected_objects: s = obj.get_debug_text() if s: self.app.ui.debug_text.post_lines(s) # remove objects marked for destruction to_destroy = [] for obj in self.objects.values(): if obj.should_destroy: to_destroy.append(obj.name) for obj_name in to_destroy: self.objects.pop(obj_name) if len(to_destroy) > 0: self.app.ui.edit_list_panel.items_changed() if self.hud: self.hud.update() if self.paused: self._pause_time += self.app.get_elapsed_time() - self.app.last_time else: self.updates += 1 def post_update(self): "Run after GameWorld.update." if self.paused: return for obj in self.objects.values(): if obj.is_in_current_room() or obj.update_if_outside_room: # update timers for timer in list(obj.timer_functions_post_update.values())[:]: timer.update() obj.post_update() def render(self): "Sort and draw all objects in Game Mode world." visible_objects = [] for obj in self.objects.values(): if obj.should_destroy: continue obj.update_renderables() # filter out objects outside current room here # (if no current room or object is in no rooms, render it always) in_room = self.current_room is None or obj.is_in_current_room() hide_debug = obj.is_debug and not self.draw_debug_objects # respect object's "should render at all" flag if obj.visible and not hide_debug and \ (self.show_all_rooms or in_room): visible_objects.append(obj) # # process non "Y sort" objects first # draw_order = [] collision_items = [] y_objects = [] for obj in visible_objects: if obj.y_sort: y_objects.append(obj) continue for i,z in enumerate(obj.art.layers_z): # ignore invisible layers if not obj.art.layers_visibility[i]: continue # only draw collision layer if show collision is set, OR if # "draw collision layer" is set if obj.collision_shape_type == collision.CST_TILE and \ obj.col_layer_name == obj.art.layer_names[i] and \ not obj.draw_col_layer: if obj.show_collision: item = RenderItem(obj, i, z + obj.z) collision_items.append(item) continue item = RenderItem(obj, i, z + obj.z) draw_order.append(item) draw_order.sort(key=lambda item: item.sort_value, reverse=False) # # process "Y sort" objects # y_objects.sort(key=lambda obj: obj.y, reverse=True) # draw layers of each Y-sorted object in Z order for obj in y_objects: items = [] for i,z in enumerate(obj.art.layers_z): if not obj.art.layers_visibility[i]: continue if obj.collision_shape_type == collision.CST_TILE and \ obj.col_layer_name == obj.art.layer_names[i]: if obj.show_collision: item = RenderItem(obj, i, 0) collision_items.append(item) continue item = RenderItem(obj, i, z) items.append(item) items.sort(key=lambda item: item.sort_value, reverse=False) for item in items: draw_order.append(item) for item in draw_order: item.obj.render(item.layer) self.grid.render() # # draw debug stuff: collision tiles, origins/boxes, debug lines # for item in collision_items: item.obj.render(item.layer) for obj in self.objects.values(): obj.render_debug() if self.hud and self.draw_hud: self.hud.render() def save_last_state(self): "Save over last loaded state." # strip down to base filename w/o extension :/ last_state = self.last_state_loaded last_state = os.path.basename(last_state) last_state = os.path.splitext(last_state)[0] self.save_to_file(last_state) def save_to_file(self, filename=None): "Save current world state to a file." objects = [] for obj in self.objects.values(): if obj.should_save: objects.append(obj.get_dict()) d = {'objects': objects} # save rooms if any exist if len(self.rooms) > 0: rooms = [room.get_dict() for room in self.rooms.values()] d['rooms'] = rooms if self.current_room: d['current_room'] = self.current_room.name if filename and filename != '': if not filename.endswith(STATE_FILE_EXTENSION): filename += '.' + STATE_FILE_EXTENSION filename = '%s%s' % (self.game_dir, filename) else: # state filename example: # games/mytestgame2/1431116386.gs timestamp = int(time.time()) filename = '%s%s.%s' % (self.game_dir, timestamp, STATE_FILE_EXTENSION) json.dump(d, open(filename, 'w'), sort_keys=True, indent=1) self.app.log('Saved game state %s to disk.' % filename) self.app.update_window_title() def _get_all_loaded_classes(self): """ Return classname,class dict of all GameObject classes in loaded modules. """ classes = {} for module in self.modules.values(): for k,v in module.__dict__.items(): # skip anything that's not a game class if not type(v) is type: continue base_classes = (game_object.GameObject, game_hud.GameHUD, game_room.GameRoom) # TODO: find out why above works but below doesn't!! O___O #base_classes = self.builtin_base_classes if issubclass(v, base_classes): classes[k] = v return classes def get_class_by_name(self, class_name): "Return Class object for given class name." return self.classes.get(class_name, None) def reset_object_in_place(self, obj): """ "Reset" given object to its class defaults. Actually destroys object in place and creates a new one. """ x, y = obj.x, obj.y obj_class = obj.__class__.__name__ spawned = self.spawn_object_of_class(obj_class, x, y) if spawned: self.app.log('%s reset to class defaults' % obj.name) if obj is self.player: self.player = spawned obj.destroy() def duplicate_selected_objects(self): "Duplicate all selected objects. Calls GW.duplicate_object" new_objects = [] for obj in self.selected_objects: new_objects.append(self.duplicate_object(obj)) # report on objects created if len(new_objects) == 1: self.app.log('%s created from %s' % (new_objects[0].name, obj.name)) elif len(new_objects) > 1: self.app.log('%s new objects created' % len(new_objects)) def duplicate_object(self, obj): "Create a duplicate of given object." d = obj.get_dict() # offset new object's location x, y = d['x'], d['y'] x += obj.renderable.width y -= obj.renderable.height d['x'], d['y'] = x, y # new object needs a unique name, use a temp one until object exists # for real and we can give it a proper, more-likely-to-be-unique one d['name'] = obj.name + ' TEMP COPY NAME' new_obj = self.spawn_object_from_data(d) # give object a non-duplicate name self.rename_object(new_obj, new_obj.get_unique_name()) # tell object's rooms about it for room_name in new_obj.rooms: self.world.rooms[room_name].add_object(new_obj) # update list after changes have been applied to object self.app.ui.edit_list_panel.items_changed() return new_obj def rename_object(self, obj, new_name): "Give specified object a new name. Doesn't accept already-in-use names." self.objects.update(self.new_objects) for other_obj in self.objects.values(): if not other_obj is self and other_obj.name == new_name: self.app.ui.message_line.post_line("Can't rename %s to %s, name already in use" % (obj.name, new_name)) return self.objects.pop(obj.name) old_name = obj.name obj.name = new_name self.objects[obj.name] = obj for room in self.rooms.values(): if obj in room.objects.values(): room.objects.pop(old_name) room.objects[obj.name] = self def spawn_object_of_class(self, class_name, x=None, y=None): "Spawn a new object of given class name at given location." if not class_name in self.classes: # no need for log here, import_all prints exception cause #self.app.log("Couldn't find class %s" % class_name) return d = {'class_name': class_name} if x is not None and y is not None: d['x'], d['y'] = x, y new_obj = self.spawn_object_from_data(d) self.app.ui.edit_list_panel.items_changed() return new_obj def spawn_object_from_data(self, object_data): "Spawn a new object with properties populated from given data dict." # load module and class class_name = object_data.get('class_name', None) if not class_name or not class_name in self.classes: # no need for log here, import_all prints exception cause #self.app.log("Couldn't parse class %s" % class_name) return obj_class = self.classes[class_name] # pass in object data new_object = obj_class(self, object_data) return new_object def add_room(self, new_room_name, new_room_classname='GameRoom'): "Add a new Room with given name of (optional) given class." if new_room_name in self.rooms: self.log('Room called %s already exists!' % new_room_name) return new_room_class = self.classes[new_room_classname] new_room = new_room_class(self, new_room_name) self.rooms[new_room.name] = new_room def remove_room(self, room_name): "Delete Room with given name." if not room_name in self.rooms: return room = self.rooms.pop(room_name) if room is self.current_room: self.current_room = None room.destroy() def change_room(self, new_room_name): "Set world's current active room to Room with given name." if not new_room_name in self.rooms: self.app.log("Couldn't change to missing room %s" % new_room_name) return old_room = self.current_room self.current_room = self.rooms[new_room_name] # tell old and new rooms they've been exited and entered, respectively if old_room: old_room.exited(self.current_room) self.current_room.entered(old_room) def rename_room(self, room, new_room_name): "Rename given Room to new given name." old_name = room.name room.name = new_room_name self.rooms.pop(old_name) self.rooms[new_room_name] = room # update all objects in this room for obj in self.objects.values(): if old_name in obj.rooms: obj.rooms.pop(old_name) obj.rooms[new_room_name] = room def load_game_state(self, filename=DEFAULT_STATE_FILENAME): "Load game state with given filename." if not os.path.exists(filename): filename = self.game_dir + filename if not filename.endswith(STATE_FILE_EXTENSION): filename += '.' + STATE_FILE_EXTENSION self.app.enter_game_mode() self.unload_game() # tell list panel to reset, its contents might get jostled self.app.ui.edit_list_panel.game_reset() # import all submodules and catalog classes self._import_all() self.classes = self._get_all_loaded_classes() try: d = json.load(open(filename)) #self.app.log('Loading game state %s...' % filename) except: self.app.log("Couldn't load game state from %s" % filename) #self.app.log(sys.exc_info()) return errors = False # spawn objects for obj_data in d['objects']: obj = self.spawn_object_from_data(obj_data) if not obj: errors = True # spawn a WorldPropertiesObject if one doesn't exist for obj in self.new_objects.values(): if type(obj).__name__ == self.properties_object_class_name: self.properties = obj break if not self.properties: self.properties = self.spawn_object_of_class(self.properties_object_class_name, 0, 0) # spawn a WorldGlobalStateObject self.globals = self.spawn_object_of_class(self.globals_object_class_name, 0, 0) # just for first update, merge new objects list into objects list self.objects.update(self.new_objects) # create rooms for room_data in d.get('rooms', []): # get room class room_class_name = room_data.get('class_name', None) room_class = self.classes.get(room_class_name, game_room.GameRoom) room = room_class(self, room_data['name'], room_data) self.rooms[room.name] = room start_room = self.rooms.get(d.get('current_room', None), None) if start_room: self.change_room(start_room.name) # spawn hud hud_class = self.classes[d.get('hud_class', self.hud_class_name)] self.hud = hud_class(self) self.hud_class_name = hud_class.__name__ if not errors and self.app.init_success: self.app.log('Loaded game state from %s' % filename) self.last_state_loaded = filename self.set_for_all_objects('show_collision', self.show_collision_all) self.set_for_all_objects('show_bounds', self.show_bounds_all) self.set_for_all_objects('show_origin', self.show_origin_all) self.app.update_window_title() self.app.ui.edit_list_panel.items_changed() #self.report() def report(self): "Print (not log) information about current world state." print('--------------\n%s report:' % self) obj_arts, obj_rends, obj_dbg_rends, obj_cols, obj_col_rends = 0, 0, 0, 0, 0 attachments = 0 # create merged dict of existing and just-spawned objects all_objects = self.objects.copy() all_objects.update(self.new_objects) print('%s objects:' % len(all_objects)) for obj in all_objects.values(): obj_arts += len(obj.arts) if obj.renderable is not None: obj_rends += 1 if obj.origin_renderable is not None: obj_dbg_rends += 1 if obj.bounds_renderable is not None: obj_dbg_rends += 1 if obj.collision: obj_cols += 1 obj_col_rends += len(obj.collision.renderables) attachments += len(obj.attachments) print(""" %s arts in objects, %s arts loaded, %s HUD arts, %s HUD renderables, %s renderables, %s debug renderables, %s collideables, %s collideable viz renderables, %s attachments""" % (obj_arts, len(self.art_loaded), len(self.hud.arts), len(self.hud.renderables), obj_rends, obj_dbg_rends, obj_cols, obj_col_rends, attachments)) self.cl.report() print('%s charsets loaded, %s palettes' % (len(self.app.charsets), len(self.app.palettes))) print('%s arts loaded for edit' % len(self.app.art_loaded_for_edit)) def toggle_all_origin_viz(self): "Toggle visibility of XYZ markers for all object origins." self.show_origin_all = not self.show_origin_all self.set_for_all_objects('show_origin', self.show_origin_all) def toggle_all_bounds_viz(self): "Toggle visibility of boxes for all object bounds." self.show_bounds_all = not self.show_bounds_all self.set_for_all_objects('show_bounds', self.show_bounds_all) def toggle_all_collision_viz(self): "Toggle visibility of debug lines for all object Collideables." self.show_collision_all = not self.show_collision_all self.set_for_all_objects('show_collision', self.show_collision_all) def destroy(self): self.unload_game() self.art_loaded = []
RenderItem(obj, layer, sort_value)
Return first index of value.
Raises ValueError if the value is not present.
Return number of occurrences of value.
View Source
class GameCamera(Camera): pan_friction = 0.2 use_bounds = False
Inherited Members
- camera.Camera
- Camera
- start_zoom
- mouse_pan_rate
- pan_accel
- base_max_pan_speed
- pan_min_pct
- pan_max_pct
- pan_zoom_increase_factor
- zoom_accel
- max_zoom_speed
- zoom_friction
- min_velocity
- fov
- near_z
- far_z
- reset
- calc_projection_matrix
- calc_view_matrix
- get_perspective_matrix
- get_ortho_matrix
- pan
- zoom
- get_current_zoom_pct
- get_base_zoom
- set_to_base_zoom
- zoom_proportional
- find_closest_zoom_extents
- toggle_zoom_extents
- window_resized
- set_zoom
- set_loc
- set_loc_from_obj
- set_for_art
- mouse_pan
- update
- log_loc
- start_x
- start_y
- x_tilt
- y_tilt
- min_x
- max_x
- min_y
- max_y
- min_zoom
- max_zoom
View Source
class GameWorld: """ Holds global state for Game Mode. Spawns, manages, and renders GameObjects. Properties serialized via WorldPropertiesObject. Global state can be controlled via a WorldGlobalsObject. """ game_title = 'Untitled Game' "Title for game, shown in window titlebar when not editing" gravity_x, gravity_y, gravity_z = 0., 0., 0. "Gravity applied to all objects who are affected by gravity." bg_color = [0., 0., 0., 1.] "OpenGL wiewport color to render behind everything else, ie the void." hud_class_name = 'GameHUD' "String name of HUD class to use" properties_object_class_name = 'WorldPropertiesObject' globals_object_class_name = 'WorldGlobalsObject' "String name of WorldGlobalsObject class to use." player_camera_lock = True "If True, camera will be locked to player's location." object_grid_snap = True # editable properties # TODO: #update_when_unfocused = False #"If True, game sim will update even when window doesn't have input focus" draw_hud = True allow_pause = True "If False, user cannot pause game sim" collision_enabled = True "If False, CollisionLord won't bother thinking about collision at all." # toggles for "show all" debug viz modes show_collision_all = False show_bounds_all = False show_origin_all = False show_all_rooms = False "If True, show all rooms not just current one." draw_debug_objects = True "If False, objects with is_debug=True won't be drawn." room_camera_changes_enabled = True "If True, snap camera to new room's associated camera marker." list_only_current_room_objects = False "If True, list UI will only show objects in current room." builtin_module_names = ['game_object', 'game_util_objects', 'game_hud', 'game_room'] builtin_base_classes = (game_object.GameObject, game_hud.GameHUD, game_room.GameRoom) def __init__(self, app): self.app = app "Application that created this world." self.game_dir = None "Currently loaded game directory." self.sounds_dir = None self.game_name = None "Game's internal name, based on its directory." self.selected_objects = [] self.hovered_objects = [] self.hovered_focus_object = None "Set by check_hovers(), to the object that will be selected if edit mode user clicks" self.last_click_on_ui = False self.last_mouse_click_x, self.last_mouse_click_y = 0, 0 self.properties = None "Our WorldPropertiesObject" self.globals = None "Our WorldGlobalsObject - not required" self.camera = GameCamera(self.app) self.grid = GameGrid(self.app) self.grid.visible = False self.player = None self.paused = False self._pause_time = 0 self.updates = 0 "Number of updates this we have performed." self.modules = {'game_object': game_object, 'game_util_objects': game_util_objects, 'game_hud': game_hud, 'game_room': game_room} self.classname_to_spawn = None self.objects = {} "Dict of objects by name:object" self.new_objects = {} "Dict of just-spawned objects, added to above on update() after spawn" self.rooms = {} "Dict of rooms by name:room" self.current_room = None self.cl = collision.CollisionLord(self) self.hud = None self.art_loaded = [] self.drag_objects = {} "Offsets for objects player is edit-dragging" self.last_state_loaded = DEFAULT_STATE_FILENAME def play_music(self, music_filename, fade_in_time=0): "Play given music file in any SDL2_mixer-supported format." music_filename = self.game_dir + SOUNDS_DIR + music_filename self.app.al.set_music(music_filename) self.app.al.start_music(music_filename) def pause_music(self): self.app.al.pause_music() def resume_music(self): self.app.al.resume_music() def stop_music(self): "Stop any currently playing music." self.app.al.stop_all_music() def is_music_playing(self): "Return True if there is music playing." return self.app.al.is_music_playing() def pick_next_object_at(self, x, y): "Return next unselected object at given point." objects = self.get_objects_at(x, y) # early out if len(objects) == 0: return None # don't bother cycling if only one object found if len(objects) == 1 and objects[0].selectable and \ not objects[0] in self.selected_objects: return objects[0] # cycle through objects at point til an unselected one is found for obj in objects: if not obj.selectable: continue if obj in self.selected_objects: continue return obj return None def get_objects_at(self, x, y, allow_locked=False): "Return list of all objects whose bounds fall within given point." objects = [] for obj in self.objects.values(): if obj.locked and not allow_locked: continue # only allow selecting of visible objects # (can still be selected via list panel) if obj.visible and obj.is_point_inside(x, y): objects.append(obj) # sort objects in Z, highest first objects.sort(key=lambda obj: obj.z, reverse=True) return objects def select_click(self): x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # remember last place we clicked self.last_mouse_click_x, self.last_mouse_click_y = x, y if self.classname_to_spawn: new_obj = self.spawn_object_of_class(self.classname_to_spawn, x, y) if self.current_room: self.current_room.add_object(new_obj) self.app.ui.message_line.post_line('Spawned %s' % new_obj.name) return objects = self.get_objects_at(x, y) next_obj = self.pick_next_object_at(x, y) if len(objects) == 0: self.deselect_all() # ctrl: unselect first selected object found under mouse elif self.app.il.ctrl_pressed: for obj in self.selected_objects: if obj in objects: self.deselect_object(obj) break # shift: add to current selection elif self.app.il.shift_pressed: self.select_object(next_obj) # no mod keys: select only object under cursor, deselect all if none elif not next_obj and len(objects) == 0: self.deselect_all() # special case: must use shift-click for objects # beneath (lower Z) topmost elif next_obj is not objects[0]: pass else: self.select_object(next_obj) # remember object offsets from cursor for dragging for obj in self.selected_objects: self.drag_objects[obj.name] = (obj.x - x, obj.y - y, obj.z - z) def clicked(self, button): # if edit UI is up, select stuff if self.app.ui.is_game_edit_ui_visible(): if button == sdl2.SDL_BUTTON_LEFT: self.select_click() # else pass clicks to any objects under mouse else: x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # 'locked" only relevant to edit mode, ignore it if in play mode objects = self.get_objects_at(x, y, allow_locked=True) for obj in objects: if obj.handle_mouse_events and \ (not obj.locked or not self.app.can_edit): obj.clicked(button, x, y) if obj.consume_mouse_events: break def select_unclick(self): # clicks on UI are consumed and flag world to not accept unclicks # (keeps unclicks after dialog dismiss from deselecting objects) if self.last_click_on_ui: self.last_click_on_ui = False # clear drag objects, since we're leaving valid drag context # fixes unwanted drag after eg ESC exiting a menu self.drag_objects.clear() return # if we're clicking to spawn something, don't drag/select if self.classname_to_spawn: return x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # remember selected objects now, they might be deselected but still # need to have their collision turned back on. selected_objects = self.selected_objects[:] if len(self.selected_objects) > 0 and not self.app.il.shift_pressed: # if mouse has traveled much since click, deselect all objects # except one mouse is over. dx = self.last_mouse_click_x - x dy = self.last_mouse_click_y - y if math.sqrt(dx ** 2 + dy ** 2) < 1.5: for obj in self.get_objects_at(x, y): if obj in self.selected_objects: self.deselect_all() self.select_object(obj) break # end drag, forget drag object offsets self.drag_objects.clear() for obj in selected_objects: obj.enable_collision() if obj.collision_shape_type == collision.CST_TILE: obj.collision.create_shapes() obj.collision.update() def unclicked(self, button): if self.app.ui.is_game_edit_ui_visible(): if button == sdl2.SDL_BUTTON_LEFT: self.select_unclick() else: x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) objects = self.get_objects_at(x, y) for obj in objects: if obj.handle_mouse_events: obj.unclicked(button, x, y) def check_hovers(self): "Update objects on their mouse hover status" x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) new_hovers = self.get_objects_at(x, y) # if this object will be selected on left click; draw bounds & label if self.app.ui.is_game_edit_ui_visible(): next_obj = self.pick_next_object_at(x, y) self.hovered_focus_object = next_obj # if in play mode, notify objects who have begun to be hovered else: for obj in new_hovers: if obj.handle_mouse_events and not obj in self.hovered_objects: obj.hovered(x, y) # check for objects un-hovered by this move for obj in self.hovered_objects: if obj.handle_mouse_events and not obj in new_hovers: obj.unhovered(x, y) self.hovered_objects = new_hovers def mouse_wheeled(self, wheel_y): x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) objects = self.get_objects_at(x, y, allow_locked=True) for obj in objects: if obj.handle_mouse_events and \ (not obj.locked or not self.app.can_edit): obj.mouse_wheeled(wheel_y) if obj.consume_mouse_events: break def mouse_moved(self, dx, dy): if self.app.ui.active_dialog: return # bail if mouse didn't move (in world space - include camera) last input if self.app.mouse_dx == 0 and self.app.mouse_dy == 0 and \ not self.camera.moved_this_frame: return # if last onclick was a UI element, don't drag if self.last_click_on_ui: return self.check_hovers() # not dragging anything? if len(self.selected_objects) == 0: return # 0-length drags cause unwanted snapping if dx == 0 and dy == 0: return # set dragged objects to mouse + offset from mouse when drag started x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) for obj_name,offset in self.drag_objects.items(): obj = self.objects[obj_name] if obj.locked: continue obj.disable_collision() obj.x = x + offset[0] obj.y = y + offset[1] obj.z = z + offset[2] if self.object_grid_snap: obj.x = round(obj.x) obj.y = round(obj.y) # if odd width/height, origin will be between quads and # edges will be off-grid; nudge so that edges are on-grid if obj.art.width % 2 != 0: obj.x += obj.art.quad_width / 2 if obj.art.height % 2 != 0: obj.y += obj.art.quad_height / 2 def select_object(self, obj, force=False): "Add given object to our list of selected objects." if not self.app.can_edit: return if obj and (obj.selectable or force) and not obj in self.selected_objects: self.selected_objects.append(obj) self.app.ui.object_selection_changed() def deselect_object(self, obj): "Remove given object from our list of selected objects." if obj in self.selected_objects: self.selected_objects.remove(obj) self.app.ui.object_selection_changed() def deselect_all(self): "Deselect all objects." self.selected_objects = [] self.app.ui.object_selection_changed() def create_new_game(self, new_game_dir, new_game_title): "Create appropriate dirs and files for a new game, return success." self.unload_game() new_dir = self.app.documents_dir + TOP_GAME_DIR + new_game_dir + '/' if os.path.exists(new_dir): self.app.log('Game dir %s already exists!' % new_game_dir) return False os.mkdir(new_dir) os.mkdir(new_dir + ART_DIR) os.mkdir(new_dir + GAME_SCRIPTS_DIR) os.mkdir(new_dir + SOUNDS_DIR) os.mkdir(new_dir + CHARSET_DIR) os.mkdir(new_dir + PALETTE_DIR) # create a generic starter script with a GO and Player subclass f = open(new_dir + GAME_SCRIPTS_DIR + new_game_dir + '.py', 'w') f.write(STARTER_SCRIPT) f.close() # load game self.set_game_dir(new_game_dir) self.properties = self.spawn_object_of_class('WorldPropertiesObject') self.objects.update(self.new_objects) self.new_objects = {} # HACK: set some property defaults, no idea why they don't take :[ self.collision_enabled = self.properties.collision_enabled = True self.game_title = self.properties.game_title = new_game_title self.save_to_file(DEFAULT_STATE_FILENAME) return True def unload_game(self): "Unload currently loaded game." for obj in self.objects.values(): obj.destroy() self.cl.reset() self.camera.reset() self.player = None self.globals = None self.properties = None if self.hud: self.hud.destroy() self.hud = None self.objects, self.new_objects = {}, {} self.rooms = {} # art_loaded is cleared when game dir is set self.selected_objects = [] self.app.al.stop_all_music() def get_first_object_of_type(self, class_name, allow_subclasses=True): "Return first object found with given class name." c = self.get_class_by_name(class_name) for obj in self.objects.values(): # use isinstance so we catch subclasses if c and allow_subclasses: if isinstance(obj, c): return obj elif type(obj).__name__ == class_name: return obj def get_all_objects_of_type(self, class_name, allow_subclasses=True): "Return list of all objects found with given class name." c = self.get_class_by_name(class_name) objects = [] for obj in self.objects.values(): if c and allow_subclasses: if isinstance(obj, c): objects.append(obj) elif type(obj).__name__ == class_name: objects.append(obj) return objects def set_for_all_objects(self, name, value): "Set given variable name to given value for all objects." for obj in self.objects.values(): setattr(obj, name, value) def edit_art_for_selected(self): if len(self.selected_objects) == 0: return self.app.exit_game_mode() for obj in self.selected_objects: for art_filename in obj.get_all_art(): self.app.load_art_for_edit(art_filename) def move_selected(self, move_x, move_y, move_z): "Shift position of selected objects by given x,y,z amount." for obj in self.selected_objects: obj.x += move_x obj.y += move_y obj.z += move_z def reset_game(self): "Reset currently loaded game to last loaded state." if self.game_dir: self.load_game_state(self.last_state_loaded) def set_game_dir(self, dir_name, reset=False): "Load game from given game directory." if self.game_dir and dir_name == self.game_dir: self.load_game_state(DEFAULT_STATE_FILENAME) return # loading a new game, wipe art list self.art_loaded = [] # check in user documents dir first game_dir = TOP_GAME_DIR + dir_name doc_game_dir = self.app.documents_dir + game_dir for d in [doc_game_dir, game_dir]: if not os.path.exists(d): continue self.game_dir = d self.game_name = dir_name if not d.endswith('/'): self.game_dir += '/' self.app.log('Game data folder is now %s' % self.game_dir) # set sounds dir before loading state; some obj inits depend on it self.sounds_dir = self.game_dir + SOUNDS_DIR if reset: # load in a default state, eg start.gs self.load_game_state(DEFAULT_STATE_FILENAME) else: # if no reset load submodules into namespace from the get-go self._import_all() self.classes = self._get_all_loaded_classes() break if not self.game_dir: self.app.log("Couldn't find game directory %s" % dir_name) def _remove_non_current_game_modules(self): """ Remove modules from previously-loaded games from both sys and GameWorld's dicts. """ modules_to_remove = [] games_dir_prefix = TOP_GAME_DIR.replace('/', '') this_game_dir_prefix = '%s.%s' % (games_dir_prefix, self.game_name) for module_name in sys.modules: # remove any module that isn't for this game or part of its path if module_name != games_dir_prefix and \ module_name != this_game_dir_prefix and \ module_name.startswith(games_dir_prefix) and \ not module_name.startswith(this_game_dir_prefix + '.'): modules_to_remove.append(module_name) for module_name in modules_to_remove: sys.modules.pop(module_name) if module_name in self.modules: self.modules.pop(module_name) def _get_game_modules_list(self): "Return list of current game's modules from its scripts/ dir" # build list of module files modules_list = self.builtin_module_names[:] # create appropriately-formatted python import path module_path_prefix = '%s.%s.%s.' % (TOP_GAME_DIR.replace('/', ''), self.game_name, GAME_SCRIPTS_DIR.replace('/', '')) for filename in os.listdir(self.game_dir + GAME_SCRIPTS_DIR): # exclude emacs temp files and special world start script if not filename.endswith('.py'): continue if filename.startswith('.#'): continue new_module_name = module_path_prefix + filename.replace('.py', '') modules_list.append(new_module_name) return modules_list def _import_all(self): """ Populate GameWorld.modules with the modules GW._get_all_loaded_classes refers to when finding classes to spawn. """ # on first load, documents dir may not be in import path if not self.app.documents_dir in sys.path: sys.path += [self.app.documents_dir] # clean modules dict before (re)loading anything self._remove_non_current_game_modules() # make copy of old modules table for import vs reload check old_modules = self.modules.copy() self.modules = {} # load/reload new modules for module_name in self._get_game_modules_list(): try: # always reload built in modules if module_name in self.builtin_module_names or \ module_name in old_modules: m = importlib.reload(old_modules[module_name]) else: m = importlib.import_module(module_name) self.modules[module_name] = m except Exception as e: self.app.log_import_exception(e, module_name) def toggle_pause(self): "Toggles game pause state." if not self.allow_pause: return self.paused = not self.paused s = 'Game %spaused.' % ['un', ''][self.paused] self.app.ui.message_line.post_line(s) def get_elapsed_time(self): """ Return total time world has been running (ie not paused) in milliseconds. """ return self.app.get_elapsed_time() - self._pause_time def enable_player_camera_lock(self): if self.player: self.camera.focus_object = self.player def disable_player_camera_lock(self): # change only if player has focus if self.player and self.camera.focus_object is self.player: self.camera.focus_object = None def toggle_player_camera_lock(self): "Toggle whether camera is locked to player's location." if self.player and self.camera.focus_object is self.player: self.disable_player_camera_lock() else: self.enable_player_camera_lock() def toggle_grid_snap(self): self.object_grid_snap = not self.object_grid_snap def handle_input(self, event, shift_pressed, alt_pressed, ctrl_pressed): # pass event's key to any objects that want to handle it if not event.type in [sdl2.SDL_KEYDOWN, sdl2.SDL_KEYUP]: return key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode() key = key.lower() args = (key, shift_pressed, alt_pressed, ctrl_pressed) for obj in self.objects.values(): if obj.handle_key_events: if event.type == sdl2.SDL_KEYDOWN: self.try_object_method(obj, obj.handle_key_down, args) elif event.type == sdl2.SDL_KEYUP: self.try_object_method(obj, obj.handle_key_up, args) # TODO: handle_ functions for other types of input def get_colliders_at_point(self, point_x, point_y, include_object_names=[], include_class_names=[], exclude_object_names=[], exclude_class_names=[]): """ Return lists of colliding objects and shapes at given point that pass given filters. Includes are processed before excludes. """ whitelist_objects = len(include_object_names) > 0 whitelist_classes = len(include_class_names) > 0 blacklist_objects = len(exclude_object_names) > 0 blacklist_classes = len(exclude_class_names) > 0 # build list of objects to check # always ignore non-colliders colliders = [] for obj in self.objects.values(): if obj.should_collide(): colliders.append(obj) check_objects = [] if whitelist_objects or whitelist_classes: # list of class names -> list of classes include_classes = [self.get_class_by_name(class_name) for class_name in include_class_names] # only given objects of given classes if whitelist_objects and whitelist_classes: for obj_name in include_object_names: obj = self.objects[obj_name] for c in include_classes: if isinstance(obj, c) and not obj in check_objects: check_objects.append(obj) # only given objects of any class elif whitelist_objects and not whitelist_classes: check_objects += [self.objects[obj_name] for obj_name in include_object_names] # all colliders of given classes elif whitelist_classes: for obj in colliders: for c in include_classes: if isinstance(obj, c) and not obj in check_objects: check_objects.append(obj) else: check_objects = colliders[:] check_objects_unfiltered = check_objects[:] if blacklist_objects or blacklist_classes: exclude_classes = [self.get_class_by_name(class_name) for class_name in exclude_class_names] for obj in check_objects_unfiltered: if obj.name in exclude_object_names: check_objects.remove(obj) continue for c in exclude_classes: if isinstance(obj, c): check_objects.remove(obj) # check all objects that passed the filter(s) and build hit list hit_objects = [] hit_shapes = [] for obj in check_objects: # check bounds if not obj.is_point_inside(point_x, point_y): continue # point overlaps with tile collision? if obj.collision_shape_type == collision.CST_TILE: tile_x, tile_y = obj.get_tile_at_point(point_x, point_y) if (tile_x, tile_y) in obj.collision.tile_shapes: hit_objects.append(obj) hit_shapes.append(obj.collision.tile_shapes[(tile_x, tile_y)]) else: # point overlaps with primitive shape(s)? for shape in obj.collision.shapes: if shape.is_point_inside(point_y, point_y): hit_objects.append(obj) hit_shapes.append(shape) return hit_objects, hit_shapes def frame_begin(self): "Run at start of game loop iteration, before input/update/render." for obj in self.objects.values(): obj.art.updated_this_tick = False obj.frame_begin() def frame_update(self): for obj in self.objects.values(): obj.frame_update() def try_object_method(self, obj, method, args=()): "Try to run given object's given method, printing error if encountered." #print('running %s.%s' % (obj.name, method.__name__)) try: method(*args) if method.__name__ == 'update': obj.last_update_failed = False except Exception as e: if method.__name__ == 'update' and obj.last_update_failed: return obj.last_update_failed = True for line in traceback.format_exc().split('\n'): if line and not 'try_object_method' in line and line.strip() != 'method()': self.app.log(line.rstrip()) s = 'Error in %s.%s! See console.' % (obj.name, method.__name__) self.app.ui.message_line.post_line(s, 10, True) def pre_update(self): "Run GO and Room pre_updates before GameWorld.update" # add newly spawned objects to table self.objects.update(self.new_objects) self.new_objects = {} # run pre_first_update / pre_update on all appropriate objects for obj in self.objects.values(): if not obj.pre_first_update_run: self.try_object_method(obj, obj.pre_first_update) obj.pre_first_update_run = True # only run pre_update if not paused elif not self.paused and (obj.is_in_current_room() or obj.update_if_outside_room): # update timers # (copy timers list in case a timer removes itself from object) for timer in list(obj.timer_functions_pre_update.values())[:]: timer.update() obj.pre_update() for room in self.rooms.values(): if not room.pre_first_update_run: room.pre_first_update() room.pre_first_update_run = True def update(self): self.mouse_moved(self.app.mouse_dx, self.app.mouse_dy) if self.properties: self.properties.update_from_world() if not self.paused: # update objects based on movement, then resolve collisions for obj in self.objects.values(): if obj.is_in_current_room() or obj.update_if_outside_room: # update timers for timer in list(obj.timer_functions_update.values())[:]: timer.update() self.try_object_method(obj, obj.update) # subclass update may not call GameObject.update, # set last update time here once we're sure it's done obj.last_update_end = self.get_elapsed_time() if self.collision_enabled: self.cl.update() for room in self.rooms.values(): room.update() # display debug text for selected object(s) for obj in self.selected_objects: s = obj.get_debug_text() if s: self.app.ui.debug_text.post_lines(s) # remove objects marked for destruction to_destroy = [] for obj in self.objects.values(): if obj.should_destroy: to_destroy.append(obj.name) for obj_name in to_destroy: self.objects.pop(obj_name) if len(to_destroy) > 0: self.app.ui.edit_list_panel.items_changed() if self.hud: self.hud.update() if self.paused: self._pause_time += self.app.get_elapsed_time() - self.app.last_time else: self.updates += 1 def post_update(self): "Run after GameWorld.update." if self.paused: return for obj in self.objects.values(): if obj.is_in_current_room() or obj.update_if_outside_room: # update timers for timer in list(obj.timer_functions_post_update.values())[:]: timer.update() obj.post_update() def render(self): "Sort and draw all objects in Game Mode world." visible_objects = [] for obj in self.objects.values(): if obj.should_destroy: continue obj.update_renderables() # filter out objects outside current room here # (if no current room or object is in no rooms, render it always) in_room = self.current_room is None or obj.is_in_current_room() hide_debug = obj.is_debug and not self.draw_debug_objects # respect object's "should render at all" flag if obj.visible and not hide_debug and \ (self.show_all_rooms or in_room): visible_objects.append(obj) # # process non "Y sort" objects first # draw_order = [] collision_items = [] y_objects = [] for obj in visible_objects: if obj.y_sort: y_objects.append(obj) continue for i,z in enumerate(obj.art.layers_z): # ignore invisible layers if not obj.art.layers_visibility[i]: continue # only draw collision layer if show collision is set, OR if # "draw collision layer" is set if obj.collision_shape_type == collision.CST_TILE and \ obj.col_layer_name == obj.art.layer_names[i] and \ not obj.draw_col_layer: if obj.show_collision: item = RenderItem(obj, i, z + obj.z) collision_items.append(item) continue item = RenderItem(obj, i, z + obj.z) draw_order.append(item) draw_order.sort(key=lambda item: item.sort_value, reverse=False) # # process "Y sort" objects # y_objects.sort(key=lambda obj: obj.y, reverse=True) # draw layers of each Y-sorted object in Z order for obj in y_objects: items = [] for i,z in enumerate(obj.art.layers_z): if not obj.art.layers_visibility[i]: continue if obj.collision_shape_type == collision.CST_TILE and \ obj.col_layer_name == obj.art.layer_names[i]: if obj.show_collision: item = RenderItem(obj, i, 0) collision_items.append(item) continue item = RenderItem(obj, i, z) items.append(item) items.sort(key=lambda item: item.sort_value, reverse=False) for item in items: draw_order.append(item) for item in draw_order: item.obj.render(item.layer) self.grid.render() # # draw debug stuff: collision tiles, origins/boxes, debug lines # for item in collision_items: item.obj.render(item.layer) for obj in self.objects.values(): obj.render_debug() if self.hud and self.draw_hud: self.hud.render() def save_last_state(self): "Save over last loaded state." # strip down to base filename w/o extension :/ last_state = self.last_state_loaded last_state = os.path.basename(last_state) last_state = os.path.splitext(last_state)[0] self.save_to_file(last_state) def save_to_file(self, filename=None): "Save current world state to a file." objects = [] for obj in self.objects.values(): if obj.should_save: objects.append(obj.get_dict()) d = {'objects': objects} # save rooms if any exist if len(self.rooms) > 0: rooms = [room.get_dict() for room in self.rooms.values()] d['rooms'] = rooms if self.current_room: d['current_room'] = self.current_room.name if filename and filename != '': if not filename.endswith(STATE_FILE_EXTENSION): filename += '.' + STATE_FILE_EXTENSION filename = '%s%s' % (self.game_dir, filename) else: # state filename example: # games/mytestgame2/1431116386.gs timestamp = int(time.time()) filename = '%s%s.%s' % (self.game_dir, timestamp, STATE_FILE_EXTENSION) json.dump(d, open(filename, 'w'), sort_keys=True, indent=1) self.app.log('Saved game state %s to disk.' % filename) self.app.update_window_title() def _get_all_loaded_classes(self): """ Return classname,class dict of all GameObject classes in loaded modules. """ classes = {} for module in self.modules.values(): for k,v in module.__dict__.items(): # skip anything that's not a game class if not type(v) is type: continue base_classes = (game_object.GameObject, game_hud.GameHUD, game_room.GameRoom) # TODO: find out why above works but below doesn't!! O___O #base_classes = self.builtin_base_classes if issubclass(v, base_classes): classes[k] = v return classes def get_class_by_name(self, class_name): "Return Class object for given class name." return self.classes.get(class_name, None) def reset_object_in_place(self, obj): """ "Reset" given object to its class defaults. Actually destroys object in place and creates a new one. """ x, y = obj.x, obj.y obj_class = obj.__class__.__name__ spawned = self.spawn_object_of_class(obj_class, x, y) if spawned: self.app.log('%s reset to class defaults' % obj.name) if obj is self.player: self.player = spawned obj.destroy() def duplicate_selected_objects(self): "Duplicate all selected objects. Calls GW.duplicate_object" new_objects = [] for obj in self.selected_objects: new_objects.append(self.duplicate_object(obj)) # report on objects created if len(new_objects) == 1: self.app.log('%s created from %s' % (new_objects[0].name, obj.name)) elif len(new_objects) > 1: self.app.log('%s new objects created' % len(new_objects)) def duplicate_object(self, obj): "Create a duplicate of given object." d = obj.get_dict() # offset new object's location x, y = d['x'], d['y'] x += obj.renderable.width y -= obj.renderable.height d['x'], d['y'] = x, y # new object needs a unique name, use a temp one until object exists # for real and we can give it a proper, more-likely-to-be-unique one d['name'] = obj.name + ' TEMP COPY NAME' new_obj = self.spawn_object_from_data(d) # give object a non-duplicate name self.rename_object(new_obj, new_obj.get_unique_name()) # tell object's rooms about it for room_name in new_obj.rooms: self.world.rooms[room_name].add_object(new_obj) # update list after changes have been applied to object self.app.ui.edit_list_panel.items_changed() return new_obj def rename_object(self, obj, new_name): "Give specified object a new name. Doesn't accept already-in-use names." self.objects.update(self.new_objects) for other_obj in self.objects.values(): if not other_obj is self and other_obj.name == new_name: self.app.ui.message_line.post_line("Can't rename %s to %s, name already in use" % (obj.name, new_name)) return self.objects.pop(obj.name) old_name = obj.name obj.name = new_name self.objects[obj.name] = obj for room in self.rooms.values(): if obj in room.objects.values(): room.objects.pop(old_name) room.objects[obj.name] = self def spawn_object_of_class(self, class_name, x=None, y=None): "Spawn a new object of given class name at given location." if not class_name in self.classes: # no need for log here, import_all prints exception cause #self.app.log("Couldn't find class %s" % class_name) return d = {'class_name': class_name} if x is not None and y is not None: d['x'], d['y'] = x, y new_obj = self.spawn_object_from_data(d) self.app.ui.edit_list_panel.items_changed() return new_obj def spawn_object_from_data(self, object_data): "Spawn a new object with properties populated from given data dict." # load module and class class_name = object_data.get('class_name', None) if not class_name or not class_name in self.classes: # no need for log here, import_all prints exception cause #self.app.log("Couldn't parse class %s" % class_name) return obj_class = self.classes[class_name] # pass in object data new_object = obj_class(self, object_data) return new_object def add_room(self, new_room_name, new_room_classname='GameRoom'): "Add a new Room with given name of (optional) given class." if new_room_name in self.rooms: self.log('Room called %s already exists!' % new_room_name) return new_room_class = self.classes[new_room_classname] new_room = new_room_class(self, new_room_name) self.rooms[new_room.name] = new_room def remove_room(self, room_name): "Delete Room with given name." if not room_name in self.rooms: return room = self.rooms.pop(room_name) if room is self.current_room: self.current_room = None room.destroy() def change_room(self, new_room_name): "Set world's current active room to Room with given name." if not new_room_name in self.rooms: self.app.log("Couldn't change to missing room %s" % new_room_name) return old_room = self.current_room self.current_room = self.rooms[new_room_name] # tell old and new rooms they've been exited and entered, respectively if old_room: old_room.exited(self.current_room) self.current_room.entered(old_room) def rename_room(self, room, new_room_name): "Rename given Room to new given name." old_name = room.name room.name = new_room_name self.rooms.pop(old_name) self.rooms[new_room_name] = room # update all objects in this room for obj in self.objects.values(): if old_name in obj.rooms: obj.rooms.pop(old_name) obj.rooms[new_room_name] = room def load_game_state(self, filename=DEFAULT_STATE_FILENAME): "Load game state with given filename." if not os.path.exists(filename): filename = self.game_dir + filename if not filename.endswith(STATE_FILE_EXTENSION): filename += '.' + STATE_FILE_EXTENSION self.app.enter_game_mode() self.unload_game() # tell list panel to reset, its contents might get jostled self.app.ui.edit_list_panel.game_reset() # import all submodules and catalog classes self._import_all() self.classes = self._get_all_loaded_classes() try: d = json.load(open(filename)) #self.app.log('Loading game state %s...' % filename) except: self.app.log("Couldn't load game state from %s" % filename) #self.app.log(sys.exc_info()) return errors = False # spawn objects for obj_data in d['objects']: obj = self.spawn_object_from_data(obj_data) if not obj: errors = True # spawn a WorldPropertiesObject if one doesn't exist for obj in self.new_objects.values(): if type(obj).__name__ == self.properties_object_class_name: self.properties = obj break if not self.properties: self.properties = self.spawn_object_of_class(self.properties_object_class_name, 0, 0) # spawn a WorldGlobalStateObject self.globals = self.spawn_object_of_class(self.globals_object_class_name, 0, 0) # just for first update, merge new objects list into objects list self.objects.update(self.new_objects) # create rooms for room_data in d.get('rooms', []): # get room class room_class_name = room_data.get('class_name', None) room_class = self.classes.get(room_class_name, game_room.GameRoom) room = room_class(self, room_data['name'], room_data) self.rooms[room.name] = room start_room = self.rooms.get(d.get('current_room', None), None) if start_room: self.change_room(start_room.name) # spawn hud hud_class = self.classes[d.get('hud_class', self.hud_class_name)] self.hud = hud_class(self) self.hud_class_name = hud_class.__name__ if not errors and self.app.init_success: self.app.log('Loaded game state from %s' % filename) self.last_state_loaded = filename self.set_for_all_objects('show_collision', self.show_collision_all) self.set_for_all_objects('show_bounds', self.show_bounds_all) self.set_for_all_objects('show_origin', self.show_origin_all) self.app.update_window_title() self.app.ui.edit_list_panel.items_changed() #self.report() def report(self): "Print (not log) information about current world state." print('--------------\n%s report:' % self) obj_arts, obj_rends, obj_dbg_rends, obj_cols, obj_col_rends = 0, 0, 0, 0, 0 attachments = 0 # create merged dict of existing and just-spawned objects all_objects = self.objects.copy() all_objects.update(self.new_objects) print('%s objects:' % len(all_objects)) for obj in all_objects.values(): obj_arts += len(obj.arts) if obj.renderable is not None: obj_rends += 1 if obj.origin_renderable is not None: obj_dbg_rends += 1 if obj.bounds_renderable is not None: obj_dbg_rends += 1 if obj.collision: obj_cols += 1 obj_col_rends += len(obj.collision.renderables) attachments += len(obj.attachments) print(""" %s arts in objects, %s arts loaded, %s HUD arts, %s HUD renderables, %s renderables, %s debug renderables, %s collideables, %s collideable viz renderables, %s attachments""" % (obj_arts, len(self.art_loaded), len(self.hud.arts), len(self.hud.renderables), obj_rends, obj_dbg_rends, obj_cols, obj_col_rends, attachments)) self.cl.report() print('%s charsets loaded, %s palettes' % (len(self.app.charsets), len(self.app.palettes))) print('%s arts loaded for edit' % len(self.app.art_loaded_for_edit)) def toggle_all_origin_viz(self): "Toggle visibility of XYZ markers for all object origins." self.show_origin_all = not self.show_origin_all self.set_for_all_objects('show_origin', self.show_origin_all) def toggle_all_bounds_viz(self): "Toggle visibility of boxes for all object bounds." self.show_bounds_all = not self.show_bounds_all self.set_for_all_objects('show_bounds', self.show_bounds_all) def toggle_all_collision_viz(self): "Toggle visibility of debug lines for all object Collideables." self.show_collision_all = not self.show_collision_all self.set_for_all_objects('show_collision', self.show_collision_all) def destroy(self): self.unload_game() self.art_loaded = []
Holds global state for Game Mode. Spawns, manages, and renders GameObjects. Properties serialized via WorldPropertiesObject. Global state can be controlled via a WorldGlobalsObject.
View Source
def __init__(self, app): self.app = app "Application that created this world." self.game_dir = None "Currently loaded game directory." self.sounds_dir = None self.game_name = None "Game's internal name, based on its directory." self.selected_objects = [] self.hovered_objects = [] self.hovered_focus_object = None "Set by check_hovers(), to the object that will be selected if edit mode user clicks" self.last_click_on_ui = False self.last_mouse_click_x, self.last_mouse_click_y = 0, 0 self.properties = None "Our WorldPropertiesObject" self.globals = None "Our WorldGlobalsObject - not required" self.camera = GameCamera(self.app) self.grid = GameGrid(self.app) self.grid.visible = False self.player = None self.paused = False self._pause_time = 0 self.updates = 0 "Number of updates this we have performed." self.modules = {'game_object': game_object, 'game_util_objects': game_util_objects, 'game_hud': game_hud, 'game_room': game_room} self.classname_to_spawn = None self.objects = {} "Dict of objects by name:object" self.new_objects = {} "Dict of just-spawned objects, added to above on update() after spawn" self.rooms = {} "Dict of rooms by name:room" self.current_room = None self.cl = collision.CollisionLord(self) self.hud = None self.art_loaded = [] self.drag_objects = {} "Offsets for objects player is edit-dragging" self.last_state_loaded = DEFAULT_STATE_FILENAME
Title for game, shown in window titlebar when not editing
OpenGL wiewport color to render behind everything else, ie the void.
String name of HUD class to use
String name of WorldGlobalsObject class to use.
If True, camera will be locked to player's location.
If False, user cannot pause game sim
If False, CollisionLord won't bother thinking about collision at all.
If True, show all rooms not just current one.
If False, objects with is_debug=True won't be drawn.
If True, snap camera to new room's associated camera marker.
If True, list UI will only show objects in current room.
Application that created this world.
Currently loaded game directory.
Game's internal name, based on its directory.
Set by check_hovers(), to the object that will be selected if edit mode user clicks
Our WorldPropertiesObject
Our WorldGlobalsObject - not required
Number of updates this we have performed.
Dict of objects by name:object
Dict of just-spawned objects, added to above on update() after spawn
Dict of rooms by name:room
Offsets for objects player is edit-dragging
View Source
def play_music(self, music_filename, fade_in_time=0): "Play given music file in any SDL2_mixer-supported format." music_filename = self.game_dir + SOUNDS_DIR + music_filename self.app.al.set_music(music_filename) self.app.al.start_music(music_filename)
Play given music file in any SDL2_mixer-supported format.
View Source
def pause_music(self): self.app.al.pause_music()
View Source
def resume_music(self): self.app.al.resume_music()
View Source
def stop_music(self): "Stop any currently playing music." self.app.al.stop_all_music()
Stop any currently playing music.
View Source
def is_music_playing(self): "Return True if there is music playing." return self.app.al.is_music_playing()
Return True if there is music playing.
View Source
def pick_next_object_at(self, x, y): "Return next unselected object at given point." objects = self.get_objects_at(x, y) # early out if len(objects) == 0: return None # don't bother cycling if only one object found if len(objects) == 1 and objects[0].selectable and \ not objects[0] in self.selected_objects: return objects[0] # cycle through objects at point til an unselected one is found for obj in objects: if not obj.selectable: continue if obj in self.selected_objects: continue return obj return None
Return next unselected object at given point.
View Source
def get_objects_at(self, x, y, allow_locked=False): "Return list of all objects whose bounds fall within given point." objects = [] for obj in self.objects.values(): if obj.locked and not allow_locked: continue # only allow selecting of visible objects # (can still be selected via list panel) if obj.visible and obj.is_point_inside(x, y): objects.append(obj) # sort objects in Z, highest first objects.sort(key=lambda obj: obj.z, reverse=True) return objects
Return list of all objects whose bounds fall within given point.
View Source
def select_click(self): x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # remember last place we clicked self.last_mouse_click_x, self.last_mouse_click_y = x, y if self.classname_to_spawn: new_obj = self.spawn_object_of_class(self.classname_to_spawn, x, y) if self.current_room: self.current_room.add_object(new_obj) self.app.ui.message_line.post_line('Spawned %s' % new_obj.name) return objects = self.get_objects_at(x, y) next_obj = self.pick_next_object_at(x, y) if len(objects) == 0: self.deselect_all() # ctrl: unselect first selected object found under mouse elif self.app.il.ctrl_pressed: for obj in self.selected_objects: if obj in objects: self.deselect_object(obj) break # shift: add to current selection elif self.app.il.shift_pressed: self.select_object(next_obj) # no mod keys: select only object under cursor, deselect all if none elif not next_obj and len(objects) == 0: self.deselect_all() # special case: must use shift-click for objects # beneath (lower Z) topmost elif next_obj is not objects[0]: pass else: self.select_object(next_obj) # remember object offsets from cursor for dragging for obj in self.selected_objects: self.drag_objects[obj.name] = (obj.x - x, obj.y - y, obj.z - z)
View Source
def clicked(self, button): # if edit UI is up, select stuff if self.app.ui.is_game_edit_ui_visible(): if button == sdl2.SDL_BUTTON_LEFT: self.select_click() # else pass clicks to any objects under mouse else: x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # 'locked" only relevant to edit mode, ignore it if in play mode objects = self.get_objects_at(x, y, allow_locked=True) for obj in objects: if obj.handle_mouse_events and \ (not obj.locked or not self.app.can_edit): obj.clicked(button, x, y) if obj.consume_mouse_events: break
View Source
def select_unclick(self): # clicks on UI are consumed and flag world to not accept unclicks # (keeps unclicks after dialog dismiss from deselecting objects) if self.last_click_on_ui: self.last_click_on_ui = False # clear drag objects, since we're leaving valid drag context # fixes unwanted drag after eg ESC exiting a menu self.drag_objects.clear() return # if we're clicking to spawn something, don't drag/select if self.classname_to_spawn: return x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # remember selected objects now, they might be deselected but still # need to have their collision turned back on. selected_objects = self.selected_objects[:] if len(self.selected_objects) > 0 and not self.app.il.shift_pressed: # if mouse has traveled much since click, deselect all objects # except one mouse is over. dx = self.last_mouse_click_x - x dy = self.last_mouse_click_y - y if math.sqrt(dx ** 2 + dy ** 2) < 1.5: for obj in self.get_objects_at(x, y): if obj in self.selected_objects: self.deselect_all() self.select_object(obj) break # end drag, forget drag object offsets self.drag_objects.clear() for obj in selected_objects: obj.enable_collision() if obj.collision_shape_type == collision.CST_TILE: obj.collision.create_shapes() obj.collision.update()
View Source
def unclicked(self, button): if self.app.ui.is_game_edit_ui_visible(): if button == sdl2.SDL_BUTTON_LEFT: self.select_unclick() else: x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) objects = self.get_objects_at(x, y) for obj in objects: if obj.handle_mouse_events: obj.unclicked(button, x, y)
View Source
def check_hovers(self): "Update objects on their mouse hover status" x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) new_hovers = self.get_objects_at(x, y) # if this object will be selected on left click; draw bounds & label if self.app.ui.is_game_edit_ui_visible(): next_obj = self.pick_next_object_at(x, y) self.hovered_focus_object = next_obj # if in play mode, notify objects who have begun to be hovered else: for obj in new_hovers: if obj.handle_mouse_events and not obj in self.hovered_objects: obj.hovered(x, y) # check for objects un-hovered by this move for obj in self.hovered_objects: if obj.handle_mouse_events and not obj in new_hovers: obj.unhovered(x, y) self.hovered_objects = new_hovers
Update objects on their mouse hover status
View Source
def mouse_wheeled(self, wheel_y): x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) objects = self.get_objects_at(x, y, allow_locked=True) for obj in objects: if obj.handle_mouse_events and \ (not obj.locked or not self.app.can_edit): obj.mouse_wheeled(wheel_y) if obj.consume_mouse_events: break
View Source
def mouse_moved(self, dx, dy): if self.app.ui.active_dialog: return # bail if mouse didn't move (in world space - include camera) last input if self.app.mouse_dx == 0 and self.app.mouse_dy == 0 and \ not self.camera.moved_this_frame: return # if last onclick was a UI element, don't drag if self.last_click_on_ui: return self.check_hovers() # not dragging anything? if len(self.selected_objects) == 0: return # 0-length drags cause unwanted snapping if dx == 0 and dy == 0: return # set dragged objects to mouse + offset from mouse when drag started x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) for obj_name,offset in self.drag_objects.items(): obj = self.objects[obj_name] if obj.locked: continue obj.disable_collision() obj.x = x + offset[0] obj.y = y + offset[1] obj.z = z + offset[2] if self.object_grid_snap: obj.x = round(obj.x) obj.y = round(obj.y) # if odd width/height, origin will be between quads and # edges will be off-grid; nudge so that edges are on-grid if obj.art.width % 2 != 0: obj.x += obj.art.quad_width / 2 if obj.art.height % 2 != 0: obj.y += obj.art.quad_height / 2
View Source
def select_object(self, obj, force=False): "Add given object to our list of selected objects." if not self.app.can_edit: return if obj and (obj.selectable or force) and not obj in self.selected_objects: self.selected_objects.append(obj) self.app.ui.object_selection_changed()
Add given object to our list of selected objects.
View Source
def deselect_object(self, obj): "Remove given object from our list of selected objects." if obj in self.selected_objects: self.selected_objects.remove(obj) self.app.ui.object_selection_changed()
Remove given object from our list of selected objects.
View Source
def deselect_all(self): "Deselect all objects." self.selected_objects = [] self.app.ui.object_selection_changed()
Deselect all objects.
View Source
def create_new_game(self, new_game_dir, new_game_title): "Create appropriate dirs and files for a new game, return success." self.unload_game() new_dir = self.app.documents_dir + TOP_GAME_DIR + new_game_dir + '/' if os.path.exists(new_dir): self.app.log('Game dir %s already exists!' % new_game_dir) return False os.mkdir(new_dir) os.mkdir(new_dir + ART_DIR) os.mkdir(new_dir + GAME_SCRIPTS_DIR) os.mkdir(new_dir + SOUNDS_DIR) os.mkdir(new_dir + CHARSET_DIR) os.mkdir(new_dir + PALETTE_DIR) # create a generic starter script with a GO and Player subclass f = open(new_dir + GAME_SCRIPTS_DIR + new_game_dir + '.py', 'w') f.write(STARTER_SCRIPT) f.close() # load game self.set_game_dir(new_game_dir) self.properties = self.spawn_object_of_class('WorldPropertiesObject') self.objects.update(self.new_objects) self.new_objects = {} # HACK: set some property defaults, no idea why they don't take :[ self.collision_enabled = self.properties.collision_enabled = True self.game_title = self.properties.game_title = new_game_title self.save_to_file(DEFAULT_STATE_FILENAME) return True
Create appropriate dirs and files for a new game, return success.
View Source
def unload_game(self): "Unload currently loaded game." for obj in self.objects.values(): obj.destroy() self.cl.reset() self.camera.reset() self.player = None self.globals = None self.properties = None if self.hud: self.hud.destroy() self.hud = None self.objects, self.new_objects = {}, {} self.rooms = {} # art_loaded is cleared when game dir is set self.selected_objects = [] self.app.al.stop_all_music()
Unload currently loaded game.
View Source
def get_first_object_of_type(self, class_name, allow_subclasses=True): "Return first object found with given class name." c = self.get_class_by_name(class_name) for obj in self.objects.values(): # use isinstance so we catch subclasses if c and allow_subclasses: if isinstance(obj, c): return obj elif type(obj).__name__ == class_name: return obj
Return first object found with given class name.
View Source
def get_all_objects_of_type(self, class_name, allow_subclasses=True): "Return list of all objects found with given class name." c = self.get_class_by_name(class_name) objects = [] for obj in self.objects.values(): if c and allow_subclasses: if isinstance(obj, c): objects.append(obj) elif type(obj).__name__ == class_name: objects.append(obj) return objects
Return list of all objects found with given class name.
View Source
def set_for_all_objects(self, name, value): "Set given variable name to given value for all objects." for obj in self.objects.values(): setattr(obj, name, value)
Set given variable name to given value for all objects.
View Source
def edit_art_for_selected(self): if len(self.selected_objects) == 0: return self.app.exit_game_mode() for obj in self.selected_objects: for art_filename in obj.get_all_art(): self.app.load_art_for_edit(art_filename)
View Source
def move_selected(self, move_x, move_y, move_z): "Shift position of selected objects by given x,y,z amount." for obj in self.selected_objects: obj.x += move_x obj.y += move_y obj.z += move_z
Shift position of selected objects by given x,y,z amount.
View Source
def reset_game(self): "Reset currently loaded game to last loaded state." if self.game_dir: self.load_game_state(self.last_state_loaded)
Reset currently loaded game to last loaded state.
View Source
def set_game_dir(self, dir_name, reset=False): "Load game from given game directory." if self.game_dir and dir_name == self.game_dir: self.load_game_state(DEFAULT_STATE_FILENAME) return # loading a new game, wipe art list self.art_loaded = [] # check in user documents dir first game_dir = TOP_GAME_DIR + dir_name doc_game_dir = self.app.documents_dir + game_dir for d in [doc_game_dir, game_dir]: if not os.path.exists(d): continue self.game_dir = d self.game_name = dir_name if not d.endswith('/'): self.game_dir += '/' self.app.log('Game data folder is now %s' % self.game_dir) # set sounds dir before loading state; some obj inits depend on it self.sounds_dir = self.game_dir + SOUNDS_DIR if reset: # load in a default state, eg start.gs self.load_game_state(DEFAULT_STATE_FILENAME) else: # if no reset load submodules into namespace from the get-go self._import_all() self.classes = self._get_all_loaded_classes() break if not self.game_dir: self.app.log("Couldn't find game directory %s" % dir_name)
Load game from given game directory.
View Source
def toggle_pause(self): "Toggles game pause state." if not self.allow_pause: return self.paused = not self.paused s = 'Game %spaused.' % ['un', ''][self.paused] self.app.ui.message_line.post_line(s)
Toggles game pause state.
View Source
def get_elapsed_time(self): """ Return total time world has been running (ie not paused) in milliseconds. """ return self.app.get_elapsed_time() - self._pause_time
Return total time world has been running (ie not paused) in milliseconds.
View Source
def enable_player_camera_lock(self): if self.player: self.camera.focus_object = self.player
View Source
def disable_player_camera_lock(self): # change only if player has focus if self.player and self.camera.focus_object is self.player: self.camera.focus_object = None
View Source
def toggle_player_camera_lock(self): "Toggle whether camera is locked to player's location." if self.player and self.camera.focus_object is self.player: self.disable_player_camera_lock() else: self.enable_player_camera_lock()
Toggle whether camera is locked to player's location.
View Source
def toggle_grid_snap(self): self.object_grid_snap = not self.object_grid_snap
View Source
def handle_input(self, event, shift_pressed, alt_pressed, ctrl_pressed): # pass event's key to any objects that want to handle it if not event.type in [sdl2.SDL_KEYDOWN, sdl2.SDL_KEYUP]: return key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode() key = key.lower() args = (key, shift_pressed, alt_pressed, ctrl_pressed) for obj in self.objects.values(): if obj.handle_key_events: if event.type == sdl2.SDL_KEYDOWN: self.try_object_method(obj, obj.handle_key_down, args) elif event.type == sdl2.SDL_KEYUP: self.try_object_method(obj, obj.handle_key_up, args) # TODO: handle_ functions for other types of input
View Source
def get_colliders_at_point(self, point_x, point_y, include_object_names=[], include_class_names=[], exclude_object_names=[], exclude_class_names=[]): """ Return lists of colliding objects and shapes at given point that pass given filters. Includes are processed before excludes. """ whitelist_objects = len(include_object_names) > 0 whitelist_classes = len(include_class_names) > 0 blacklist_objects = len(exclude_object_names) > 0 blacklist_classes = len(exclude_class_names) > 0 # build list of objects to check # always ignore non-colliders colliders = [] for obj in self.objects.values(): if obj.should_collide(): colliders.append(obj) check_objects = [] if whitelist_objects or whitelist_classes: # list of class names -> list of classes include_classes = [self.get_class_by_name(class_name) for class_name in include_class_names] # only given objects of given classes if whitelist_objects and whitelist_classes: for obj_name in include_object_names: obj = self.objects[obj_name] for c in include_classes: if isinstance(obj, c) and not obj in check_objects: check_objects.append(obj) # only given objects of any class elif whitelist_objects and not whitelist_classes: check_objects += [self.objects[obj_name] for obj_name in include_object_names] # all colliders of given classes elif whitelist_classes: for obj in colliders: for c in include_classes: if isinstance(obj, c) and not obj in check_objects: check_objects.append(obj) else: check_objects = colliders[:] check_objects_unfiltered = check_objects[:] if blacklist_objects or blacklist_classes: exclude_classes = [self.get_class_by_name(class_name) for class_name in exclude_class_names] for obj in check_objects_unfiltered: if obj.name in exclude_object_names: check_objects.remove(obj) continue for c in exclude_classes: if isinstance(obj, c): check_objects.remove(obj) # check all objects that passed the filter(s) and build hit list hit_objects = [] hit_shapes = [] for obj in check_objects: # check bounds if not obj.is_point_inside(point_x, point_y): continue # point overlaps with tile collision? if obj.collision_shape_type == collision.CST_TILE: tile_x, tile_y = obj.get_tile_at_point(point_x, point_y) if (tile_x, tile_y) in obj.collision.tile_shapes: hit_objects.append(obj) hit_shapes.append(obj.collision.tile_shapes[(tile_x, tile_y)]) else: # point overlaps with primitive shape(s)? for shape in obj.collision.shapes: if shape.is_point_inside(point_y, point_y): hit_objects.append(obj) hit_shapes.append(shape) return hit_objects, hit_shapes
Return lists of colliding objects and shapes at given point that pass given filters. Includes are processed before excludes.
View Source
def frame_begin(self): "Run at start of game loop iteration, before input/update/render." for obj in self.objects.values(): obj.art.updated_this_tick = False obj.frame_begin()
Run at start of game loop iteration, before input/update/render.
View Source
def frame_update(self): for obj in self.objects.values(): obj.frame_update()
View Source
def try_object_method(self, obj, method, args=()): "Try to run given object's given method, printing error if encountered." #print('running %s.%s' % (obj.name, method.__name__)) try: method(*args) if method.__name__ == 'update': obj.last_update_failed = False except Exception as e: if method.__name__ == 'update' and obj.last_update_failed: return obj.last_update_failed = True for line in traceback.format_exc().split('\n'): if line and not 'try_object_method' in line and line.strip() != 'method()': self.app.log(line.rstrip()) s = 'Error in %s.%s! See console.' % (obj.name, method.__name__) self.app.ui.message_line.post_line(s, 10, True)
Try to run given object's given method, printing error if encountered.
View Source
def pre_update(self): "Run GO and Room pre_updates before GameWorld.update" # add newly spawned objects to table self.objects.update(self.new_objects) self.new_objects = {} # run pre_first_update / pre_update on all appropriate objects for obj in self.objects.values(): if not obj.pre_first_update_run: self.try_object_method(obj, obj.pre_first_update) obj.pre_first_update_run = True # only run pre_update if not paused elif not self.paused and (obj.is_in_current_room() or obj.update_if_outside_room): # update timers # (copy timers list in case a timer removes itself from object) for timer in list(obj.timer_functions_pre_update.values())[:]: timer.update() obj.pre_update() for room in self.rooms.values(): if not room.pre_first_update_run: room.pre_first_update() room.pre_first_update_run = True
Run GO and Room pre_updates before GameWorld.update
View Source
def update(self): self.mouse_moved(self.app.mouse_dx, self.app.mouse_dy) if self.properties: self.properties.update_from_world() if not self.paused: # update objects based on movement, then resolve collisions for obj in self.objects.values(): if obj.is_in_current_room() or obj.update_if_outside_room: # update timers for timer in list(obj.timer_functions_update.values())[:]: timer.update() self.try_object_method(obj, obj.update) # subclass update may not call GameObject.update, # set last update time here once we're sure it's done obj.last_update_end = self.get_elapsed_time() if self.collision_enabled: self.cl.update() for room in self.rooms.values(): room.update() # display debug text for selected object(s) for obj in self.selected_objects: s = obj.get_debug_text() if s: self.app.ui.debug_text.post_lines(s) # remove objects marked for destruction to_destroy = [] for obj in self.objects.values(): if obj.should_destroy: to_destroy.append(obj.name) for obj_name in to_destroy: self.objects.pop(obj_name) if len(to_destroy) > 0: self.app.ui.edit_list_panel.items_changed() if self.hud: self.hud.update() if self.paused: self._pause_time += self.app.get_elapsed_time() - self.app.last_time else: self.updates += 1
View Source
def post_update(self): "Run after GameWorld.update." if self.paused: return for obj in self.objects.values(): if obj.is_in_current_room() or obj.update_if_outside_room: # update timers for timer in list(obj.timer_functions_post_update.values())[:]: timer.update() obj.post_update()
Run after GameWorld.update.
View Source
def render(self): "Sort and draw all objects in Game Mode world." visible_objects = [] for obj in self.objects.values(): if obj.should_destroy: continue obj.update_renderables() # filter out objects outside current room here # (if no current room or object is in no rooms, render it always) in_room = self.current_room is None or obj.is_in_current_room() hide_debug = obj.is_debug and not self.draw_debug_objects # respect object's "should render at all" flag if obj.visible and not hide_debug and \ (self.show_all_rooms or in_room): visible_objects.append(obj) # # process non "Y sort" objects first # draw_order = [] collision_items = [] y_objects = [] for obj in visible_objects: if obj.y_sort: y_objects.append(obj) continue for i,z in enumerate(obj.art.layers_z): # ignore invisible layers if not obj.art.layers_visibility[i]: continue # only draw collision layer if show collision is set, OR if # "draw collision layer" is set if obj.collision_shape_type == collision.CST_TILE and \ obj.col_layer_name == obj.art.layer_names[i] and \ not obj.draw_col_layer: if obj.show_collision: item = RenderItem(obj, i, z + obj.z) collision_items.append(item) continue item = RenderItem(obj, i, z + obj.z) draw_order.append(item) draw_order.sort(key=lambda item: item.sort_value, reverse=False) # # process "Y sort" objects # y_objects.sort(key=lambda obj: obj.y, reverse=True) # draw layers of each Y-sorted object in Z order for obj in y_objects: items = [] for i,z in enumerate(obj.art.layers_z): if not obj.art.layers_visibility[i]: continue if obj.collision_shape_type == collision.CST_TILE and \ obj.col_layer_name == obj.art.layer_names[i]: if obj.show_collision: item = RenderItem(obj, i, 0) collision_items.append(item) continue item = RenderItem(obj, i, z) items.append(item) items.sort(key=lambda item: item.sort_value, reverse=False) for item in items: draw_order.append(item) for item in draw_order: item.obj.render(item.layer) self.grid.render() # # draw debug stuff: collision tiles, origins/boxes, debug lines # for item in collision_items: item.obj.render(item.layer) for obj in self.objects.values(): obj.render_debug() if self.hud and self.draw_hud: self.hud.render()
Sort and draw all objects in Game Mode world.
View Source
def save_last_state(self): "Save over last loaded state." # strip down to base filename w/o extension :/ last_state = self.last_state_loaded last_state = os.path.basename(last_state) last_state = os.path.splitext(last_state)[0] self.save_to_file(last_state)
Save over last loaded state.
View Source
def save_to_file(self, filename=None): "Save current world state to a file." objects = [] for obj in self.objects.values(): if obj.should_save: objects.append(obj.get_dict()) d = {'objects': objects} # save rooms if any exist if len(self.rooms) > 0: rooms = [room.get_dict() for room in self.rooms.values()] d['rooms'] = rooms if self.current_room: d['current_room'] = self.current_room.name if filename and filename != '': if not filename.endswith(STATE_FILE_EXTENSION): filename += '.' + STATE_FILE_EXTENSION filename = '%s%s' % (self.game_dir, filename) else: # state filename example: # games/mytestgame2/1431116386.gs timestamp = int(time.time()) filename = '%s%s.%s' % (self.game_dir, timestamp, STATE_FILE_EXTENSION) json.dump(d, open(filename, 'w'), sort_keys=True, indent=1) self.app.log('Saved game state %s to disk.' % filename) self.app.update_window_title()
Save current world state to a file.
View Source
def get_class_by_name(self, class_name): "Return Class object for given class name." return self.classes.get(class_name, None)
Return Class object for given class name.
View Source
def reset_object_in_place(self, obj): """ "Reset" given object to its class defaults. Actually destroys object in place and creates a new one. """ x, y = obj.x, obj.y obj_class = obj.__class__.__name__ spawned = self.spawn_object_of_class(obj_class, x, y) if spawned: self.app.log('%s reset to class defaults' % obj.name) if obj is self.player: self.player = spawned obj.destroy()
"Reset" given object to its class defaults. Actually destroys object in place and creates a new one.
View Source
def duplicate_selected_objects(self): "Duplicate all selected objects. Calls GW.duplicate_object" new_objects = [] for obj in self.selected_objects: new_objects.append(self.duplicate_object(obj)) # report on objects created if len(new_objects) == 1: self.app.log('%s created from %s' % (new_objects[0].name, obj.name)) elif len(new_objects) > 1: self.app.log('%s new objects created' % len(new_objects))
Duplicate all selected objects. Calls GW.duplicate_object
View Source
def duplicate_object(self, obj): "Create a duplicate of given object." d = obj.get_dict() # offset new object's location x, y = d['x'], d['y'] x += obj.renderable.width y -= obj.renderable.height d['x'], d['y'] = x, y # new object needs a unique name, use a temp one until object exists # for real and we can give it a proper, more-likely-to-be-unique one d['name'] = obj.name + ' TEMP COPY NAME' new_obj = self.spawn_object_from_data(d) # give object a non-duplicate name self.rename_object(new_obj, new_obj.get_unique_name()) # tell object's rooms about it for room_name in new_obj.rooms: self.world.rooms[room_name].add_object(new_obj) # update list after changes have been applied to object self.app.ui.edit_list_panel.items_changed() return new_obj
Create a duplicate of given object.
View Source
def rename_object(self, obj, new_name): "Give specified object a new name. Doesn't accept already-in-use names." self.objects.update(self.new_objects) for other_obj in self.objects.values(): if not other_obj is self and other_obj.name == new_name: self.app.ui.message_line.post_line("Can't rename %s to %s, name already in use" % (obj.name, new_name)) return self.objects.pop(obj.name) old_name = obj.name obj.name = new_name self.objects[obj.name] = obj for room in self.rooms.values(): if obj in room.objects.values(): room.objects.pop(old_name) room.objects[obj.name] = self
Give specified object a new name. Doesn't accept already-in-use names.
View Source
def spawn_object_of_class(self, class_name, x=None, y=None): "Spawn a new object of given class name at given location." if not class_name in self.classes: # no need for log here, import_all prints exception cause #self.app.log("Couldn't find class %s" % class_name) return d = {'class_name': class_name} if x is not None and y is not None: d['x'], d['y'] = x, y new_obj = self.spawn_object_from_data(d) self.app.ui.edit_list_panel.items_changed() return new_obj
Spawn a new object of given class name at given location.
View Source
def spawn_object_from_data(self, object_data): "Spawn a new object with properties populated from given data dict." # load module and class class_name = object_data.get('class_name', None) if not class_name or not class_name in self.classes: # no need for log here, import_all prints exception cause #self.app.log("Couldn't parse class %s" % class_name) return obj_class = self.classes[class_name] # pass in object data new_object = obj_class(self, object_data) return new_object
Spawn a new object with properties populated from given data dict.
View Source
def add_room(self, new_room_name, new_room_classname='GameRoom'): "Add a new Room with given name of (optional) given class." if new_room_name in self.rooms: self.log('Room called %s already exists!' % new_room_name) return new_room_class = self.classes[new_room_classname] new_room = new_room_class(self, new_room_name) self.rooms[new_room.name] = new_room
Add a new Room with given name of (optional) given class.
View Source
def remove_room(self, room_name): "Delete Room with given name." if not room_name in self.rooms: return room = self.rooms.pop(room_name) if room is self.current_room: self.current_room = None room.destroy()
Delete Room with given name.
View Source
def change_room(self, new_room_name): "Set world's current active room to Room with given name." if not new_room_name in self.rooms: self.app.log("Couldn't change to missing room %s" % new_room_name) return old_room = self.current_room self.current_room = self.rooms[new_room_name] # tell old and new rooms they've been exited and entered, respectively if old_room: old_room.exited(self.current_room) self.current_room.entered(old_room)
Set world's current active room to Room with given name.
View Source
def rename_room(self, room, new_room_name): "Rename given Room to new given name." old_name = room.name room.name = new_room_name self.rooms.pop(old_name) self.rooms[new_room_name] = room # update all objects in this room for obj in self.objects.values(): if old_name in obj.rooms: obj.rooms.pop(old_name) obj.rooms[new_room_name] = room
Rename given Room to new given name.
View Source
def load_game_state(self, filename=DEFAULT_STATE_FILENAME): "Load game state with given filename." if not os.path.exists(filename): filename = self.game_dir + filename if not filename.endswith(STATE_FILE_EXTENSION): filename += '.' + STATE_FILE_EXTENSION self.app.enter_game_mode() self.unload_game() # tell list panel to reset, its contents might get jostled self.app.ui.edit_list_panel.game_reset() # import all submodules and catalog classes self._import_all() self.classes = self._get_all_loaded_classes() try: d = json.load(open(filename)) #self.app.log('Loading game state %s...' % filename) except: self.app.log("Couldn't load game state from %s" % filename) #self.app.log(sys.exc_info()) return errors = False # spawn objects for obj_data in d['objects']: obj = self.spawn_object_from_data(obj_data) if not obj: errors = True # spawn a WorldPropertiesObject if one doesn't exist for obj in self.new_objects.values(): if type(obj).__name__ == self.properties_object_class_name: self.properties = obj break if not self.properties: self.properties = self.spawn_object_of_class(self.properties_object_class_name, 0, 0) # spawn a WorldGlobalStateObject self.globals = self.spawn_object_of_class(self.globals_object_class_name, 0, 0) # just for first update, merge new objects list into objects list self.objects.update(self.new_objects) # create rooms for room_data in d.get('rooms', []): # get room class room_class_name = room_data.get('class_name', None) room_class = self.classes.get(room_class_name, game_room.GameRoom) room = room_class(self, room_data['name'], room_data) self.rooms[room.name] = room start_room = self.rooms.get(d.get('current_room', None), None) if start_room: self.change_room(start_room.name) # spawn hud hud_class = self.classes[d.get('hud_class', self.hud_class_name)] self.hud = hud_class(self) self.hud_class_name = hud_class.__name__ if not errors and self.app.init_success: self.app.log('Loaded game state from %s' % filename) self.last_state_loaded = filename self.set_for_all_objects('show_collision', self.show_collision_all) self.set_for_all_objects('show_bounds', self.show_bounds_all) self.set_for_all_objects('show_origin', self.show_origin_all) self.app.update_window_title() self.app.ui.edit_list_panel.items_changed() #self.report()
Load game state with given filename.
View Source
def report(self): "Print (not log) information about current world state." print('--------------\n%s report:' % self) obj_arts, obj_rends, obj_dbg_rends, obj_cols, obj_col_rends = 0, 0, 0, 0, 0 attachments = 0 # create merged dict of existing and just-spawned objects all_objects = self.objects.copy() all_objects.update(self.new_objects) print('%s objects:' % len(all_objects)) for obj in all_objects.values(): obj_arts += len(obj.arts) if obj.renderable is not None: obj_rends += 1 if obj.origin_renderable is not None: obj_dbg_rends += 1 if obj.bounds_renderable is not None: obj_dbg_rends += 1 if obj.collision: obj_cols += 1 obj_col_rends += len(obj.collision.renderables) attachments += len(obj.attachments) print(""" %s arts in objects, %s arts loaded, %s HUD arts, %s HUD renderables, %s renderables, %s debug renderables, %s collideables, %s collideable viz renderables, %s attachments""" % (obj_arts, len(self.art_loaded), len(self.hud.arts), len(self.hud.renderables), obj_rends, obj_dbg_rends, obj_cols, obj_col_rends, attachments)) self.cl.report() print('%s charsets loaded, %s palettes' % (len(self.app.charsets), len(self.app.palettes))) print('%s arts loaded for edit' % len(self.app.art_loaded_for_edit))
Print (not log) information about current world state.
View Source
def toggle_all_origin_viz(self): "Toggle visibility of XYZ markers for all object origins." self.show_origin_all = not self.show_origin_all self.set_for_all_objects('show_origin', self.show_origin_all)
Toggle visibility of XYZ markers for all object origins.
View Source
def toggle_all_bounds_viz(self): "Toggle visibility of boxes for all object bounds." self.show_bounds_all = not self.show_bounds_all self.set_for_all_objects('show_bounds', self.show_bounds_all)
Toggle visibility of boxes for all object bounds.
View Source
def toggle_all_collision_viz(self): "Toggle visibility of debug lines for all object Collideables." self.show_collision_all = not self.show_collision_all self.set_for_all_objects('show_collision', self.show_collision_all)
Toggle visibility of debug lines for all object Collideables.
View Source
def destroy(self): self.unload_game() self.art_loaded = []