class Jumping(CollisionSystem): ''' Make the character jump. Components used :func:`wecs.core.and_filter` 'character' | :class:`wecs.panda3d.character.CharacterController` | :class:`wecs.panda3d.character.JumpingMovement` | :class:`wecs.panda3d.character.FallingMovement` | :class:`wecs.panda3d.model.Scene` | :class:`wecs.panda3d.model.Clock` | :class:`wecs.panda3d.model.Model` ''' entity_filters = { 'character': and_filter([ Proxy('scene_node'), Proxy('character_node'), Clock, CharacterController, JumpingMovement, FallingMovement, ]), } proxies = { 'character_node': ProxyType(Model, 'node'), 'scene_node': ProxyType(Model, 'parent'), } def update(self, entities_by_filter): for entity in entities_by_filter['character']: controller = entity[CharacterController] falling_movement = entity[FallingMovement] jumping_movement = entity[JumpingMovement] if controller.jumps and falling_movement.ground_contact: falling_movement.inertia += jumping_movement.impulse
class CollisionSystem(System): proxies = { 'character_node': ProxyType(Model, 'node'), 'scene_node': ProxyType(Model, 'parent'), } def init_sensors(self, entity, movement): solids = movement.solids for tag, solid in solids.items(): solid['tag'] = tag if solid['shape'] is CollisionSphere: shape = CollisionSphere(solid['center'], solid['radius']) self.add_shape(entity, movement, solid, shape) elif solid['shape'] is CollisionCapsule: shape = CollisionCapsule( solid['end_a'], solid['end_b'], solid['radius'], ) self.add_shape(entity, movement, solid, shape) if movement.debug: scene_proxy = self.proxies['scene_node'] scene = entity[scene_proxy.component_type] scene_node = scene_proxy.field(entity) movement.traverser.show_collisions(scene_node) def add_shape(self, entity, movement, solid, shape): model_proxy = self.proxies['character_node'] model = entity[model_proxy.component_type] model_node = model_proxy.field(entity) node = NodePath( CollisionNode('{}-{}'.format( movement.tag_name, solid['tag'], ))) solid['node'] = node node.node().add_solid(shape) node.node().set_from_collide_mask(movement.from_collide_mask) node.node().set_into_collide_mask(movement.into_collide_mask) node.reparent_to(model_node) movement.traverser.add_collider(node, movement.queue) node.set_python_tag(movement.tag_name, movement) if 'debug' in solid and solid['debug']: node.show() def run_sensors(self, entity, movement): scene_proxy = self.proxies['scene_node'] scene = entity[scene_proxy.component_type] scene_node = scene_proxy.field(entity) movement.traverser.traverse(scene_node) movement.queue.sort_entries() movement.contacts = movement.queue.entries
class Take(System): entity_filters = { 'cursor': [Input, MouseOveringCamera, UserInterface, Inventory], } proxies = { 'model': ProxyType(wecs.panda3d.prototype.Model, 'node'), } input_context = 'interface' def update(self, entities_by_filter): for entity in entities_by_filter['cursor']: input = entity[Input] # Can the player currently take? if self.input_context in input.contexts: context = base.device_listener.read_context(self.input_context) # Does he take? if context.get('take', False): ui = entity[UserInterface] target = ui.targeted_entity # Does he point at a targetable entity? if target is not None: target_entity = self.world.get_entity(target) # Can it be taken? if Takeable in target_entity: # Then take it. self.take(entity, target_entity) def take(self, entity, target_entity): item_node = self.proxies['model'].field(target_entity) item_node.detach_node() # TODO: Reattach to inventory del entity[Takeable] entity[Inventory].items.append(target_entity._uid) entity[Inventory].dirty = True print(entity[Inventory].items)
class ErectCharacter(System): entity_filters = { 'character': [ Proxy('model_node'), CharacterController, ], } proxies = {'model_node': ProxyType(Model, 'node')} def update(self, entities_by_filter): for entity in entities_by_filter['character']: character = entity[CharacterController] model_node = self.proxies['model_node'].field(entity) up = character.gravity * -1 roll = math.atan(character.gravity.x / character.gravity.z) / (2.0 * math.pi) * 360.0 model_node.set_r(model_node, roll) # FIXME: We now shoud recalculate gravity by also rolling the vector. pitch = math.atan(character.gravity.y / character.gravity.z) / (2.0 * math.pi) * 360.0 model_node.set_p(model_node, -pitch) character.gravity = Vec3(0, 0, -character.gravity.length())
class Bumping(CollisionSystem): ''' Stop the character from moving through solid geometry. Components used :func:`wecs.core.and_filter` 'character' | :class:`wecs.panda3d.model.Scene` | :class:`wecs.panda3d.character.CharacterController` | :class:`wecs.panda3d.character.BumpingMovement` | :class:`wecs.panda3d.model.Clock` | :class:`wecs.panda3d.model.Model` ''' entity_filters = { 'character': and_filter([ Proxy('scene_node'), Proxy('character_node'), Clock, CharacterController, BumpingMovement, ]), } proxies = { 'character_node': ProxyType(Model, 'node'), 'scene_node': ProxyType(Model, 'parent'), } def enter_filter_character(self, entity): movement = entity[BumpingMovement] self.init_sensors(entity, movement) bumper = movement.solids['bumper'] node = bumper['node'] movement.queue.add_collider(node, node) def update(self, entities_by_filter): for entity in entities_by_filter['character']: scene_proxy = self.proxies['scene_node'] scene = entity[scene_proxy.component_type] scene_node = scene_proxy.field(entity) character = entity[CharacterController] movement = entity[BumpingMovement] bumper = movement.solids['bumper'] node = bumper['node'] node.set_pos(character.translation) movement.traverser.traverse(scene_node) character.translation = node.get_pos()
class AnimateCharacter(System): entity_filters = { 'animated_character': and_filter([Proxy('actor'), Animation, CharacterController]) } proxies = {'actor': ProxyType(Actor, 'node')} def update(self, entities_by_filter): for entity in entities_by_filter['animated_character']: controller = entity[CharacterController] animation = entity[Animation] actor_proxy = self.proxies['actor'] actor = actor_proxy.field(entity) if FallingMovement in entity: grounded = entity[FallingMovement].ground_contact else: grounded = False initial = "idle" if not grounded: if controller.translation.z > 0.1: initial = "jump" elif controller.translation.z < -0.1: initial = "fall" elif controller.crouches: initial = "crouch" animation.to_play = [initial, "walk_forward", "run_forward"] # TODO: bad constant, 1.4? Should be fixed in animation # when the right value is found in lab. forward_speed = abs(controller.translation.y * 1.4) idle = max(0, (1 - forward_speed)) walk = 1 - abs(forward_speed - 0.5) * 2 run = max(0, forward_speed * 2 - 1) blends = [idle, walk, run] # sideways movement # TODO: same here, another constant. Fix in animation after lab. strafe_speed = (controller.translation.x * 1.4) if not strafe_speed == 0: blends.append(abs(strafe_speed)) if strafe_speed > 0: animation.to_play.append("walk_right") elif strafe_speed < 0: animation.to_play.append("walk_left") animation.framerate = (0.5 + (forward_speed + abs(strafe_speed))) # If walking backwards simply play the animation in reverse # TODO: Only do this when there's no animations for walking backwards. if controller.translation.y < 0: animation.framerate = -animation.framerate if controller.translation.z < -0.2: animation.framerate *= 0.2 animation.blends = blends
class TurningBackToCamera(System): ''' Turns character away from the camera. Components used :func:`wecs.core.and_filter` 'character' | :class:`wecs.panda3d.character.TurningBackToCameraMovement` | :class:`wecs.panda3d.character.CharacterController` | :class:`wecs.panda3d.model.Model` | :class:`wecs.panda3d.camera.ThirdPersonCamera` | :class:`wecs.panda3d.camera.TurntableCamera` | :class:`wecs.panda3d.model.Clock` ''' entity_filters = { 'character': and_filter([ Proxy('character_node'), Clock, AutomaticTurningMovement, TurningBackToCameraMovement, CharacterController, or_filter([ WalkingMovement, FloatingMovement, ]), Camera, ObjectCentricCameraMode, ]) } proxies = {'character_node': ProxyType(Model, 'node')} def update(self, entities_by_filter): for entity in entities_by_filter['character']: character = entity[CharacterController] camera = entity[Camera] center = entity[ObjectCentricCameraMode] turning = entity[TurningBackToCameraMovement] autoturning = entity[AutomaticTurningMovement] model_node = self.proxies['character_node'].field(entity) dt = entity[Clock].game_time autoturning.direction = model_node.get_relative_vector( camera.pivot, Vec3(0, 1, 0), ) if character.move.xy.length() >= turning.threshold * dt: autoturning.alignment = turning.view_axis_alignment else: autoturning.alignment = 0.0
class Animate(System): entity_filters = { 'animation': and_filter([ Proxy('actor'), Animation, ]) } proxies = {'actor': ProxyType(Actor, 'node')} def update(self, entities_by_filter): for entity in entities_by_filter['animation']: animation = entity[Animation] actor_proxy = self.proxies['actor'] actor = actor_proxy.field(entity) if not animation.playing == animation.to_play: if len(animation.to_play) > 0: actor.enableBlend() else: actor.disableBlend() # TODO: Don't stop and swap different animations instantly # but ease in (and bounce?) between them. # Stop animations not in to_play. for name in animation.playing: if name not in animation.to_play: actor.stop(name) actor.setControlEffect(name, 0) # Play newly added animations. for n, name in enumerate(animation.to_play): if name not in animation.playing: actor.loop(name) animation.playing = animation.to_play # Set blends each frame for b, blend in enumerate(animation.blends): if b < len(animation.playing): name = animation.playing[b] actor.setControlEffect(name, blend / len(animation.playing)) # Set framerate each frame for name in animation.playing: actor.setPlayRate(animation.framerate, name)
class BareTypeProxy(System): entity_filters = { 'test': Proxy('proxy'), } proxies = { 'proxy': ProxyType(TestComponent, 'foo'), } def update(self, entity_by_filters): for entity in entity_by_filters['test']: proxy = self.proxies['proxy'] # test = entity[proxy.component_type] token = proxy.field(entity) global token_out token_out = token
class ReorientInputBasedOnCamera(System): """ By default, player input is relative to the character. If it is viewed from a third person perspective, it is usually preferable to rotate them so that they align with the camera instead. """ entity_filters = { 'reorient': and_filter([ Camera, ObjectCentricCameraMode, CameraReorientedInput, Proxy('model'), CharacterController, ]), } proxies = { 'model': ProxyType(Model, 'node'), } def enter_filter_reorient(self, entity): camera = entity[Camera] reorienter = entity[CameraReorientedInput] reorienter.node.reparent_to(camera.camera) def exit_filter_reorient(self, entity): reorienter = entity[CameraReorientedInput] reorienter.node.detach_node() def update(self, entities_by_filter): for entity in entities_by_filter['reorient']: model_proxy = self.proxies['model'] model = entity[model_proxy.component_type] model_node = model_proxy.field(entity) character = entity[CharacterController] camera = entity[Camera] reorienter = entity[CameraReorientedInput] reorienter.node.set_p(model_node, 0) character.translation = model_node.get_relative_vector( reorienter.node, character.translation, )
class FaceMovement(System): entity_filters = { 'character': and_filter([ Proxy('geometry_node'), Clock, CharacterController, FacingMovement, ]), } proxies = {'geometry_node': ProxyType(Geometry, 'node')} def update(self, entities_by_filter): for entity in entities_by_filter['character']: geometry_proxy = self.proxies['geometry_node'] geometry = entity[geometry_proxy.component_type] geometry_node = geometry_proxy.field(entity) controller = entity[CharacterController] x, y, z = controller.last_translation_speed geometry_node.look_at(x, y, 0)
class ExecuteMovement(System): ''' Transcribe the final intended movement to the model, making it an actual movement. Components used :func:`wecs.core.and_filter` 'character' | :class:`wecs.panda3d.character.CharacterController` | :class:`wecs.panda3d.model.Model` | :class:`wecs.panda3d.model.Clock` ''' entity_filters = { 'character': and_filter([ Proxy('character_node'), Clock, CharacterController, ]), } proxies = {'character_node': ProxyType(Model, 'node')} def update(self, entities_by_filter): for entity in entities_by_filter['character']: model_proxy = self.proxies['character_node'] model = entity[model_proxy.component_type] character = entity[CharacterController] dt = entity[Clock].game_time # Translation: Simple self-relative movement for now. model_node = model_proxy.field(entity) model_node.set_pos(model_node, character.translation) character.last_translation_speed = character.translation / dt # Rotation if character.clamp_pitch: # Adjust intended pitch until it won't move you over a pole. preclamp_pitch = model_node.get_p() + character.rotation.y clamped_pitch = max(min(preclamp_pitch, 89.9), -89.9) character.rotation.y += clamped_pitch - preclamp_pitch model_node.set_hpr(model_node, character.rotation) character.last_rotation_speed = character.rotation / dt
class DirectlyIndicateDirection(System): entity_filters = { 'character': and_filter([ Proxy('model'), AutomaticTurningMovement, TwinStickMovement, CharacterController, Camera, ObjectCentricCameraMode, ]) } proxies = {'model': ProxyType(Model, 'node')} input_context = 'character_direction' def update(self, entities_by_filter): for entity in entities_by_filter['character']: input = entity[Input] if self.input_context in input.contexts: context = base.device_listener.read_context(self.input_context) self.process_input(context, entity) def process_input(self, context, entity): model_proxy = self.proxies['model'] model_node = model_proxy.field(entity) camera = entity[Camera] turning = entity[AutomaticTurningMovement] if context['direction'] is not None: turning.direction = model_node.get_relative_vector( camera.pivot, Vec3( context['direction'].x, context['direction'].y, 0, ), ) else: turning.direction = Vec3(0, 1, 0)
class AdjustGravity(System): entity_filters = { 'character': [ Proxy('model_node'), CharacterController, ], } proxies = {'model_node': ProxyType(Model, 'node')} def update(self, entities_by_filter): for entity in entities_by_filter['character']: character = entity[CharacterController] model_node = self.proxies['model_node'].field(entity) gravity_node = base.render attractor = model_node.get_pos(gravity_node) attractor.y = 0.0 attractor.normalize() attractor *= 9.81 local_gravity = model_node.get_relative_vector( gravity_node, attractor, ) character.gravity = local_gravity
from panda3d.core import Point3 from panda3d.core import Vec3 from panda3d.core import CollisionSphere # from wecs import cefconsole import wecs from wecs.core import ProxyType from wecs.aspects import Aspect from wecs.aspects import factory # from wecs.panda3d import debug from wecs.panda3d.constants import FALLING_MASK from wecs.panda3d.constants import BUMPING_MASK m_proxy = { 'model': ProxyType(wecs.panda3d.prototype.Model, 'node'), } cn_proxy = { 'character_node': ProxyType(wecs.panda3d.prototype.Model, 'node'), 'scene_node': ProxyType(wecs.panda3d.prototype.Model, 'parent'), } # Each frame, run these systems. This defines the game itself. system_types = [ wecs.panda3d.prototype.ManageModels, wecs.panda3d.camera.PrepareCameras(proxies=m_proxy), wecs.mechanics.clock.DetermineTimestep, wecs.panda3d.character.UpdateCharacter(proxies=cn_proxy), wecs.panda3d.character.Walking, wecs.panda3d.character.Bumping(proxies=cn_proxy), wecs.panda3d.character.Falling(proxies=cn_proxy),
class Falling(CollisionSystem): ''' Stop the character from falling through solid geometry. Components used :func:`wecs.core.and_filter` 'character' | :class:`wecs.panda3d.character.CharacterController` | :class:`wecs.panda3d.character.FallingMovement` | :class:`wecs.panda3d.model.Clock` | :class:`wecs.panda3d.model.Model` ''' entity_filters = { 'character': and_filter([ Proxy('scene_node'), Proxy('character_node'), Clock, CharacterController, FallingMovement, ]), } proxies = { 'character_node': ProxyType(Model, 'node'), 'scene_node': ProxyType(Model, 'parent'), } def enter_filter_character(self, entity): self.init_sensors(entity, entity[FallingMovement]) def update(self, entities_by_filter): for entity in entities_by_filter['character']: # Adjust the falling inertia by gravity, and position the # lifter collider. self.predict_falling(entity) # Find collisions with the ground. self.run_sensors(entity, entity[FallingMovement]) # Adjust the character's intended translation so that his # falling is stoppedby the ground. self.fall_and_land(entity) def predict_falling(self, entity): character = entity[CharacterController] model_proxy = self.proxies['character_node'] model = entity[model_proxy.component_type] model_node = model_proxy.field(entity) scene_proxy = self.proxies['scene_node'] scene = entity[scene_proxy.component_type] scene_node = scene_proxy.field(entity) clock = entity[Clock] controller = entity[CharacterController] falling_movement = entity[FallingMovement] # Adjust inertia by gravity frame_gravity = character.gravity * clock.game_time falling_movement.inertia += frame_gravity # Adjust lifter collider by inertia frame_inertia = falling_movement.inertia * clock.game_time lifter = falling_movement.solids['lifter'] node = lifter['node'] node.set_pos(controller.translation + frame_inertia) def fall_and_land(self, entity): falling_movement = entity[FallingMovement] clock = entity[Clock] controller = entity[CharacterController] falling_movement.ground_contact = False frame_falling = falling_movement.inertia * clock.game_time if len(falling_movement.contacts) > 0: lifter = falling_movement.solids['lifter']['node'] center = falling_movement.solids['lifter']['center'] radius = falling_movement.solids['lifter']['radius'] height_corrections = [] for contact in falling_movement.contacts: # FIXME: We're assuming a sphere here. # We only use the lower half of the sphere, no equator. if contact.get_surface_normal(lifter).get_z() > 0.0: contact_point = contact.get_surface_point(lifter) contact_point -= center # In solid's space xy = contact_point.xy expected_z = -sqrt(radius ** 2 - xy.length() ** 2) actual_z = contact_point.get_z() height_corrections.append(actual_z - expected_z) if height_corrections: frame_falling += Vec3(0, 0, max(height_corrections)) falling_movement.inertia = Vec3(0, 0, 0) falling_movement.ground_contact = True # Now we know how falling / lifting influences the movement controller.translation += frame_falling
class Inertiing(System): ''' Accelerate character, as opposed to an instantanious velocity. Components used :func:`wecs.core.and_filter` 'character' | :class:`wecs.panda3d.character.CharacterController` | :class:`wecs.panda3d.character.InertialMovement` | :class:`wecs.panda3d.model.Model` | :class:`wecs.model.clock` ''' entity_filters = { 'character': and_filter([ Proxy('character_node'), Clock, CharacterController, InertialMovement, ]), } proxies = {'character_node': ProxyType(Model, 'node')} def enter_filter_character(self, entity): movement = entity[InertialMovement] model_proxy = self.proxies['character_node'] model = entity[model_proxy.component_type] model_node = model_proxy.field(entity) movement.node.reparent_to(model_node) movement.node.set_hpr(0, 0, 0) def exit_filter_character(self, entity): # detach InertialMovement.node import pdb; pdb.set_trace() def update(self, entities_by_filter): for entity in entities_by_filter['character']: dt = entity[Clock].game_time model_proxy = self.proxies['character_node'] model = entity[model_proxy.component_type] model_node = model_proxy.field(entity) character = entity[CharacterController] inertia = entity[InertialMovement] # Usually you want to apply inertia only to x and y, and # ignore z, so we cache it. old_z = character.translation.z # We use inertia.node to represent "last frame's" model # orientation, scaled for how much inertia you'd like to # keep model-relative. Wow, what a sentence... # When you run forward and turn around, where should inertia # carry you? Physically, towards your new backward # direction. The opposite end of the scale of realism is # that your inertia vector turns around with you, and keeps # carrying you towards your new forward. # So if inertia.rotated_inertia = 1.0, inertia.node will # be aligned with the model, and thus the inertia vector # turns with you. If inertia.rotated_inertia = 0.0, # inertia.node will extrapolate the model's past rotation, # and the inertia vector will thus be kept still relative to # the surroundings. And if it is between those, it will # interpolate accordingly. inertia.node.set_hpr( -character.last_rotation_speed * dt * (1 - inertia.rotated_inertia), ) last_speed_vector = model_node.get_relative_vector( inertia.node, character.last_translation_speed, ) # Now we calculate the wanted speed difference, and scale it # within gameplay limits. wanted_speed_vector = character.translation / dt if inertia.delta_inputs: delta_v = wanted_speed_vector else: delta_v = wanted_speed_vector - last_speed_vector max_delta_v = inertia.acceleration * dt if delta_v.length() > max_delta_v: capped_delta_v = delta_v / delta_v.length() * max_delta_v else: capped_delta_v = delta_v character.translation = (last_speed_vector + capped_delta_v) * dt if inertia.ignore_z: character.translation.z = old_z
class AutomaticallyTurnTowardsDirection(System): ''' Turns character away from the camera. Components used :func:`wecs.core.and_filter` 'character' | :class:`wecs.panda3d.character.TurningBackToCameraMovement` | :class:`wecs.panda3d.character.CharacterController` | :class:`wecs.panda3d.model.Model` | :class:`wecs.panda3d.camera.ThirdPersonCamera` | :class:`wecs.panda3d.camera.TurntableCamera` | :class:`wecs.panda3d.model.Clock` ''' entity_filters = { 'character': and_filter([ Proxy('character_node'), Clock, AutomaticTurningMovement, CharacterController, or_filter([ WalkingMovement, FloatingMovement, ]), ]) } proxies = {'character_node': ProxyType(Model, 'node')} def update(self, entities_by_filter): for entity in entities_by_filter['character']: character = entity[CharacterController] turning = entity[AutomaticTurningMovement] model_node = self.proxies['character_node'].field(entity) if WalkingMovement in entity: movement = entity[WalkingMovement] else: movement = entity[FloatingMovement] dt = entity[Clock].game_time if turning.direction.xy.length() > 0.0: # How much would he have to adjust heading to face # towards the given vector? direc = Vec2(turning.direction.xy) angle = Vec2(0, 1).signed_angle_deg(direc) # How far can we turn this frame? Clamp to that. max_angle = movement.turning_speed if abs(angle) > max_angle: angle = copysign(max_angle, angle) # How much of that do we *want* to turn? angle *= turning.alignment # So let's turn, and clamp, in case we're already turning. old_rotation = character.rotation.x character.rotation.x += angle character.rotation.x = min( character.rotation.x, max_angle, ) character.rotation.x = max( character.rotation.x, -max_angle, ) # Since the camera rotates with the character, we need # to counteract that as well. delta_rotation = character.rotation.x - old_rotation turning.angle = delta_rotation # FIXME: This needs to be its own system camera = entity[Camera] camera.pivot.set_h(camera.pivot.get_h() - delta_rotation)
class AvatarUI(System): """ Command rules: if not embodied and not targeting_selection: selection(goto target) if not embodied and targeting_selection: selection(idle) if embodied and selecting and not targeting_selection: selection(goto target) if embodied and selecting and targeting_selection: selection(idle) if embodied and not selecting and targeting_selection: self(idle) if embodied and not selecting and not targeting_selection: self(goto target) """ entity_filters = { 'cursor': [Input, MouseOveringCamera, UserInterface], } proxies = { 'parent': ProxyType(wecs.panda3d.prototype.Model, 'parent'), } input_context = 'select_entity' def update(self, entities_by_filter): for entity in entities_by_filter['cursor']: input = entity[Input] if self.input_context in input.contexts: context = base.device_listener.read_context(self.input_context) self.process_input(entity, context) def process_input(self, entity, context): ui = entity[UserInterface] mouseover = entity[MouseOveringCamera] mouseovered_entity = None if mouseover.entity is not None: mouseovered_entity = self.world.get_entity(mouseover.entity) targeting_self = mouseover.entity == entity._uid selected_entity = None if ui.selected_entity is not None: selected_entity = self.world.get_entity(ui.selected_entity) target_entity = None if ui.targeted_entity is not None: target_entity = self.world.get_entity(ui.targeted_entity) point_coordinates = ui.point_coordinates targeting_selection = False if selected_entity is not None and ui.selected_entity == ui.targeted_entity: targeting_selection = True embodied = Embodiable in entity targeting_embodiable = None if mouseovered_entity is not None: targeting_embodiable = Embodiable in mouseovered_entity # Now we can evaluate the player's input. First, he clicked to # select. if context.get('select', False): if target_entity is None or Selectable not in target_entity: # Selecting while not pointing at a valid target # unselects. ui.selected_entity = None ui.select_indicator.detach_node() else: # But selecting a selectable entity... selects it. if not embodied or mouseover.entity != entity._uid: ui.selected_entity = target_entity._uid # The player instead clicked to give a command, and there is a # valid target, ... elif context.get('command', False): # 3rd person mode, giving command with to selected entity if not embodied and selected_entity and target_entity and not targeting_selection: action = ['walk_to_entity', mouseover.entity] if point_coordinates: action.append(point_coordinates) self.command(selected_entity, *action) if not embodied and targeting_selection: self.command(selected_entity, 'idle') if embodied and selected_entity and target_entity and not targeting_selection: action = ['walk_to_entity', mouseover.entity] if point_coordinates: action.append(point_coordinates) self.command(selected_entity, *action) if embodied and ui.selected_entity and target_entity and not targeting_selection: self.command(entity, 'idle') if embodied and targeting_selection: self.command(selected_entity, 'idle') if embodied and not ui.selected_entity and targeting_selection: self.command(entity, 'idle') if embodied and not ui.selected_entity and target_entity and not targeting_self: action = ['walk_to_entity', mouseover.entity] if point_coordinates: action.append(point_coordinates) self.command(entity, *action) if embodied and targeting_self and not ui.selected_entity: self.command(entity, 'idle') # Now the player clicked to dis-/embody... elif context.get('embody', False): if not embodied and not targeting_embodiable and selected_entity: self.embody(entity, selected_entity) if not embodied and targeting_embodiable: self.embody(entity, target_entity) if embodied and targeting_embodiable and not targeting_self: self.jump_body(entity, target_entity) if embodied and not targeting_embodiable: self.disembody(entity) def command(self, entity, *action): ai = entity[BehaviorAI] ai.behavior = action def embody(self, entity, target): pc_mind.add(target) third_person.add(target) self.world.destroy_entity(entity) def jump_body(self, entity, target): pc_mind.remove(entity) third_person.remove(entity) pc_mind.add(target) third_person.add(target) def disembody(self, entity): scene = self.proxies['parent'].field(entity) pos = entity[Camera].camera.get_pos(scene) hpr = entity[Camera].camera.get_hpr(scene) pos.z += 0.5 hpr.y = 0 hpr.z = 0 pc_mind.remove(entity) if first_person.in_entity(entity): first_person.remove(entity) if third_person.in_entity(entity): third_person.remove(entity) spawn_point = { wecs.panda3d.prototype.Model: dict( parent=scene, post_attach=lambda: wecs.panda3d.prototype.transform( pos=pos, hpr=hpr, ), ), } observer.add( self.world.create_entity(name="Observer"), overrides={ **spawn_point, }, )
class CollisionSystem(System): proxies = { 'character_node': ProxyType(Model, 'node'), 'scene_node': ProxyType(Model, 'parent'), } def init_sensors(self, entity, movement): solids = movement.solids for tag, solid in solids.items(): if 'shape' in solid: # Create solids from specification solid['tag'] = tag if solid['shape'] is CollisionSphere: shape = CollisionSphere(solid['center'], solid['radius']) elif solid['shape'] is CollisionCapsule: shape = CollisionCapsule( solid['end_a'], solid['end_b'], solid['radius'], ) else: raise Exception("Shape unsupported.") self.add_shape(entity, movement, solid, shape) else: # Fetch solids from model model_node = self.proxies['character_node'].field(entity) solid_nodes = model_node.find_all_matches( f'**/{movement.node_name}', ) # FIXME: This is a temporary prevention of sing multiple # solids in one movement system. This whole .py needs to # be refactored to account for multiple ones. assert len(solids) == 1 solid_node = solid_nodes[0] solid_node.reparent_to(model_node) solid['node'] = solid_node solid_objects = solid_node.node().get_solids() # FIXME: As above, please refactor this .py to account # for multiple solids. assert len(solids) == 1 solid_object = solid_objects[0] solid['shape'] = solid_object.type if solid['shape'] == CollisionSphere: solid['center'] = solid_object.center solid['radius'] = solid_object.radius # FIXME: This is mostly copypasta from add_solid, which # should be broken up. node = solid_node.node() node.set_from_collide_mask(movement.from_collide_mask) node.set_into_collide_mask(movement.into_collide_mask) movement.traverser.add_collider( solid_node, movement.queue, ) node.set_python_tag(movement.tag_name, movement) if movement.debug: solid_node.show() if movement.debug: scene_proxy = self.proxies['scene_node'] scene = entity[scene_proxy.component_type] scene_node = scene_proxy.field(entity) movement.traverser.show_collisions(scene_node) def add_shape(self, entity, movement, solid, shape): model_proxy = self.proxies['character_node'] model = entity[model_proxy.component_type] model_node = model_proxy.field(entity) node = NodePath( CollisionNode( f'{movement.tag_name}-{solid["tag"]}', ), ) solid['node'] = node node.node().add_solid(shape) node.node().set_from_collide_mask(movement.from_collide_mask) node.node().set_into_collide_mask(movement.into_collide_mask) node.reparent_to(model_node) movement.traverser.add_collider(node, movement.queue) node.set_python_tag(movement.tag_name, movement) if 'debug' in solid and solid['debug']: node.show() def run_sensors(self, entity, movement): scene_proxy = self.proxies['scene_node'] scene = entity[scene_proxy.component_type] scene_node = scene_proxy.field(entity) movement.traverser.traverse(scene_node) movement.queue.sort_entries() movement.contacts = movement.queue.entries
# These modules contain the actual game mechanics, which we are tying # together into an application in this file: import movement import paddles import ball logging.getLogger().setLevel(logging.DEBUG) if __name__ == '__main__': ECSShowBase() # ShowBase + base.ecs_world + base.add_system() base.disable_mouse() base.accept('escape', sys.exit) model_proxies = { 'model': ProxyType(wecs.panda3d.prototype.Model, 'node'), } systems = [ # Attach the entity's Model. This gives an entity a node as # presence in the scene graph. # Attach Geometry to the Model's node. wecs.panda3d.prototype.ManageModels(), # If the Paddle's size has changed, apply it to the Model. paddles.ResizePaddles(proxies=model_proxies), # Read player input and store it on Movement paddles.GivePaddlesMoveCommands(proxies=model_proxies), # Apply the Movement movement.MoveObject(proxies=model_proxies), # Did the paddle move too far? Back to the boundary with it! paddles.PaddleTouchesBoundary(), # If the Ball has hit the edge, it reflects off it.
class MouseOverOnEntity(System): entity_filters = { 'mouseoverable': [Proxy('model'), MouseOverable], 'mouseoverable_geometry': [Proxy('geometry'), MouseOverableGeometry], 'camera': [Camera, Input, MouseOveringCamera], } proxies = { 'model': ProxyType(Model, 'node'), 'geometry': ProxyType(Geometry, 'node'), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.traverser = CollisionTraverser() self.queue = CollisionHandlerQueue() self.picker_ray = CollisionRay() self.picker_node = CollisionNode('mouse ray') self.picker_node.add_solid(self.picker_ray) self.picker_node.set_from_collide_mask(MOUSEOVER_MASK) self.picker_node.set_into_collide_mask(0x0) self.picker_node_path = NodePath(self.picker_node) self.traverser.add_collider(self.picker_node_path, self.queue) def enter_filter_mouseoverable(self, entity): model_proxy = self.proxies['model'] model_node = model_proxy.field(entity) mouseoverable = entity[MouseOverable] into_node = CollisionNode('wecs_mouseoverable') into_node.add_solid(mouseoverable.solid) into_node.set_from_collide_mask(0x0) into_node.set_into_collide_mask(mouseoverable.mask) into_node_path = model_node.attach_new_node(into_node) into_node_path.set_python_tag('wecs_mouseoverable', entity._uid) def exit_filter_mouseoverable(self, entity): # FIXME: Undo all the other stuff that accumulated! entity[MouseOverable].solid.detach_node() def enter_filter_mouseoverable_geometry(self, entity): into_node = self.proxies['geometry'].field(entity) old_mask = into_node.get_collide_mask() new_mask = old_mask | entity[MouseOverableGeometry].mask into_node.set_collide_mask(new_mask) into_node.find('**/+GeomNode').set_python_tag('wecs_mouseoverable', entity._uid) def update(self, entities_by_filter): for entity in entities_by_filter['camera']: mouse_overing = entity[MouseOveringCamera] camera = entity[Camera] input = entity[Input] # Reset overed entity to None mouse_overing.entity = None mouse_overing.collision_entry = None requested = 'mouse_over' in input.contexts has_mouse = base.mouseWatcherNode.has_mouse() if requested and has_mouse: # Attach and align testing ray, and run collisions self.picker_node_path.reparent_to(camera.camera) mpos = base.mouseWatcherNode.get_mouse() self.picker_ray.set_from_lens( base.camNode, mpos.getX(), mpos.getY(), ) self.traverser.traverse(camera.camera.get_top()) # Remember reference to mouseovered entity, if any if self.queue.get_num_entries() > 0: self.queue.sort_entries() entry = self.queue.get_entry(0) picked_node = entry.get_into_node_path() picked_uid = picked_node.get_python_tag( 'wecs_mouseoverable') mouse_overing.entity = picked_uid mouse_overing.collision_entry = entry
class UpdateMouseOverUI(System): entity_filters = { 'cursor': [Input, MouseOveringCamera, UserInterface], } input_context = 'select_entity' proxies = { 'model': ProxyType(Model, 'node'), } def exit_filter_cursor(self, entity): ui = entity[UserInterface] ui.select_indicator.detach_node() ui.target_indicator.detach_node() ui.point_indicator.detach_node() def update(self, entities_by_filter): for entity in entities_by_filter['cursor']: ui = entity[UserInterface] mouseover = entity[MouseOveringCamera] mouseovered_entity_uid = mouseover.entity mouseovered_entity = None selected_entity = None target_entity = None point_coordinates = None # Does the currently selected entity still exist, and which # is it? if ui.selected_entity is not None: try: selected_entity = self.world.get_entity(ui.selected_entity) except NoSuchUID: ui.selected_entity = None # Update the selection indicator if selected_entity is not None: selected_node = self.proxies['model'].field(selected_entity) ui.select_indicator.reparent_to(selected_node) else: ui.select_indicator.detach_node() # Update target and point indicators. if mouseovered_entity_uid is not None: # If we are pointing at an entity, then target, point, # or do neither, depending on the entity. mouseovered_entity = self.world.get_entity( mouseovered_entity_uid) mouseovered_entity_node = self.proxies['model'].field( mouseovered_entity) if Targetable in mouseovered_entity: target_entity = mouseovered_entity ui.target_indicator.reparent_to(mouseovered_entity_node) ui.point_indicator.detach_node() elif Pointable in mouseovered_entity: target_entity = mouseovered_entity ui.target_indicator.detach_node() ui.point_indicator.reparent_to(mouseovered_entity_node) # FIXME: Adjust position point_coordinates = mouseover.collision_entry.get_surface_point( mouseovered_entity_node) ui.point_indicator.set_pos(point_coordinates) else: ui.target_indicator.detach_node() ui.point_indicator.detach_node() else: # If we're not pointing at an entity, don't indicate a # target. ui.target_indicator.detach_node() ui.point_indicator.detach_node() # Write results into the component for consumption by other # systems. if selected_entity is None: ui.selected_entity = None else: ui.selected_entity = selected_entity._uid if target_entity is None: ui.targeted_entity = None else: ui.targeted_entity = target_entity._uid ui.point_coordinates = point_coordinates
class NonBareTypeProxy(ProxyingNullSystem): proxies = { 'null_proxy': ProxyType(NullComponent), }
class UpdateCharacter(System): ''' Convert input to character movement. Components used :func:`wecs.core.and_filter` 'character' | :class:`wecs.panda3d.character.CharacterController` | :class:`wecs.panda3d.model.Clock` | :class:`wecs.panda3d.mode.Model` ''' entity_filters = { 'character': and_filter([ Proxy('character_node'), # Not used, but required for a complete character Clock, CharacterController, ]), 'input': and_filter([ CharacterController, Input, ]), } proxies = {'character_node': ProxyType(Model, 'node')} input_context = 'character_movement' def update(self, entities_by_filter): for entity in entities_by_filter['input']: input = entity[Input] if self.input_context in input.contexts: context = base.device_listener.read_context(self.input_context) character = entity[CharacterController] if context['direction'] is not None: character.move.x = context['direction'].x character.move.y = context['direction'].y else: character.move = Vec2(0, 0) if context['rotation'] is not None: character.heading = -context['rotation'].x character.pitch = context['rotation'].y else: character.heading = 0 character.pitch = 0 # Special movement modes. # By default, you run ("sprint"), unless you press e, in # which case you walk. You can crouch by pressing q; this # overrides walking and running. Jump by pressing space. # This logic is implemented by the Walking system. Here, # only intention is signalled. character.jumps = False character.sprints = False character.crouches = False if 'jump' in context: character.jumps = context['jump'] if 'sprint' in context: character.sprints = context['sprint'] if 'crouch' in context: character.crouches = context['crouch'] for entity in entities_by_filter['character']: controller = entity[CharacterController] dt = entity[Clock].game_time # Rotation controller.rotation = Vec3( controller.heading * dt, controller.pitch * dt, 0, ) # Translation # Controllers gamepad etc.) fill a whole rectangle of input # space, but characters are limited to a circle. If you're # strafing diagonally, you still don't get sqrt(2) speed. xy_dist = sqrt(controller.move.x ** 2 + controller.move.y ** 2) xy_scaling = 1.0 if xy_dist > 1: xy_scaling = 1.0 / xy_dist x = controller.move.x * xy_scaling y = controller.move.y * xy_scaling z = controller.move.z * xy_scaling controller.translation = Vec3(x * dt, y * dt, z * dt)
class TurningBackToCamera(System): ''' Turns character away from the camera. Components used :func:`wecs.core.and_filter` 'character' | :class:`wecs.panda3d.character.TurningBackToCameraMovement` | :class:`wecs.panda3d.character.CharacterController` | :class:`wecs.panda3d.model.Model` | :class:`wecs.panda3d.camera.ThirdPersonCamera` | :class:`wecs.panda3d.camera.TurntableCamera` | :class:`wecs.panda3d.model.Clock` ''' entity_filters = { 'character': and_filter([ Proxy('character_node'), Clock, TurningBackToCameraMovement, CharacterController, or_filter([ WalkingMovement, FloatingMovement, ]), Camera, ObjectCentricCameraMode, ]) } proxies = {'character_node': ProxyType(Model, 'node')} def update(self, entities_by_filter): for entity in entities_by_filter['character']: character = entity[CharacterController] camera = entity[Camera] center = entity[ObjectCentricCameraMode] turning = entity[TurningBackToCameraMovement] if WalkingMovement in entity: movement = entity[WalkingMovement] else: movement = entity[FloatingMovement] dt = entity[Clock].game_time if character.move.x**2 + character.move.y**2 > (turning.threshold * dt)**2: # What's the angle to turn? target_angle = camera.pivot.get_h() % 360 if target_angle > 180.0: target_angle = target_angle - 360.0 # How far can we turn this frame? Clamp to that. max_angle = movement.turning_speed * dt if abs(target_angle) > max_angle: target_angle *= max_angle / abs(target_angle) # How much of that do we *want* to turn? target_angle *= turning.view_axis_alignment # So let's turn, and clamp, in case we're already turning. old_rotation = character.rotation.x character.rotation.x += target_angle character.rotation.x = min(character.rotation.x, movement.turning_speed * dt) character.rotation.x = max(character.rotation.x, -movement.turning_speed * dt) # Since the camera rotates with the character, we need # to counteract that as well. delta_rotation = character.rotation.x - old_rotation camera.pivot.set_h(camera.pivot.get_h() - delta_rotation)
class AvatarUI(System): """ Command rules: if not embodied and not targeting_selection: selection(goto target) if not embodied and targeting_selection: selection(idle) if embodied and selecting and not targeting_selection: selection(goto target) if embodied and selecting and targeting_selection: selection(idle) if embodied and not selecting and targeting_selection: self(idle) if embodied and not selecting and not targeting_selection: self(goto target) """ entity_filters = { 'cursor': [Input, MouseOveringCamera, UserInterface], } proxies = { 'parent': ProxyType(Model, 'parent'), } input_context = 'select_entity' def update(self, entities_by_filter): for entity in entities_by_filter['cursor']: input = entity[Input] if self.input_context in input.contexts: context = base.device_listener.read_context(self.input_context) self.process_input(entity, context) def process_input(self, entity, context): ui = entity[UserInterface] mouseover = entity[MouseOveringCamera] mouseovered_entity = None if mouseover.entity is not None: mouseovered_entity = self.world.get_entity(mouseover.entity) targeting_self = mouseover.entity == entity._uid selected_entity = None if ui.selected_entity is not None: selected_entity = self.world.get_entity(ui.selected_entity) target_entity = None if ui.targeted_entity is not None: target_entity = self.world.get_entity(ui.targeted_entity) point_coordinates = ui.point_coordinates targeting_selection = False if selected_entity is not None and ui.selected_entity == ui.targeted_entity: targeting_selection = True embodied = Embodiable in entity targeting_embodiable = None if mouseovered_entity is not None: targeting_embodiable = Embodiable in mouseovered_entity # Now we can evaluate the player's input. First, he clicked to # select. if context.get('select', False): if target_entity is None or Selectable not in target_entity: # Selecting while not pointing at a valid target # unselects. ui.selected_entity = None ui.select_indicator.detach_node() else: # But selecting a selectable entity... selects it. if not embodied or mouseover.entity != entity._uid: ui.selected_entity = target_entity._uid # The player instead clicked to give a command, and there is a # valid target, ... elif context.get('command', False): # 3rd person mode, giving command with to selected entity if not embodied and selected_entity and target_entity and not targeting_selection: action = ['walk_to_entity', mouseover.entity] if point_coordinates: action.append(point_coordinates) self.command(selected_entity, *action) if not embodied and targeting_selection: self.command(selected_entity, 'idle') if embodied and selected_entity and target_entity and not targeting_selection: action = ['walk_to_entity', mouseover.entity] if point_coordinates: action.append(point_coordinates) self.command(selected_entity, *action) if embodied and ui.selected_entity and target_entity and not targeting_selection: self.command(entity, 'idle') if embodied and targeting_selection: self.command(selected_entity, 'idle') if embodied and not ui.selected_entity and targeting_selection: self.command(entity, 'idle') if embodied and not ui.selected_entity and target_entity and not targeting_self: action = ['walk_to_entity', mouseover.entity] if point_coordinates: action.append(point_coordinates) self.command(entity, *action) if embodied and targeting_self and not ui.selected_entity: self.command(entity, 'idle') # Now the player clicked to dis-/embody... elif context.get('embody', False): if not embodied and not targeting_embodiable and selected_entity: self.embody(entity, selected_entity) if not embodied and targeting_embodiable: self.embody(entity, target_entity) if embodied and targeting_embodiable and not targeting_self: self.jump_body(entity, target_entity) if embodied and not targeting_embodiable: self.disembody(entity) def command(self, entity, *action): ai = entity[BehaviorAI] ai.behavior = action def embody(self, entity, target): raise NotImplementedError def jump_body(self, entity, target): raise NotImplementedError def disembody(self, entity): raise NotImplementedError
class PrepareCameras(System): """ Add a `Camera` to an entity with the `'model`' proxy to attach a camera to its node. Add `MountedCameraMode` or `ObjectCentricCameraMode` to specify how the camera should place itself. """ entity_filters = { 'camera': and_filter([ Camera, Proxy('model'), ]), 'mount': and_filter([ Camera, MountedCameraMode, ]), 'mount_actor': and_filter([ Camera, MountedCameraMode, Actor, ]), 'center': and_filter([ Camera, ObjectCentricCameraMode, ]), } proxies = { 'model': ProxyType(Model, 'node'), } def enter_filter_camera(self, entity): model_proxy = self.proxies['model'] model = entity[model_proxy.component_type] camera = entity[Camera] camera.camera.reparent_to(camera.pivot) camera.pivot.reparent_to(model_proxy.field(entity)) camera.camera.node().get_lens().set_fov(camera.fov) def exit_filter_camera(self, entity): camera = entity[Camera] camera.pivot.detach_node() camera.camera.detach_node() def enter_filter_mount(self, entity): camera = entity[Camera] camera.pivot.set_pos(0, 0, 0) camera.pivot.set_hpr(0, 0, 0) camera.camera.set_pos(0, 0, 0) camera.camera.set_hpr(0, 0, 0) def enter_filter_mount_actor(self, entity): camera = entity[Camera] node = entity[Actor].node joint = node.expose_joint(None, 'modelRoot', 'camera') if joint: camera.pivot.set_pos((0, 0, 0)) camera.pivot.reparent_to(joint) def exit_filter_mount(self, entity): model_proxy = self.proxies['model'] model = entity[model_proxy.component_type] camera = entity[Camera] def update(self, entities_by_filter): for entity in entities_by_filter['center']: center = entity[ObjectCentricCameraMode] center.heading = 0 center.pitch = 0 center.zoom = 0