├── .gitignore ├── LICENSE ├── README.md ├── assets ├── chess │ ├── dancepole.bam │ ├── dancepole.blend │ ├── gameboard.bam │ ├── gameboard.blend │ ├── gamepieces.bam │ ├── gamepieces.blend │ ├── scene.bam │ └── scene.blend ├── mrman.png ├── peter.bam ├── peter.blend ├── roadE.bam ├── roadF.bam └── roadF.blend ├── benchmark.py ├── bin ├── game.py ├── keybindings.toml ├── main.py ├── map_muncher.py ├── posttag.yaml └── pretag.yaml ├── docs ├── Makefile ├── _templates │ └── apidoc │ │ ├── module.rst_t │ │ ├── package.rst_t │ │ └── toc.rst_t ├── api │ ├── modules.rst │ ├── wecs.aspects.rst │ ├── wecs.boilerplate.rst │ ├── wecs.cefconsole.rst │ ├── wecs.core.rst │ ├── wecs.equipment.rst │ ├── wecs.graphviz.rst │ ├── wecs.inventory.rst │ ├── wecs.mechanics.clock.rst │ ├── wecs.mechanics.rst │ ├── wecs.panda3d.ai.rst │ ├── wecs.panda3d.animation.rst │ ├── wecs.panda3d.aspects.rst │ ├── wecs.panda3d.camera.rst │ ├── wecs.panda3d.character.rst │ ├── wecs.panda3d.clock.rst │ ├── wecs.panda3d.core.rst │ ├── wecs.panda3d.debug.rst │ ├── wecs.panda3d.input.rst │ ├── wecs.panda3d.model.rst │ ├── wecs.panda3d.prototype.rst │ ├── wecs.panda3d.rst │ ├── wecs.repl.rst │ ├── wecs.rooms.rst │ └── wecs.rst ├── conf.py ├── gen.sh ├── index.rst ├── make.bat └── manual │ ├── boilerplate.md │ ├── boilerplate.rst │ ├── contribute.md │ ├── contribute.rst │ ├── design.md │ ├── design.rst │ ├── manual.rst │ ├── prefab.md │ ├── prefab.rst │ ├── readme.rst │ ├── todo.md │ ├── todo.rst │ ├── tutorial.md │ └── tutorial.rst ├── examples ├── minimal │ └── main.py ├── panda3d-animation-lab │ ├── game.py │ ├── grid.bam │ ├── grid.blend │ ├── grid_small.bam │ └── main.py ├── panda3d-behaviors │ ├── aspects.py │ ├── avatar_ui.py │ ├── behaviors.py │ ├── game.py │ ├── keybindings.config │ └── main.py ├── panda3d-character-controller-minimal │ ├── game.py │ ├── keybindings.config │ ├── main.py │ └── roadE.bam ├── panda3d-character-controller │ ├── game.py │ ├── keybindings.config │ ├── main.py │ └── roadE.bam ├── panda3d-cutting-edge │ ├── aspects.py │ ├── avatar_ui.py │ ├── behaviors.py │ ├── game.py │ ├── keybindings.config │ ├── main.py │ ├── make_lab_map.py │ ├── rectangle_map.bam │ └── simplepbr_test.py ├── panda3d-physics │ ├── ball.bam │ └── main.py ├── panda3d-point-and-click │ ├── game.py │ ├── keybindings.config │ ├── main.py │ └── table.png ├── panda3d-pong │ ├── ball.py │ ├── game.py │ ├── main_with_boilerplate.py │ ├── main_without_boilerplate.py │ ├── movement.py │ ├── paddles.py │ └── resources │ │ ├── ball.bam │ │ ├── ball.blend │ │ ├── paddle.bam │ │ └── paddle.blend ├── panda3d-twinstick │ ├── aspects.py │ ├── behaviors.py │ ├── game.py │ ├── keybindings.config │ └── main.py └── rpg │ ├── aging.py │ ├── character.py │ ├── dialogue.py │ ├── lifecycle.py │ ├── magic.py │ ├── main.py │ └── textio.py ├── requirements.txt ├── setup.py ├── tests ├── fixtures.py ├── test_aspects.py ├── test_clock.py ├── test_core │ ├── test_ecs.py │ ├── test_entity_dunders.py │ ├── test_filters.py │ └── test_reference.py ├── test_inventory.py ├── test_panda3d │ ├── test_ai.py │ ├── test_behavior_trees.py │ ├── test_core.py │ └── test_model.py └── test_rooms.py └── wecs ├── README.md ├── __init__.py ├── aspects.py ├── boilerplate.py ├── core.py ├── equipment.py ├── graphviz.py ├── inventory.py ├── mechanics ├── __init__.py └── clock.py ├── panda3d ├── __init__.py ├── ai.py ├── animation.py ├── avatar_ui.py ├── behavior_trees.py ├── camera.py ├── character.py ├── clock.py ├── constants.py ├── core.py ├── debug.py ├── gravity.py ├── input.py ├── interaction.py ├── map_muncher.py ├── mouseover.py ├── prototype.py └── spawnpoints.py ├── repl.py └── rooms.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | #*# 3 | __pycache__/ 4 | .pytest_cache/ 5 | *.blend?* 6 | *.egg-info/ 7 | build/ 8 | _build/ 9 | dist/ 10 | .coverage 11 | htmlcov/ 12 | error.log 13 | docs/manual/readme.md 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, TheCheapestPixels 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What is WECS? 2 | ------------- 3 | 4 | *WECS* stands for World, Entities, Components, Systems. It implements 5 | the architecture pattern known as [ECS, EC, Component system (and 6 | probably by several other names as well)] 7 | (https://en.wikipedia.org/wiki/Entity_component_system), which is 8 | popular in game development. 9 | 10 | WECS aims at putting usability first, and to not let performance 11 | optimizations compromise it. 12 | 13 | Beyond the core which implements the ECS, the goal of WECS is to 14 | accumulate enough game mechanics and boilerplate code so that the time 15 | between imagining a game and getting to the point where you actually 16 | work on your specific game mechanics is down to a few minutes. 17 | 18 | In particular, systems and components to work with the 19 | [Panda3D engine](https://www.panda3d.org/) are provided. 20 | 21 | 22 | Installation, etc. 23 | ------------------ 24 | 25 | * Installation: `pip install wecs` 26 | * Documentation: [readthedocs.io](https://wecs.readthedocs.io/en/latest/) 27 | * Source code: [GitHub repository](https://github.com/TheCheapestPixels/wecs) 28 | * Chat: [Panda3D Offtopic Discord server](https://discord.gg/pcR4ZTS), 29 | channel [#wecs](https://discord.com/channels/722508679118848012/722510686474731651) 30 | 31 | 32 | Hello World 33 | ----------- 34 | 35 | ```python 36 | from wecs.core import * 37 | 38 | world = World() 39 | ``` 40 | 41 | A world contains `Entities`; Also `Systems`, more about those later. 42 | 43 | entity = world.create_entity() 44 | 45 | `Entities` themselves contain `Components`; They are also nothing more 46 | than a container for components. They are a general form of 47 | "game object", and can be turned into more specific objects by adding 48 | `Components` to them. 49 | 50 | `Components` are data structures with no inherent functionality (i.e. 51 | they have no functions). Their presence on an entity describes the state 52 | of an aspect of that entity. For example, a certain game object could be 53 | the player's car, having a `Geometry` component with its graphical 54 | model, a `Car` component describing things like motor power and fuel in 55 | the tank, a `PhysicsBody` keeping track of the car's representation in 56 | the physics simulation, and many more. Or it could be something as 57 | simple as "An entity with this component can count", or "It can print 58 | its count (if it has one)": 59 | 60 | @Component() 61 | class Counter: 62 | count: int = 0 63 | 64 | @Component() 65 | class Printer: 66 | name: str = "Bob" 67 | 68 | entity.add_component(Counter()) 69 | entity.add_component(Printer()) 70 | 71 | During each `world.update()`, the `World` will go through its list of 72 | `Systems`, and run each in turn. Each `System` has a list of `Filters` 73 | which test whether `System` should process an entity, and in what kind 74 | of role. 75 | 76 | class CountAndPrint(System): 77 | entity_filters = { 78 | 'count': Counter, 79 | 'print': and_filter(Counter, Printer), 80 | } 81 | 82 | def update(self, entities_by_filter): 83 | for entity in entities_by_filter['count']: 84 | entity[Counter].count += 1 85 | for entity in entities_by_filter['print']: 86 | msg = "{} has counted to {}.".format( 87 | entity[Printer].name, 88 | entity[Counter].count, 89 | ) 90 | print(msg) 91 | 92 | world.add_system(CountAndPrint(), 0) 93 | 94 | The `0` in `world.add_system(CountAndPrint(), 0)` is the `sort` of the 95 | system. Systems are run in ascending order of sort. As an aside, it'd be 96 | a good idea to break this into two systems; Who knows what else other 97 | people want to happen between counting and printing? 98 | 99 | Now we can step time forward in this little universe: 100 | 101 | world.update() 102 | Bob has counted to 1. 103 | 104 | This concludes the Hello World example. 105 | -------------------------------------------------------------------------------- /assets/chess/dancepole.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/chess/dancepole.bam -------------------------------------------------------------------------------- /assets/chess/dancepole.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/chess/dancepole.blend -------------------------------------------------------------------------------- /assets/chess/gameboard.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/chess/gameboard.bam -------------------------------------------------------------------------------- /assets/chess/gameboard.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/chess/gameboard.blend -------------------------------------------------------------------------------- /assets/chess/gamepieces.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/chess/gamepieces.bam -------------------------------------------------------------------------------- /assets/chess/gamepieces.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/chess/gamepieces.blend -------------------------------------------------------------------------------- /assets/chess/scene.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/chess/scene.bam -------------------------------------------------------------------------------- /assets/chess/scene.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/chess/scene.blend -------------------------------------------------------------------------------- /assets/mrman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/mrman.png -------------------------------------------------------------------------------- /assets/peter.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/peter.bam -------------------------------------------------------------------------------- /assets/peter.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/peter.blend -------------------------------------------------------------------------------- /assets/roadE.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/roadE.bam -------------------------------------------------------------------------------- /assets/roadF.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/roadF.bam -------------------------------------------------------------------------------- /assets/roadF.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/assets/roadF.blend -------------------------------------------------------------------------------- /benchmark.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | 5 | class BaseBenchmark: 6 | def __init__(self, name): 7 | self.name = name 8 | 9 | def get_mem_usage(self): 10 | return "", "" 11 | 12 | def setup(self, num_entities, num_components): # pylint: disable=unused-argument 13 | return 0 14 | 15 | def update_cold(self): 16 | return 0 17 | 18 | def update_warm(self): 19 | return 0 20 | 21 | def run(self): 22 | print('={}='.format(self.name)) 23 | print('==Memory==') 24 | print('Entity: {}, NullComponent: {}'.format( 25 | *self.get_mem_usage() 26 | )) 27 | print() 28 | 29 | print('==Time==') 30 | incs = [1, 100, 1000] 31 | for num_ent in incs: 32 | for num_comp in incs: 33 | print('Entities: {}, Components: {}'.format(num_ent, num_comp)) 34 | time_start = time.perf_counter_ns() 35 | self.setup(num_ent, num_comp) 36 | time_setup = (time.perf_counter_ns() - time_start) / 100_000 37 | 38 | time_start = time.perf_counter_ns() 39 | self.update_cold() 40 | time_update_cold = (time.perf_counter_ns() - time_start) / 100_000 41 | 42 | time_start = time.perf_counter_ns() 43 | self.update_warm() 44 | time_update_warm = (time.perf_counter_ns() - time_start) / 100_000 45 | 46 | time_total = time_setup + time_update_cold + time_update_warm 47 | print('\t{:0.2f}ms (setup: {:0.2f}ms, cold update: {:0.2f}ms, warm update: {:0.2f}ms)'.format( 48 | time_total, 49 | time_setup, 50 | time_update_cold, 51 | time_update_warm, 52 | )) 53 | 54 | 55 | class SimpleEcsBench(BaseBenchmark): 56 | def __init__(self): 57 | import simpleecs 58 | import simpleecs.components 59 | self.component_classes = [ 60 | type( 61 | 'NullComponent{}'.format(i), 62 | simpleecs.components.NullComponent.__bases__, 63 | dict(simpleecs.components.NullComponent.__dict__), 64 | ) 65 | for i in range(10_000) 66 | ] 67 | self.world = None 68 | 69 | super().__init__('simpleecs') 70 | 71 | def get_mem_usage(self): 72 | import simpleecs 73 | import simpleecs.components 74 | return ( 75 | sys.getsizeof(simpleecs.World().create_entity()), 76 | sys.getsizeof(simpleecs.components.NullComponent()) 77 | ) 78 | 79 | def setup(self, num_entities, num_components): 80 | import simpleecs 81 | import simpleecs.systems 82 | 83 | self.world = simpleecs.World() 84 | self.world.add_system( 85 | simpleecs.systems.NullSystem(), 86 | ) 87 | 88 | for _ in range(num_entities): 89 | self.world.create_entity([ 90 | self.component_classes[compnum] 91 | for compnum in range(num_components) 92 | ]) 93 | 94 | def update_cold(self): 95 | self.world.update(0) 96 | 97 | def update_warm(self): 98 | self.world.update(0) 99 | 100 | 101 | if __name__ == '__main__': 102 | BENCHMARKS = [ 103 | SimpleEcsBench() 104 | ] 105 | for bench in BENCHMARKS: 106 | bench.run() 107 | -------------------------------------------------------------------------------- /bin/keybindings.toml: -------------------------------------------------------------------------------- 1 | [debug] 2 | 3 | [debug.quit] 4 | _type = "trigger" 5 | _device_order = ["keyboard"] 6 | keyboard = "escape" 7 | 8 | [debug.console] 9 | _type = "trigger" 10 | _device_order = ["keyboard"] 11 | keyboard = "f9" 12 | 13 | [debug.frame_rate_meter] 14 | _type = "trigger" 15 | _device_order = ["keyboard"] 16 | keyboard = "f10" 17 | 18 | [debug.pdb] 19 | _type = "trigger" 20 | _device_order = ["keyboard"] 21 | keyboard = "f11" 22 | 23 | [debug.pstats] 24 | _type = "trigger" 25 | _device_order = ["keyboard"] 26 | keyboard = "f12" 27 | 28 | [character_movement] 29 | 30 | [character_movement.direction] 31 | _type = "axis2d" 32 | _device_order = ["gamepad", "spatial_mouse", "keyboard"] 33 | gamepad = "left_x,left_y" 34 | spatial_mouse = "x:scale=3,y:scale=3" 35 | keyboard = "a,d,s,w" 36 | 37 | [character_movement.rotation] 38 | _type = "axis2d" 39 | _device_order = ["gamepad", "spatial_mouse", "keyboard"] 40 | gamepad = "dpad_left,dpad_right,dpad_down,dpad_up" 41 | spatial_mouse = "yaw:flip:scale=3,pitch:scale=2" 42 | keyboard = "arrow_left,arrow_right,arrow_down,arrow_up" 43 | 44 | [character_movement.jump] 45 | _type = "trigger" 46 | _device_order = ["gamepad", "spatial_mouse", "keyboard"] 47 | gamepad = "face_x" 48 | spatial_mouse = "z:button>=0.3" 49 | keyboard = "space" 50 | 51 | [character_movement.crouch] 52 | _type = "button" 53 | _device_order = ["gamepad", "spatial_mouse", "keyboard"] 54 | gamepad = "face_b" 55 | spatial_mouse = "z:button<=-0.3" 56 | keyboard = "c" 57 | 58 | [character_movement.sprint] 59 | _type = "button" 60 | _device_order = ["gamepad", "keyboard"] 61 | gamepad = "ltrigger" 62 | keyboard = "e" 63 | 64 | [camera_movement] 65 | 66 | # [camera_movement.rotation] 67 | # _type = "axis2d" 68 | # _device_order = ["gamepad", "spatial_mouse", "keyboard"] 69 | # gamepad = "right_x:exp=2,right_y:exp=2:scale=-1" 70 | # spatial_mouse = "yaw:flip:scale=2,pitch" 71 | # keyboard = "mouse_x_delta,mouse_y_delta" 72 | 73 | [camera_movement.zoom] 74 | _type = "axis" 75 | _device_order = ["keyboard"] 76 | keyboard = "u,o" 77 | 78 | [clock_control] 79 | 80 | [clock_control.time_zoom] 81 | _type = "axis" 82 | _device_order = ["keyboard"] 83 | keyboard = "-,+" 84 | 85 | [select_entity] 86 | 87 | [select_entity.select] 88 | _type = "trigger" 89 | _device_order = ["keyboard"] 90 | keyboard = "mouse1" 91 | 92 | [select_entity.command] 93 | _type = "trigger" 94 | _device_order = ["keyboard"] 95 | keyboard = "mouse3" 96 | 97 | [select_entity.embody] 98 | _type = "trigger" 99 | _device_order = ["keyboard"] 100 | keyboard = "1" 101 | -------------------------------------------------------------------------------- /bin/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from wecs import boilerplate 6 | 7 | 8 | if __name__ == '__main__': 9 | boilerplate.run_game( 10 | module_name=os.path.dirname(__file__), 11 | # console=True, 12 | console=False, 13 | keybindings=True, 14 | ) 15 | -------------------------------------------------------------------------------- /bin/map_muncher.py: -------------------------------------------------------------------------------- 1 | """ 2 | Map muncher - Convert property-annotated maps to runnable output. 3 | 4 | * If the input file is given as .blend, run blend2bam on it. 5 | * Pretag 6 | * Find all nodes with `linked_collection` tag 7 | * If such a node also has a `linked_file` tag, the given file is the 8 | authoritative source for the model to use, and the value of 9 | `linked_collection` must be the name of the node in that file that 10 | will be used to attack at this node. 11 | The file may first have to be converted via blend2bam. 12 | * If the node does not have a `linked_file` tag, the node and its 13 | subgraph will be used. 14 | * Detach and replace with spawn points 15 | * Post-tag 16 | * save 17 | """ 18 | 19 | 20 | def tag_node(graph, pretags): 21 | for node_name, tags in pretags.items(): 22 | if node_name == '_root': 23 | node = graph 24 | else: 25 | node = graph.find('**/{}'.format(node_name)) 26 | if not node.is_empty(): 27 | for tag_name, tag_value in tags.items(): 28 | node.set_tag(tag_name, tag_value) 29 | 30 | 31 | def munch_map(graph): 32 | """ 33 | * Find nodes that have a `linked_collection` tag. 34 | * If it has a `linked_file` tag 35 | """ 36 | 37 | instances = {} # 'model source': NodePath 38 | 39 | # Gather instances and replace them with spawn points 40 | for node in graph.find_all_matches('**/=linked_collection'): 41 | collection = node.get_tag('linked_collection') 42 | 43 | # Replace node with a spawn point 44 | spawn_name = 'spawn_point:{}'.format(node.get_name()) 45 | spawn_point = node.attach_new_node(spawn_name) 46 | spawn_point.wrt_reparent_to(node.get_parent()) 47 | spawn_point.set_tag('collection', collection) 48 | node.detach_node() 49 | 50 | # If the node's collection is already in the instances, 51 | # we're done. Otherwise we'll have to extract the node's 52 | # canonical source from the linked file and save it on 53 | # its own. 54 | if collection not in instances: 55 | instances[collection] = node 56 | 57 | # If a source file is given, rewrite 58 | # `//.blend` to `.bam` 59 | if node.has_tag('linked_file'): 60 | source_file = node.get_tag('linked_file') 61 | if source_file.endswith('.blend'): 62 | source_file = source_file[:-5] + 'bam' 63 | if source_file.startswith('//'): 64 | source_file = source_file[2:] 65 | node.set_tag('linked_file', source_file) 66 | 67 | return (graph, instances) 68 | 69 | 70 | if __name__ == '__main__': 71 | import argparse 72 | import pathlib 73 | import yaml 74 | from direct.showbase.ShowBase import ShowBase 75 | 76 | # Command-line args 77 | parser = argparse.ArgumentParser() 78 | parser.add_argument('input_file') 79 | parser.add_argument('output_file') 80 | args = parser.parse_args() 81 | input_dir = pathlib.Path(args.input_file).parent 82 | 83 | # Load the input file 84 | ShowBase() 85 | graph = base.loader.load_model(args.input_file) 86 | 87 | # Pretagging 88 | with open('pretag.yaml', 'r') as f: 89 | spec_text = f.read() 90 | specs = yaml.load(spec_text, Loader=yaml.BaseLoader) 91 | if specs is not None: 92 | tag_node(graph, specs) 93 | 94 | # Detach instances, copy or save their model file 95 | pruned_map, instances = munch_map(graph) 96 | 97 | for collection in instances: 98 | node = instances[collection] 99 | if node.has_tag('linked_file'): 100 | # Extract the node to use from linked files 101 | linked_file = node.get_tag('linked_file') 102 | collection_set = base.loader.load_model( 103 | input_dir / linked_file, 104 | ) 105 | collection_node = collection_set.find(collection) 106 | instances[collection] = collection_node 107 | 108 | # Posttagging 109 | with open('posttag.yaml', 'r') as f: 110 | spec_text = f.read() 111 | specs = yaml.load(spec_text, Loader=yaml.BaseLoader) 112 | if specs is not None: 113 | for collection, node_specs in specs.items(): 114 | if collection != '_scene': 115 | tag_node(instances[collection], node_specs) 116 | else: 117 | tag_node(graph, node_specs) 118 | 119 | for name, node in instances.items(): 120 | print(name) 121 | for tag_name in node.get_tags(): 122 | print(" {}: {}".format(tag_name, node.get_tag(tag_name))) 123 | 124 | # Save 125 | # TODO: Invoke blend2bam where needed 126 | graph.write_bam_file(args.output_file) 127 | 128 | for collection, node in instances.items(): 129 | filename = '{}.bam'.format(collection) 130 | node.write_bam_file(filename) 131 | -------------------------------------------------------------------------------- /bin/posttag.yaml: -------------------------------------------------------------------------------- 1 | _scene: 2 | spawn_point:peter: 3 | character_type: player_character 4 | spawn_point:peter.001: 5 | character_type: non_player_character 6 | peter: 7 | _root: 8 | entity_type: character 9 | board: 10 | _root: 11 | entity_type: map 12 | dancepole: 13 | _root: 14 | entity_type: map 15 | test_cube: 16 | _root: 17 | entity_type: map 18 | bishop: 19 | _root: 20 | entity_type: nothing 21 | king: 22 | _root: 23 | entity_type: nothing 24 | knight: 25 | _root: 26 | entity_type: nothing 27 | pawn: 28 | _root: 29 | entity_type: nothing 30 | queen: 31 | _root: 32 | entity_type: nothing 33 | rook: 34 | _root: 35 | entity_type: nothing 36 | -------------------------------------------------------------------------------- /bin/pretag.yaml: -------------------------------------------------------------------------------- 1 | board: 2 | linked_collection: board 3 | test_cube: 4 | linked_collection: test_cube 5 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_templates/apidoc/module.rst_t: -------------------------------------------------------------------------------- 1 | {%- if show_headings %} 2 | {{- [basename, "module"] | join(' ') | e | heading }} 3 | 4 | {% endif -%} 5 | .. automodule:: {{ qualname }} 6 | {%- for option in automodule_options %} 7 | :{{ option }}: 8 | {%- endfor %} 9 | 10 | -------------------------------------------------------------------------------- /docs/_templates/apidoc/package.rst_t: -------------------------------------------------------------------------------- 1 | {%- macro automodule(modname, options) -%} 2 | .. automodule:: {{ modname }} 3 | {%- for option in options %} 4 | :{{ option }}: 5 | {%- endfor %} 6 | {%- endmacro %} 7 | 8 | {%- macro toctree(docnames) -%} 9 | .. toctree:: 10 | {% for docname in docnames %} 11 | {{ docname }} 12 | {%- endfor %} 13 | {%- endmacro %} 14 | 15 | {%- if is_namespace %} 16 | {{- [pkgname, "namespace"] | join(" ") | e | heading }} 17 | {% else %} 18 | {{- [pkgname, "package"] | join(" ") | e | heading }} 19 | {% endif %} 20 | 21 | {%- if modulefirst and not is_namespace %} 22 | {{ automodule(pkgname, automodule_options) }} 23 | {% endif %} 24 | 25 | {%- if subpackages %} 26 | {{ toctree(subpackages) }} 27 | {% endif %} 28 | 29 | {%- if submodules %} 30 | {% if separatemodules %} 31 | {{ toctree(submodules) }} 32 | {%- else %} 33 | {%- for submodule in submodules %} 34 | {% if show_headings %} 35 | {{- [submodule, "module"] | join(" ") | e | heading(2) }} 36 | {% endif %} 37 | {{ automodule(submodule, automodule_options) }} 38 | {% endfor %} 39 | {%- endif %} 40 | {% endif %} 41 | 42 | {%- if not modulefirst and not is_namespace %} 43 | {{ automodule(pkgname, automodule_options) }} 44 | {% endif %} 45 | -------------------------------------------------------------------------------- /docs/_templates/apidoc/toc.rst_t: -------------------------------------------------------------------------------- 1 | {{ header | heading }} 2 | 3 | .. toctree:: 4 | :maxdepth: {{ maxdepth }} 5 | {% for docname in docnames %} 6 | {{ docname }} 7 | {%- endfor %} 8 | 9 | -------------------------------------------------------------------------------- /docs/api/modules.rst: -------------------------------------------------------------------------------- 1 | wecs 2 | ==== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | wecs 8 | -------------------------------------------------------------------------------- /docs/api/wecs.aspects.rst: -------------------------------------------------------------------------------- 1 | wecs.aspects module 2 | =================== 3 | 4 | .. automodule:: wecs.aspects 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.boilerplate.rst: -------------------------------------------------------------------------------- 1 | wecs.boilerplate module 2 | ======================= 3 | 4 | .. automodule:: wecs.boilerplate 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.cefconsole.rst: -------------------------------------------------------------------------------- 1 | wecs.cefconsole module 2 | ====================== 3 | 4 | .. automodule:: wecs.cefconsole 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.core.rst: -------------------------------------------------------------------------------- 1 | wecs.core module 2 | ================ 3 | 4 | .. automodule:: wecs.core 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.equipment.rst: -------------------------------------------------------------------------------- 1 | wecs.equipment module 2 | ===================== 3 | 4 | .. automodule:: wecs.equipment 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.graphviz.rst: -------------------------------------------------------------------------------- 1 | wecs.graphviz module 2 | ==================== 3 | 4 | .. automodule:: wecs.graphviz 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.inventory.rst: -------------------------------------------------------------------------------- 1 | wecs.inventory module 2 | ===================== 3 | 4 | .. automodule:: wecs.inventory 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.mechanics.clock.rst: -------------------------------------------------------------------------------- 1 | wecs.mechanics.clock module 2 | =========================== 3 | 4 | .. automodule:: wecs.mechanics.clock 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.mechanics.rst: -------------------------------------------------------------------------------- 1 | wecs.mechanics package 2 | ====================== 3 | 4 | .. automodule:: wecs.mechanics 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | 10 | .. toctree:: 11 | 12 | wecs.mechanics.clock 13 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.ai.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.ai module 2 | ====================== 3 | 4 | .. automodule:: wecs.panda3d.ai 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.animation.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.animation module 2 | ============================= 3 | 4 | .. automodule:: wecs.panda3d.animation 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.aspects.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.aspects module 2 | =========================== 3 | 4 | .. automodule:: wecs.panda3d.aspects 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.camera.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.camera module 2 | ========================== 3 | 4 | .. automodule:: wecs.panda3d.camera 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.character.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.character module 2 | ============================= 3 | 4 | .. automodule:: wecs.panda3d.character 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.clock.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.clock module 2 | ========================= 3 | 4 | .. automodule:: wecs.panda3d.clock 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.core.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.core module 2 | ======================== 3 | 4 | .. automodule:: wecs.panda3d.core 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.debug.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.debug module 2 | ========================= 3 | 4 | .. automodule:: wecs.panda3d.debug 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.input.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.input module 2 | ========================= 3 | 4 | .. automodule:: wecs.panda3d.input 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.model.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.model module 2 | ========================= 3 | 4 | .. automodule:: wecs.panda3d.model 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.prototype.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d.prototype module 2 | ============================= 3 | 4 | .. automodule:: wecs.panda3d.prototype 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.panda3d.rst: -------------------------------------------------------------------------------- 1 | wecs.panda3d package 2 | ==================== 3 | 4 | .. automodule:: wecs.panda3d 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | 10 | .. toctree:: 11 | 12 | wecs.panda3d.ai 13 | wecs.panda3d.animation 14 | wecs.panda3d.aspects 15 | wecs.panda3d.camera 16 | wecs.panda3d.character 17 | wecs.panda3d.clock 18 | wecs.panda3d.core 19 | wecs.panda3d.debug 20 | wecs.panda3d.input 21 | wecs.panda3d.model 22 | wecs.panda3d.prototype 23 | -------------------------------------------------------------------------------- /docs/api/wecs.repl.rst: -------------------------------------------------------------------------------- 1 | wecs.repl module 2 | ================ 3 | 4 | .. automodule:: wecs.repl 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.rooms.rst: -------------------------------------------------------------------------------- 1 | wecs.rooms module 2 | ================= 3 | 4 | .. automodule:: wecs.rooms 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/api/wecs.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. automodule:: wecs 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | .. toctree:: 10 | 11 | wecs.core 12 | wecs.mechanics 13 | wecs.panda3d 14 | wecs.aspects 15 | wecs.boilerplate 16 | wecs.cefconsole 17 | wecs.equipment 18 | wecs.graphviz 19 | wecs.inventory 20 | wecs.repl 21 | wecs.rooms 22 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'WECS' 21 | copyright = '2019, TheCheapestPixels' 22 | author = 'TheCheapestPixels' 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | 'sphinx.ext.autodoc', 32 | ] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # List of patterns, relative to source directory, that match files and 38 | # directories to ignore when looking for source files. 39 | # This pattern also affects html_static_path and html_extra_path. 40 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 41 | autodoc_member_order = 'bysource' 42 | # -- Options for HTML output ------------------------------------------------- 43 | 44 | # The theme to use for HTML and HTML Help pages. See the documentation for 45 | # a list of builtin themes. 46 | html_theme = 'sphinx_rtd_theme' 47 | html_theme_options = { 48 | 'style_nav_header_background': '#735cdd', 49 | #'logo_only': True, 50 | 'collapse_navigation': False, 51 | 'prev_next_buttons_location': 'both', 52 | 'style_external_links': True, 53 | 'display_version': False, 54 | } 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | html_static_path = ['_static'] 60 | master_doc = 'index' 61 | from sphinx.ext.autodoc import between 62 | def setup(app): 63 | # Register a sphinx.ext.autodoc.between listener to ignore everything 64 | # between lines that contain the word IGNORE 65 | app.connect('autodoc-process-docstring', between('^.*IGNORE.*$', exclude=True)) 66 | 67 | return app 68 | -------------------------------------------------------------------------------- /docs/gen.sh: -------------------------------------------------------------------------------- 1 | cp ../README.md manual/readme.md 2 | for f in manual/*.md ; do pandoc $f -o $(echo $f|sed -e ''s/.md$/.rst/); done 3 | sphinx-apidoc -Mo api ../wecs --separate -t _templates/apidoc 4 | make html 5 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. WECS documentation master file, created by 2 | sphinx-quickstart on Mon Dec 30 02:32:47 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to the WECS documentation! 7 | ================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 8 11 | :caption: Contents: 12 | 13 | manual/manual 14 | api/wecs 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/manual/boilerplate.md: -------------------------------------------------------------------------------- 1 | Panda3D Boilerplate 2 | =================== 3 | 4 | Boilerplate for Panda3D 5 | ----------------------- 6 | 7 | You want to prototype in Panda3D *now*? Good! Here's the `main.py` of 8 | your project: 9 | 10 | ```bash 11 | #!/usr/bin/env python 12 | 13 | from wecs import boilerplate 14 | 15 | 16 | def run_game(): 17 | boilerplate.run_game() 18 | 19 | 20 | if __name__ == '__main__': 21 | run_game() 22 | ``` 23 | 24 | Then write a `game.py` to set up the world, which is `base.ecs_world`. 25 | FIXME: `system_types` 26 | 27 | Now run `python main.py`, and you'll be dropped right into your game. 28 | 29 | FIXME: Mention wecs_null_project 30 | 31 | 32 | Integrations 33 | ------------ 34 | 35 | FIXME: 36 | * keybindings 37 | * simplepbr 38 | * ??? -------------------------------------------------------------------------------- /docs/manual/boilerplate.rst: -------------------------------------------------------------------------------- 1 | Panda3D Boilerplate 2 | =================== 3 | 4 | Boilerplate for Panda3D 5 | ----------------------- 6 | 7 | You want to prototype in Panda3D *now*? Good! Here’s the ``main.py`` of 8 | your project: 9 | 10 | .. code:: bash 11 | 12 | #!/usr/bin/env python 13 | 14 | from wecs import boilerplate 15 | 16 | 17 | def run_game(): 18 | boilerplate.run_game() 19 | 20 | 21 | if __name__ == '__main__': 22 | run_game() 23 | 24 | Then write a ``game.py`` to set up the world, which is 25 | ``base.ecs_world``. FIXME: ``system_types`` 26 | 27 | Now run ``python main.py``, and you’ll be dropped right into your game. 28 | 29 | FIXME: Mention wecs_null_project 30 | 31 | Integrations 32 | ------------ 33 | 34 | FIXME: \* cefconsole \* keybindings \* simplepbr \* ??? 35 | -------------------------------------------------------------------------------- /docs/manual/contribute.md: -------------------------------------------------------------------------------- 1 | Contributing to WECS 2 | -------------------- 3 | 4 | WECS is currently in the alpha stage of development, closing in on beta. 5 | That means that while most major features have been put into place, and 6 | the viability of WECS has been shown, there are still some essential 7 | features missing, and many, many rough edges remain to be smoothed out. 8 | We would love any support we can get. 9 | 10 | Feel free to fork the project, play around with it, expand it, and build 11 | games an applications on top of it. Any problem that you encounter, or 12 | question that comes up and is not answered by the manual, is an edge 13 | that needs to be sanded down. Please report it, or fix it and submit a 14 | pull request. 15 | 16 | 17 | ### Code 18 | 19 | WECS has a rather long TODO list. If you feel like tackling any task, 20 | or adding other features that you feel are missing, have at it. 21 | 22 | 23 | #### Style Guide 24 | 25 | Our style guideline is 26 | 27 | * PEP8 with a line length limit of "72 would be preferrable, up to 100 28 | are justifiable." 29 | * Symbolic strings (constants, file names, etc.) are in single quotes, 30 | strings for human consumption in double quotes. 31 | 32 | Since we are ad-hoc-ing our style a lot, this manual section also needs 33 | to be elaborated on. 34 | 35 | 36 | ### Documentation 37 | 38 | WECS' documentation comes from two sources: 39 | 40 | * a set of Markdown files, namely the `README.md`, and additional manual 41 | files residing in `docs/manual/`, 42 | * the docstrings in the code from which the API Reference is generated. 43 | 44 | Generating the documentation has following requirements: 45 | 46 | * The Python packages `sphinx`, `sphinx-autoapi`, and 47 | `sphinx_rtd_theme`. 48 | * `pandoc`, which is *not* a Python package, and has to be installed via 49 | your operating system's package manager. 50 | 51 | To generate the documentation, change to WECS' `docs/` directory and run 52 | `./gen.sh`. This will... 53 | 54 | * copy `README.md` into the `docs/manual/readme.md`, 55 | * use `pandoc` to convert the `.md` files in `docs/manual/` to `.rst`, 56 | * generate the documentation with Sphinx. 57 | 58 | The generated documentation can be found in `docs/_build`. The HTML 59 | document starts with `_build/html/index.html`. 60 | 61 | 62 | #### Style Guide 63 | 64 | Markdown documentation style: 65 | 66 | * There are two empty lines before a headline (except for the one 67 | starting a document), except between consecutive headlines, between 68 | which there is one empty line. 69 | ```text 70 | Manual Page 71 | ----------- 72 | 73 | Chapter One 74 | ----------- 75 | 76 | Lorem Ipsum and so on... 77 | 78 | 79 | Chapter Two 80 | ----------- 81 | 82 | ... ti amat. 83 | ``` 84 | * Lines are no longer than 72 characters for easier display on half a 85 | screen. Things like links are exempted. 86 | ```text 87 | 1 2 3 4 5 6 7 88 | 123456789012345678901234567890123456789012345678901234567890123456789012 89 | This is the line that doesn't end; Yes, it goes on and on, my friend. 90 | Some dev guy started writing it not knowing what it was, and he'll 91 | continue writing it forever just because 92 | [this is the line that doesn't end...](https://www.youtube.com/watch?v=xz6OGVCdov8) 93 | ``` 94 | * There are no comments in Markdown. 95 | -------------------------------------------------------------------------------- /docs/manual/contribute.rst: -------------------------------------------------------------------------------- 1 | Contributing to WECS 2 | -------------------- 3 | 4 | WECS is currently in the alpha stage of development, closing in on beta. 5 | That means that while most major features have been put into place, and 6 | the viability of WECS has been shown, there are still some essential 7 | features missing, and many, many rough edges remain to be smoothed out. 8 | We would love any support we can get. 9 | 10 | Feel free to fork the project, play around with it, expand it, and build 11 | games an applications on top of it. Any problem that you encounter, or 12 | question that comes up and is not answered by the manual, is an edge 13 | that needs to be sanded down. Please report it, or fix it and submit a 14 | pull request. 15 | 16 | Code 17 | ~~~~ 18 | 19 | WECS has a rather long TODO list. If you feel like tackling any task, or 20 | adding other features that you feel are missing, have at it. 21 | 22 | Style Guide 23 | ^^^^^^^^^^^ 24 | 25 | Our style guideline is 26 | 27 | - PEP8 with a line length limit of “72 would be preferrable, up to 100 28 | are justifiable.” 29 | - Symbolic strings (constants, file names, etc.) are in single quotes, 30 | strings for human consumption in double quotes. 31 | 32 | Since we are ad-hoc-ing our style a lot, this manual section also needs 33 | to be elaborated on. 34 | 35 | Documentation 36 | ~~~~~~~~~~~~~ 37 | 38 | WECS’ documentation comes from two sources: 39 | 40 | - a set of Markdown files, namely the ``README.md``, and additional 41 | manual files residing in ``docs/manual/``, 42 | - the docstrings in the code from which the API Reference is generated. 43 | 44 | Generating the documentation has following requirements: 45 | 46 | - The Python packages ``sphinx``, ``sphinx-autoapi``, and 47 | ``sphinx_rtd_theme``. 48 | - ``pandoc``, which is *not* a Python package, and has to be installed 49 | via your operating system’s package manager. 50 | 51 | To generate the documentation, change to WECS’ ``docs/`` directory and 52 | run ``./gen.sh``. This will… 53 | 54 | - copy ``README.md`` into the ``docs/manual/readme.md``, 55 | - use ``pandoc`` to convert the ``.md`` files in ``docs/manual/`` to 56 | ``.rst``, 57 | - generate the documentation with Sphinx. 58 | 59 | The generated documentation can be found in ``docs/_build``. The HTML 60 | document starts with ``_build/html/index.html``. 61 | 62 | .. _style-guide-1: 63 | 64 | Style Guide 65 | ^^^^^^^^^^^ 66 | 67 | Markdown documentation style: 68 | 69 | - There are two empty lines before a headline (except for the one 70 | starting a document), except between consecutive headlines, between 71 | which there is one empty line. 72 | 73 | .. code:: text 74 | 75 | Manual Page 76 | ----------- 77 | 78 | Chapter One 79 | ----------- 80 | 81 | Lorem Ipsum and so on... 82 | 83 | 84 | Chapter Two 85 | ----------- 86 | 87 | ... ti amat. 88 | 89 | - Lines are no longer than 72 characters for easier display on half a 90 | screen. Things like links are exempted. 91 | 92 | .. code:: text 93 | 94 | 1 2 3 4 5 6 7 95 | 123456789012345678901234567890123456789012345678901234567890123456789012 96 | This is the line that doesn't end; Yes, it goes on and on, my friend. 97 | Some dev guy started writing it not knowing what it was, and he'll 98 | continue writing it forever just because 99 | [this is the line that doesn't end...](https://www.youtube.com/watch?v=xz6OGVCdov8) 100 | 101 | - There are no comments in Markdown. 102 | -------------------------------------------------------------------------------- /docs/manual/manual.rst: -------------------------------------------------------------------------------- 1 | Manual 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | readme 8 | tutorial 9 | prefab 10 | boilerplate 11 | contribute 12 | design 13 | todo 14 | -------------------------------------------------------------------------------- /docs/manual/prefab.md: -------------------------------------------------------------------------------- 1 | Prefabricated System/Component Groups 2 | ===================================== 3 | 4 | General Game Mechanics 5 | ---------------------- 6 | 7 | FIXME: Clock, Inventory, Rooms, etc. 8 | 9 | 10 | WECS Components / Systems for Panda3D 11 | ------------------------------------- 12 | 13 | FIXME: There needs to be an actual overview here... 14 | 15 | There are some game mechanics for panda3d that come out of the box: 16 | 17 | * Camera modes: 18 | * First-person 19 | * Third-person 20 | * Turntable 21 | 22 | * Character controller: 23 | * Flying/Floating 24 | * Crouching, Walking, Running, Sprinting and Strafing 25 | * Bumping, Falling and Jumping 26 | * Basic NPC movement 27 | * Stamina 28 | 29 | * Animation: 30 | * Blended animations 31 | * Basic character controller animation 32 | -------------------------------------------------------------------------------- /docs/manual/prefab.rst: -------------------------------------------------------------------------------- 1 | Prefabricated System/Component Groups 2 | ===================================== 3 | 4 | General Game Mechanics 5 | ---------------------- 6 | 7 | FIXME: Clock, Inventory, Rooms, etc. 8 | 9 | WECS Components / Systems for Panda3D 10 | ------------------------------------- 11 | 12 | FIXME: There needs to be an actual overview here… 13 | 14 | There are some game mechanics for panda3d that come out of the box: 15 | 16 | - Camera modes: 17 | 18 | - First-person 19 | - Third-person 20 | - Turntable 21 | 22 | - Character controller: 23 | 24 | - Flying/Floating 25 | - Crouching, Walking, Running, Sprinting and Strafing 26 | - Bumping, Falling and Jumping 27 | - Basic NPC movement 28 | - Stamina 29 | 30 | - Animation: 31 | 32 | - Blended animations 33 | - Basic character controller animation 34 | -------------------------------------------------------------------------------- /docs/manual/readme.rst: -------------------------------------------------------------------------------- 1 | What is WECS? 2 | ------------- 3 | 4 | *WECS* stands for World, Entities, Components, Systems. It implements 5 | the architecture pattern known as [ECS, EC, Component system (and 6 | probably by several other names as well)] 7 | (https://en.wikipedia.org/wiki/Entity_component_system), which is 8 | popular in game development. 9 | 10 | WECS aims at putting usability first, and to not let performance 11 | optimizations compromise it. 12 | 13 | Beyond the core which implements the ECS, the goal of WECS is to 14 | accumulate enough game mechanics and boilerplate code so that the time 15 | between imagining a game and getting to the point where you actually 16 | work on your specific game mechanics is down to a few minutes. 17 | 18 | In particular, systems and components to work with the `Panda3D 19 | engine `__ are provided. 20 | 21 | Installation, etc. 22 | ------------------ 23 | 24 | - Installation: ``pip install wecs`` 25 | - Documentation: 26 | `readthedocs.io `__ 27 | - Source code: `GitHub 28 | repository `__ 29 | - Chat: `Panda3D Offtopic Discord 30 | server `__, channel 31 | `#wecs `__ 32 | 33 | Hello World 34 | ----------- 35 | 36 | .. code:: python 37 | 38 | from wecs.core import * 39 | 40 | world = World() 41 | 42 | A world contains ``Entities``; Also ``Systems``, more about those later. 43 | 44 | :: 45 | 46 | entity = world.create_entity() 47 | 48 | ``Entities`` themselves contain ``Components``; They are also nothing 49 | more than a container for components. They are a general form of “game 50 | object”, and can be turned into more specific objects by adding 51 | ``Components`` to them. 52 | 53 | ``Components`` are data structures with no inherent functionality (i.e. 54 | they have no functions). Their presence on an entity describes the state 55 | of an aspect of that entity. For example, a certain game object could be 56 | the player’s car, having a ``Geometry`` component with its graphical 57 | model, a ``Car`` component describing things like motor power and fuel 58 | in the tank, a ``PhysicsBody`` keeping track of the car’s representation 59 | in the physics simulation, and many more. Or it could be something as 60 | simple as “An entity with this component can count”, or “It can print 61 | its count (if it has one)”: 62 | 63 | :: 64 | 65 | @Component() 66 | class Counter: 67 | count: int = 0 68 | 69 | @Component() 70 | class Printer: 71 | name: str = "Bob" 72 | 73 | entity.add_component(Counter()) 74 | entity.add_component(Printer()) 75 | 76 | During each ``world.update()``, the ``World`` will go through its list 77 | of ``Systems``, and run each in turn. Each ``System`` has a list of 78 | ``Filters`` which test whether ``System`` should process an entity, and 79 | in what kind of role. 80 | 81 | :: 82 | 83 | class CountAndPrint(System): 84 | entity_filters = { 85 | 'count': Counter, 86 | 'print': and_filter(Counter, Printer), 87 | } 88 | 89 | def update(self, entities_by_filter): 90 | for entity in entities_by_filter['count']: 91 | entity[Counter].count += 1 92 | for entity in entities_by_filter['print']: 93 | msg = "{} has counted to {}.".format( 94 | entity[Printer].name, 95 | entity[Counter].count, 96 | ) 97 | print(msg) 98 | 99 | world.add_system(CountAndPrint(), 0) 100 | 101 | The ``0`` in ``world.add_system(CountAndPrint(), 0)`` is the ``sort`` of 102 | the system. Systems are run in ascending order of sort. As an aside, 103 | it’d be a good idea to break this into two systems; Who knows what else 104 | other people want to happen between counting and printing? 105 | 106 | Now we can step time forward in this little universe: 107 | 108 | :: 109 | 110 | world.update() 111 | Bob has counted to 1. 112 | 113 | This concludes the Hello World example. 114 | -------------------------------------------------------------------------------- /docs/manual/todo.md: -------------------------------------------------------------------------------- 1 | TODO List 2 | ========= 3 | 4 | Hot Topics 5 | ---------- 6 | 7 | * Pinned tasks 8 | * Update PyPI package 9 | * panda3d 10 | * Check the `task_mgr` for tasks already existing at a given sort 11 | * If that’s not possible, `System`ify existing Panda3D `tasks` 12 | * character.Walking 13 | * Decreased control while in the air 14 | * Null input should have zero effect, not effect towards zero 15 | movement 16 | * character.Jumping 17 | * Multijump 18 | * mechanics 19 | * Move `equipment`, `inventory`, and `rooms` here 20 | * Character animation 21 | 22 | 23 | Lukewarm 24 | -------- 25 | 26 | * `wecs.console` 27 | * The current version basically only shows that functionally, it 28 | exists. 29 | * It needs to look prettier 30 | * There needs to be insight into current component values 31 | * Entities should be pinnable, going to the top of the list 32 | * The list should be sortable / filterable by component presence and 33 | values 34 | * Components, and sets of them, should be drag-and-dropable from 35 | entity to entity 36 | * There should be entity / component creation, and a “shelf” to put 37 | (sets of) unattached components on 38 | * A waste bin that destroys entities / components dragged onto it 39 | * Adding / removing aspects 40 | * There should also be a column set for system membership 41 | 42 | 43 | Icebox 44 | ------ 45 | 46 | * Bugs 47 | * CharacterController: 48 | * Bumping: Go into an edge. You will find yourself sticking to it 49 | instead of gliding off to one side. 50 | * Bumping: Go through a thin wall. 51 | * Bumping: Walk into a wall at a near-perpendicular angle, 52 | drifting towards a corner. When the corner is reached, the 53 | character will take a sudden side step. Easy to see when 54 | walking into a tree. Probably the result not taking inertia 55 | into account. 56 | * Falling: Stand on a mountain ridge. You will jitter up and 57 | down. 58 | * example: Break Map / LoadMapsAndActors out of game.py 59 | * CollideCamerasWithTerrain 60 | * With the head stuck against a wall (e.g. in the tunnel), this 61 | places the camera into the wall, allowing to see through it. 62 | * If the angle camera-wall and camera-character is small, the 63 | wall gets culled, probably due to the near plane being in the 64 | wall. 65 | * Changes in camera distance after startup do not get respected. 66 | * Tests 67 | * Tests for `get_component_dependencies()` / 68 | `get_system_component_dependencies()` 69 | * Is there proper component cleanup when an entity is removed? 70 | * Does removing entities affect the currently running system? 71 | * Coverage is... lacking. 72 | * Documentation 73 | * More docstrings 74 | * doctests 75 | * Development pipeline 76 | * tox 77 | * core 78 | * API improvements 79 | * `entity = world[entity_uid]` 80 | * `entity = other_entity.get_component(Reference).uid` 81 | * Unique `Components`; Only one per type in the world at any given 82 | time, to be tested between removing old and adding new components? 83 | * De-/serialize world state 84 | * boilerplate 85 | * Dump `Aspect`s into graphviz 86 | * graphviz 87 | * Inheritance diagrams of `Aspect`s 88 | * panda3d 89 | * character 90 | * Bumpers bumping against each other, distributing the push 91 | between them. 92 | * climbing 93 | * ai 94 | * Turn towards entity 95 | * Move towards entity 96 | * Perceive entity 97 | * Debug console 98 | * mechanics 99 | * Meter systems: i.e. Health, Mana 100 | * ai 101 | * Hierarchical Finite State Machine 102 | * Behavior Trees 103 | * GOAP / STRIPS 104 | * All code 105 | * Change `filtered_entities` to `entities_by_filter` 106 | * `system.destroy_entity()` now gets `components_by_type` 107 | argument (in turn superceded by `exit_filter_foo(self.entity)`). 108 | * I’ve been really bad about implementing 109 | `system.destroy_entity()`s... 110 | * `clock.timestep` is deprecated. Replace with `.wall_time`, 111 | `.frame_time`, or `.game_time`. 112 | * examples: Minimalistic implementations of different genres, acting as 113 | guideposts for system / component development. 114 | * Walking simulator 115 | * documents / audio logs 116 | * triggering changes in the world 117 | * Platformer 118 | * 2D or 3D? Make sure that it doesn’t matter. 119 | * Minimal NPC AI 120 | * Twin stick shooter 121 | * Tactical NPC AI 122 | * Creed-like climber 123 | * Stealth game 124 | * First-person shooter: “Five Minutes of Violence” 125 | * Driving game: “Friction: Zero” 126 | * Abstract puzzle game: “sixxis” 127 | * Candidate for list culling: Probably provides no reusable 128 | mechanics 129 | * Match 3 130 | * Rhythm game 131 | * Candidate for list culling: Just a specific subgenre of 132 | abstract puzzle games. Then again, it is a specific mechanic 133 | that defines a (sub)genre... 134 | * Environmental puzzle game 135 | * Turn-based strategy 136 | * Strategic AI 137 | * Real-time strategy 138 | * Strategic AI 139 | * Point and click 140 | * Role-playing game 141 | * Character sheet and randomized skill tests 142 | * Talking 143 | * Adventure 144 | * Flight simulator 145 | * City / tycoon / business / farming / life simulation 146 | * Rail shooter / Shooting gallery 147 | * Brawler 148 | * Bullet Hell 149 | * Submarine simulator 150 | -------------------------------------------------------------------------------- /docs/manual/todo.rst: -------------------------------------------------------------------------------- 1 | TODO List 2 | ========= 3 | 4 | Hot Topics 5 | ---------- 6 | 7 | - Pinned tasks 8 | 9 | - Update PyPI package 10 | 11 | - panda3d 12 | 13 | - Check the ``task_mgr`` for tasks already existing at a given sort 14 | - If that’s not possible, ``System``\ ify existing Panda3D ``tasks`` 15 | - character.Walking 16 | 17 | - Decreased control while in the air 18 | - Null input should have zero effect, not effect towards zero 19 | movement 20 | 21 | - character.Jumping 22 | 23 | - Multijump 24 | 25 | - mechanics 26 | 27 | - Move ``equipment``, ``inventory``, and ``rooms`` here 28 | 29 | - Character animation 30 | 31 | Lukewarm 32 | -------- 33 | 34 | - ``wecs.console`` 35 | 36 | - The current version basically only shows that functionally, it 37 | exists. 38 | - It needs to look prettier 39 | - There needs to be insight into current component values 40 | - Entities should be pinnable, going to the top of the list 41 | - The list should be sortable / filterable by component presence and 42 | values 43 | - Components, and sets of them, should be drag-and-dropable from 44 | entity to entity 45 | - There should be entity / component creation, and a “shelf” to put 46 | (sets of) unattached components on 47 | - A waste bin that destroys entities / components dragged onto it 48 | - Adding / removing aspects 49 | - There should also be a column set for system membership 50 | 51 | Icebox 52 | ------ 53 | 54 | - Bugs 55 | 56 | - CharacterController: 57 | 58 | - Bumping: Go into an edge. You will find yourself sticking to it 59 | instead of gliding off to one side. 60 | - Bumping: Go through a thin wall. 61 | - Bumping: Walk into a wall at a near-perpendicular angle, 62 | drifting towards a corner. When the corner is reached, the 63 | character will take a sudden side step. Easy to see when 64 | walking into a tree. Probably the result not taking inertia 65 | into account. 66 | - Falling: Stand on a mountain ridge. You will jitter up and 67 | down. 68 | - example: Break Map / LoadMapsAndActors out of game.py 69 | 70 | - CollideCamerasWithTerrain 71 | 72 | - With the head stuck against a wall (e.g. in the tunnel), this 73 | places the camera into the wall, allowing to see through it. 74 | - If the angle camera-wall and camera-character is small, the 75 | wall gets culled, probably due to the near plane being in the 76 | wall. 77 | - Changes in camera distance after startup do not get respected. 78 | 79 | - Tests 80 | 81 | - Tests for ``get_component_dependencies()`` / 82 | ``get_system_component_dependencies()`` 83 | - Is there proper component cleanup when an entity is removed? 84 | - Does removing entities affect the currently running system? 85 | - Coverage is… lacking. 86 | 87 | - Documentation 88 | 89 | - More docstrings 90 | - doctests 91 | 92 | - Development pipeline 93 | 94 | - tox 95 | 96 | - core 97 | 98 | - API improvements 99 | 100 | - ``entity = world[entity_uid]`` 101 | - ``entity = other_entity.get_component(Reference).uid`` 102 | 103 | - Unique ``Components``; Only one per type in the world at any given 104 | time, to be tested between removing old and adding new components? 105 | - De-/serialize world state 106 | 107 | - boilerplate 108 | 109 | - Dump ``Aspect``\ s into graphviz 110 | 111 | - graphviz 112 | 113 | - Inheritance diagrams of ``Aspect``\ s 114 | 115 | - panda3d 116 | 117 | - character 118 | 119 | - Bumpers bumping against each other, distributing the push 120 | between them. 121 | - climbing 122 | 123 | - ai 124 | 125 | - Turn towards entity 126 | - Move towards entity 127 | - Perceive entity 128 | 129 | - Debug console 130 | 131 | - mechanics 132 | 133 | - Meter systems: i.e. Health, Mana 134 | 135 | - ai 136 | 137 | - Hierarchical Finite State Machine 138 | - Behavior Trees 139 | - GOAP / STRIPS 140 | 141 | - All code 142 | 143 | - Change ``filtered_entities`` to ``entities_by_filter`` 144 | - ``system.destroy_entity()`` now gets ``components_by_type`` 145 | argument (in turn superceded by ``exit_filter_foo(self.entity)``). 146 | - I’ve been really bad about implementing 147 | ``system.destroy_entity()``\ s… 148 | - ``clock.timestep`` is deprecated. Replace with ``.wall_time``, 149 | ``.frame_time``, or ``.game_time``. 150 | 151 | - examples: Minimalistic implementations of different genres, acting as 152 | guideposts for system / component development. 153 | 154 | - Walking simulator 155 | 156 | - documents / audio logs 157 | - triggering changes in the world 158 | 159 | - Platformer 160 | 161 | - 2D or 3D? Make sure that it doesn’t matter. 162 | - Minimal NPC AI 163 | 164 | - Twin stick shooter 165 | 166 | - Tactical NPC AI 167 | 168 | - Creed-like climber 169 | - Stealth game 170 | - First-person shooter: “Five Minutes of Violence” 171 | - Driving game: “Friction: Zero” 172 | - Abstract puzzle game: “sixxis” 173 | 174 | - Candidate for list culling: Probably provides no reusable 175 | mechanics 176 | 177 | - Match 3 178 | - Rhythm game 179 | 180 | - Candidate for list culling: Just a specific subgenre of 181 | abstract puzzle games. Then again, it is a specific mechanic 182 | that defines a (sub)genre… 183 | 184 | - Environmental puzzle game 185 | - Turn-based strategy 186 | 187 | - Strategic AI 188 | 189 | - Real-time strategy 190 | 191 | - Strategic AI 192 | 193 | - Point and click 194 | - Role-playing game 195 | 196 | - Character sheet and randomized skill tests 197 | - Talking 198 | 199 | - Adventure 200 | - Flight simulator 201 | - City / tycoon / business / farming / life simulation 202 | - Rail shooter / Shooting gallery 203 | - Brawler 204 | - Bullet Hell 205 | - Submarine simulator 206 | -------------------------------------------------------------------------------- /examples/minimal/main.py: -------------------------------------------------------------------------------- 1 | from wecs.core import World, Component, System, and_filter, UID, NoSuchUID 2 | 3 | 4 | # There is a world with an entity in it. 5 | world = World() 6 | entity = world.create_entity() 7 | 8 | 9 | # Entities can be counters. 10 | @Component() 11 | class Counter: 12 | value: int 13 | 14 | 15 | # The entity in the world is a counter. 16 | entity.add_component(Counter(value=0)) 17 | 18 | 19 | # It is possible that in a world, all counters increase their count by 20 | # one each frame. 21 | class Count(System): 22 | entity_filters = {'counts': and_filter([Counter])} 23 | 24 | def update(self, entities_by_filter): 25 | for entity in entities_by_filter['counts']: 26 | entity.get_component(Counter).value += 1 27 | 28 | 29 | # In this world, that is the case, and happens at time index 0 of the 30 | # frame. 31 | world.add_system(Count(), 0) 32 | 33 | # Let's make some time pass in the world. 34 | world.update() 35 | 36 | 37 | # Whoops, no output? Typically we'd add a component to the entity to 38 | # also make it a printer, but I want to show you entity references, 39 | # so we'll do this unnecessarily complicated. There'll be a system 40 | # that does printing for every entity *referenced by* a printing 41 | # entity. 42 | 43 | @Component() 44 | class Printer: 45 | printee: UID 46 | 47 | 48 | class Print(System): 49 | entity_filters = {'prints': and_filter([Printer])} 50 | 51 | def update(self, entities_by_filter): 52 | for entity in entities_by_filter['prints']: 53 | reference = entity.get_component(Printer).printee 54 | # Maybe the reference doesn't point anywhere anymore? 55 | if reference is None: 56 | print("Empty reference.") 57 | return 58 | # Since those references are UIDs of entitites, not entitites 59 | # themselves, we'll need to resolve them. It may happen that a 60 | # referenced entity has been destroyed, so we'll need to handle that 61 | # case here as well. 62 | try: 63 | printed_entity = self.world.get_entity(reference) 64 | except NoSuchUID: 65 | print("Dangling reference.") 66 | return 67 | # But is it even counter anymore? 68 | if not printed_entity.has_component(Counter): 69 | print("Referenced entity is not a counter.") 70 | return 71 | # Okay, so the entity's printee is an existing entity that is a 72 | # Counter. We can actually print its value! 73 | print(printed_entity.get_component(Counter).value) 74 | 75 | 76 | # So, let's update our world... 77 | world.add_system(Print(), 1) 78 | other_entity = world.create_entity() 79 | other_entity.add_component(Printer(printee=entity._uid)) 80 | # ...and see whether it works. 81 | world.update() 82 | # ...and if we make the entity a non-counter? 83 | entity.remove_component(Counter) 84 | world.update() 85 | # ...and if there is no other entity? 86 | world.destroy_entity(entity) 87 | world.update() 88 | # ...and if we unset the printee reference? 89 | other_entity.get_component(Printer).printee = None 90 | world.update() 91 | -------------------------------------------------------------------------------- /examples/panda3d-animation-lab/game.py: -------------------------------------------------------------------------------- 1 | from panda3d.core import Point3 2 | from panda3d.core import Vec3 3 | 4 | from wecs.aspects import Aspect 5 | from wecs import panda3d 6 | from wecs import mechanics 7 | from wecs import cefconsole 8 | from wecs.panda3d import aspects 9 | 10 | 11 | # Each frame, run these systems. This defines the game itself. 12 | system_types = [ 13 | panda3d.LoadModels, # Loads models, sets up actors, makes them collibable. 14 | mechanics.DetermineTimestep, # How long is this frame? Update all clocks. 15 | # What movement do the characters intend to do? 16 | panda3d.AcceptInput, # Input from player, ranges ([-1; 1]), not scaled for time. 17 | panda3d.Think, # Input from AIs, the same 18 | # panda3d.UpdateStamina, # A game mechanic that cancels move modes if the character is exhausted, "unintending" them 19 | # panda3d.TurningBackToCamera, # Characters can have a tendency towards walk towards away-from-camera that adjusts their intention. 20 | panda3d.UpdateCharacter, # Scale inputs by frame time, making them "Intended movement in this frame." 21 | # The following systems adjust the intended movement 22 | panda3d.Floating, # Scale by speed for floating 23 | panda3d.Walking, # Scale by speed for walk / run / crouch / sprint 24 | panda3d.Inertiing, # Clamp movement speed delta by inertia. 25 | panda3d.Bumping, # Bump into things (and out again). 26 | panda3d.Falling, # Fall, or stand on the ground. 27 | panda3d.Jumping, # Impart upward impulse. 28 | panda3d.ExecuteMovement, # Turn intention into actual movement. 29 | panda3d.AnimateCharacter, 30 | panda3d.Animate, 31 | # We're done with character movement, now update the cameras and console. 32 | panda3d.UpdateCameras, 33 | # panda3d.CollideCamerasWithTerrain, 34 | cefconsole.UpdateWecsSubconsole, 35 | cefconsole.WatchEntitiesInSubconsole, 36 | ] 37 | 38 | 39 | # Aspects are basically classes for entities. Here are two that we will use. 40 | game_map = Aspect( 41 | [panda3d.Position, 42 | panda3d.Model, 43 | panda3d.Scene, 44 | panda3d.CollidableGeometry, 45 | panda3d.FlattenStrong, 46 | cefconsole.WatchedEntity, 47 | ], 48 | overrides={ 49 | panda3d.Model: dict(model_name='grid.bam'), 50 | panda3d.Scene: dict(node=base.render), 51 | }, 52 | ) 53 | lab_character = Aspect([aspects.player_character, cefconsole.WatchedEntity]) 54 | 55 | 56 | # Create entities 57 | game_map.add(base.ecs_world.create_entity()) 58 | lab_character.add(base.ecs_world.create_entity(name="peter")) 59 | -------------------------------------------------------------------------------- /examples/panda3d-animation-lab/grid.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-animation-lab/grid.bam -------------------------------------------------------------------------------- /examples/panda3d-animation-lab/grid.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-animation-lab/grid.blend -------------------------------------------------------------------------------- /examples/panda3d-animation-lab/grid_small.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-animation-lab/grid_small.bam -------------------------------------------------------------------------------- /examples/panda3d-animation-lab/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from wecs import boilerplate 4 | 5 | 6 | if __name__ == '__main__': 7 | boilerplate.run_game(console=True) 8 | -------------------------------------------------------------------------------- /examples/panda3d-behaviors/behaviors.py: -------------------------------------------------------------------------------- 1 | from pychology.behavior_trees import Action 2 | from pychology.behavior_trees import Priorities 3 | from pychology.behavior_trees import Chain 4 | from pychology.behavior_trees import DoneOnPrecondition 5 | from pychology.behavior_trees import FailOnPrecondition 6 | 7 | import wecs 8 | 9 | from wecs.panda3d.behavior_trees import DoneTimer 10 | from wecs.panda3d.behavior_trees import IdleWhenDoneTree 11 | 12 | 13 | def idle(): 14 | return IdleWhenDoneTree( 15 | Chain( 16 | DoneTimer( 17 | wecs.panda3d.behavior_trees.timeout(3.0), 18 | Action(wecs.panda3d.behavior_trees.turn(1.0)), 19 | ), 20 | DoneTimer( 21 | wecs.panda3d.behavior_trees.timeout(3.0), 22 | Action(wecs.panda3d.behavior_trees.turn(-1.0)), 23 | ), 24 | ), 25 | ) 26 | 27 | 28 | def walk_to_entity(): 29 | return IdleWhenDoneTree( 30 | Priorities( 31 | FailOnPrecondition( 32 | wecs.panda3d.behavior_trees.is_pointable, 33 | DoneOnPrecondition( 34 | wecs.panda3d.behavior_trees.distance_smaller(1.5), 35 | Action(wecs.panda3d.behavior_trees.walk_to_entity), 36 | ), 37 | ), 38 | DoneOnPrecondition( 39 | wecs.panda3d.behavior_trees.distance_smaller(0.01), 40 | Action(wecs.panda3d.behavior_trees.walk_to_entity), 41 | ), 42 | ), 43 | ) 44 | -------------------------------------------------------------------------------- /examples/panda3d-behaviors/game.py: -------------------------------------------------------------------------------- 1 | import wecs 2 | 3 | import aspects 4 | import avatar_ui 5 | 6 | 7 | # Each frame, run these systems. This defines the game itself. 8 | system_types = [ 9 | # Set up newly added models/camera, tear down removed ones 10 | wecs.panda3d.prototype.ManageModels, 11 | wecs.panda3d.camera.PrepareCameras, 12 | # Update clocks 13 | wecs.mechanics.clock.DetermineTimestep, 14 | # Interface interactions 15 | wecs.panda3d.mouseover.MouseOverOnEntity, 16 | wecs.panda3d.mouseover.UpdateMouseOverUI, 17 | avatar_ui.AvatarUI, 18 | # Set inputs to the character controller 19 | wecs.panda3d.ai.Think, 20 | wecs.panda3d.ai.BehaviorInhibitsDirectCharacterControl, 21 | wecs.panda3d.character.UpdateCharacter, 22 | # Fudge the inputs to achieve the kind of control that you want 23 | wecs.panda3d.character.ReorientInputBasedOnCamera, 24 | # Character controller 25 | wecs.panda3d.character.Floating, 26 | wecs.panda3d.character.Walking, 27 | wecs.panda3d.character.Inertiing, 28 | wecs.panda3d.character.Bumping, 29 | wecs.panda3d.character.Falling, 30 | wecs.panda3d.character.Jumping, 31 | wecs.panda3d.character.DirectlyIndicateDirection, 32 | wecs.panda3d.character.TurningBackToCamera, 33 | wecs.panda3d.character.AutomaticallyTurnTowardsDirection, 34 | wecs.panda3d.character.ExecuteMovement, 35 | # Animation 36 | wecs.panda3d.animation.AnimateCharacter, 37 | wecs.panda3d.animation.Animate, 38 | # Camera 39 | wecs.panda3d.camera.ReorientObjectCentricCamera, 40 | wecs.panda3d.camera.ZoomObjectCentricCamera, 41 | wecs.panda3d.camera.CollideCamerasWithTerrain, 42 | # Debug keys (`escape` to close, etc.) 43 | wecs.panda3d.debug.DebugTools, 44 | ] 45 | 46 | 47 | # Now let's create Rebeccas at the spawn points: 48 | 49 | aspects.non_player_character.add( 50 | base.ecs_world.create_entity(name="Rebecca 1"), 51 | overrides={ 52 | **aspects.rebecca, 53 | **aspects.spawn_point_1, 54 | }, 55 | ) 56 | 57 | 58 | aspects.non_player_character.add( 59 | base.ecs_world.create_entity(name="Rebecca 2"), 60 | overrides={ 61 | **aspects.rebecca, 62 | **aspects.spawn_point_2, 63 | }, 64 | ) 65 | 66 | 67 | aspects.non_player_character.add( 68 | base.ecs_world.create_entity(name="Rebecca 3"), 69 | overrides={ 70 | **aspects.rebecca, 71 | **aspects.spawn_point_3, 72 | }, 73 | ) 74 | 75 | 76 | # ...and a player 77 | 78 | aspects.observer.add( 79 | base.ecs_world.create_entity(name="Observer"), 80 | overrides={ 81 | **aspects.spawn_point_air, 82 | }, 83 | ) 84 | 85 | # To be created as a player character, instead just do this: 86 | # 87 | # aspects.player_character.add( 88 | # base.ecs_world.create_entity(name="Playerbecca"), 89 | # overrides={ 90 | # **aspects.rebecca, 91 | # **aspects.spawn_point_air, 92 | # }, 93 | # ) 94 | -------------------------------------------------------------------------------- /examples/panda3d-behaviors/keybindings.config: -------------------------------------------------------------------------------- 1 | context debug 2 | trigger quit 3 | keyboard escape 4 | trigger console 5 | keyboard f9 6 | trigger frame_rate_meter 7 | keyboard f10 8 | trigger pdb 9 | keyboard f11 10 | trigger pstats 11 | keyboard f12 12 | context character_movement 13 | axis2d direction 14 | gamepad left_x:deadzone=0.02 left_y:deadzone=0.02 15 | keyboard a d s w 16 | axis2d rotation 17 | gamepad dpad_left dpad_right dpad_down dpad_up 18 | trigger jump 19 | gamepad face_a 20 | keyboard space 21 | button crouch 22 | gamepad face_y 23 | keyboard c 24 | button sprints 25 | gamepad ltrigger 26 | keyboard e 27 | context camera_movement 28 | axis2d rotation 29 | gamepad right_x:deadzone=0.02 right_y:deadzone=0.02:flip 30 | keyboard arrow_left arrow_right arrow_downw arrow_up 31 | context camera_zoom 32 | axis zoom 33 | keyboard u o 34 | context clock_control 35 | axis time_zoom 36 | keyboard - + 37 | context select_entity 38 | trigger select 39 | keyboard mouse1 40 | trigger command 41 | keyboard mouse3 42 | trigger embody 43 | keyboard 1 44 | -------------------------------------------------------------------------------- /examples/panda3d-behaviors/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from wecs import boilerplate 6 | 7 | 8 | if __name__ == '__main__': 9 | boilerplate.run_game( 10 | keybindings=True, 11 | ) 12 | -------------------------------------------------------------------------------- /examples/panda3d-character-controller-minimal/game.py: -------------------------------------------------------------------------------- 1 | from panda3d.core import Point3 2 | from panda3d.core import Vec3 3 | from panda3d.core import CollisionSphere 4 | 5 | # from wecs import cefconsole 6 | import wecs 7 | from wecs.core import ProxyType 8 | from wecs.aspects import Aspect 9 | from wecs.aspects import factory 10 | # from wecs.panda3d import debug 11 | 12 | from wecs.panda3d.constants import FALLING_MASK 13 | from wecs.panda3d.constants import BUMPING_MASK 14 | 15 | 16 | m_proxy = { 17 | 'model': ProxyType(wecs.panda3d.prototype.Model, 'node'), 18 | } 19 | cn_proxy = { 20 | 'character_node': ProxyType(wecs.panda3d.prototype.Model, 'node'), 21 | 'scene_node': ProxyType(wecs.panda3d.prototype.Model, 'parent'), 22 | } 23 | 24 | 25 | # Each frame, run these systems. This defines the game itself. 26 | system_types = [ 27 | wecs.panda3d.prototype.ManageModels, 28 | wecs.panda3d.camera.PrepareCameras(proxies=m_proxy), 29 | wecs.mechanics.clock.DetermineTimestep, 30 | wecs.panda3d.character.UpdateCharacter(proxies=cn_proxy), 31 | wecs.panda3d.character.Walking, 32 | wecs.panda3d.character.Bumping(proxies=cn_proxy), 33 | wecs.panda3d.character.Falling(proxies=cn_proxy), 34 | wecs.panda3d.character.ExecuteMovement(proxies=cn_proxy), 35 | wecs.panda3d.camera.ReorientObjectCentricCamera, 36 | ] 37 | 38 | 39 | # Map 40 | 41 | game_map = Aspect( 42 | [ 43 | wecs.panda3d.prototype.Model, 44 | wecs.panda3d.prototype.Geometry, 45 | wecs.panda3d.prototype.CollidableGeometry, 46 | wecs.panda3d.prototype.FlattenStrong, 47 | ], 48 | overrides={ 49 | wecs.panda3d.prototype.Geometry: dict(file='roadE.bam'), 50 | wecs.panda3d.prototype.CollidableGeometry: dict( 51 | mask=FALLING_MASK|BUMPING_MASK, 52 | ), 53 | }, 54 | ) 55 | 56 | 57 | map_entity = base.ecs_world.create_entity(name="Level geometry") 58 | game_map.add(map_entity) 59 | 60 | 61 | # Player 62 | 63 | character = Aspect( 64 | [ 65 | wecs.mechanics.clock.Clock, 66 | wecs.panda3d.prototype.Model, 67 | wecs.panda3d.prototype.Geometry, 68 | wecs.panda3d.character.CharacterController, 69 | ], 70 | overrides={ 71 | wecs.mechanics.clock.Clock: dict( 72 | clock=lambda: factory(wecs.mechanics.clock.panda3d_clock), 73 | ), 74 | }, 75 | ) 76 | 77 | 78 | def peter_bumper(): 79 | return { 80 | 'bumper': dict( 81 | shape=CollisionSphere, 82 | center=Vec3(0.0, 0.0, 1.0), 83 | radius=0.7, 84 | ), 85 | } 86 | 87 | 88 | def peter_lifter(): 89 | return { 90 | 'lifter': dict( 91 | shape=CollisionSphere, 92 | center=Vec3(0.0, 0.0, 0.25), 93 | radius=0.5, 94 | ), 95 | } 96 | 97 | 98 | walking = Aspect( 99 | [ 100 | wecs.panda3d.character.WalkingMovement, 101 | wecs.panda3d.character.BumpingMovement, 102 | wecs.panda3d.character.FallingMovement, 103 | ], 104 | overrides={ 105 | wecs.panda3d.character.BumpingMovement: dict(solids=factory(peter_bumper)), 106 | wecs.panda3d.character.FallingMovement: dict(solids=factory(peter_lifter)), 107 | }, 108 | ) 109 | 110 | 111 | avatar = Aspect( 112 | [ 113 | character, 114 | walking, 115 | ], 116 | overrides={ 117 | wecs.panda3d.prototype.Geometry: dict(file='../../assets/peter.bam'), 118 | }, 119 | ) 120 | 121 | 122 | third_person = Aspect([ 123 | wecs.panda3d.camera.Camera, 124 | wecs.panda3d.camera.ObjectCentricCameraMode, 125 | ]) 126 | 127 | 128 | pc_mind = Aspect( 129 | [ 130 | wecs.panda3d.input.Input, 131 | ], 132 | overrides={ 133 | wecs.panda3d.input.Input: dict( 134 | contexts=[ 135 | 'character_movement', 136 | 'camera_movement', 137 | ], 138 | ), 139 | }, 140 | ) 141 | 142 | 143 | player_character = Aspect([avatar, pc_mind, third_person]) 144 | 145 | 146 | player_character.add( 147 | base.ecs_world.create_entity(name="Playerbecca"), 148 | overrides={ 149 | wecs.panda3d.prototype.Model: dict( 150 | post_attach=lambda: wecs.panda3d.prototype.transform( 151 | pos=Vec3(50, 290, 0), 152 | ), 153 | ), 154 | }, 155 | ) 156 | -------------------------------------------------------------------------------- /examples/panda3d-character-controller-minimal/keybindings.config: -------------------------------------------------------------------------------- 1 | context debug 2 | trigger quit 3 | keyboard escape 4 | trigger console 5 | keyboard f9 6 | trigger frame_rate_meter 7 | keyboard f10 8 | trigger pdb 9 | keyboard f11 10 | trigger pstats 11 | keyboard f12 12 | context character_movement 13 | axis2d direction 14 | gamepad left_x left_y 15 | spatial_mouse x:scale=3 y:scale=3 16 | keyboard a d s w 17 | axis2d rotation 18 | gamepad dpad_left dpad_right dpad_down dpad_up 19 | keyboard arrow_left arrow_right arrow_down arrow_up 20 | trigger jump 21 | gamepad face_x 22 | spatial_mouse z:button>=0.3 23 | keyboard space 24 | button crouch 25 | gamepad face_b 26 | spatial_mouse z:button<=-0.3 27 | keyboard c 28 | button sprint 29 | gamepad ltrigger 30 | keyboard e 31 | context camera_movement 32 | axis2d rotation 33 | gamepad right_x:exp=2 right_y:exp=2:scale=-1 34 | spatial_mouse yaw:flip:scale=2 pitch 35 | keyboard mouse_x_delta mouse_y_delta 36 | axis zoom 37 | keyboard u o 38 | context clock_control 39 | axis time_zoom 40 | keyboard - + 41 | -------------------------------------------------------------------------------- /examples/panda3d-character-controller-minimal/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from wecs import boilerplate 6 | 7 | 8 | if __name__ == '__main__': 9 | boilerplate.run_game( 10 | keybindings=True, 11 | ) 12 | -------------------------------------------------------------------------------- /examples/panda3d-character-controller-minimal/roadE.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-character-controller-minimal/roadE.bam -------------------------------------------------------------------------------- /examples/panda3d-character-controller/keybindings.config: -------------------------------------------------------------------------------- 1 | context debug 2 | trigger quit 3 | keyboard escape 4 | trigger console 5 | keyboard f9 6 | trigger frame_rate_meter 7 | keyboard f10 8 | trigger pdb 9 | keyboard f11 10 | trigger pstats 11 | keyboard f12 12 | context character_movement 13 | axis2d direction 14 | gamepad left_x left_y 15 | spatial_mouse x:scale=3 y:scale=3 16 | keyboard a d s w 17 | axis2d rotation 18 | gamepad dpad_left dpad_right dpad_down dpad_up 19 | keyboard arrow_left arrow_right arrow_down arrow_up 20 | trigger jump 21 | gamepad face_x 22 | spatial_mouse z:button>=0.3 23 | keyboard space 24 | button crouch 25 | gamepad face_b 26 | spatial_mouse z:button<=-0.3 27 | keyboard c 28 | button sprint 29 | gamepad ltrigger 30 | keyboard e 31 | context camera_movement 32 | axis2d rotation 33 | gamepad right_x:exp=2 right_y:exp=2:scale=-1 34 | spatial_mouse yaw:flip:scale=2 pitch 35 | keyboard mouse_x_delta mouse_y_delta 36 | axis zoom 37 | keyboard u o 38 | context clock_control 39 | axis time_zoom 40 | keyboard - + 41 | -------------------------------------------------------------------------------- /examples/panda3d-character-controller/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from wecs import boilerplate 6 | 7 | 8 | if __name__ == '__main__': 9 | boilerplate.run_game( 10 | keybindings=True, 11 | ) 12 | -------------------------------------------------------------------------------- /examples/panda3d-character-controller/roadE.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-character-controller/roadE.bam -------------------------------------------------------------------------------- /examples/panda3d-cutting-edge/behaviors.py: -------------------------------------------------------------------------------- 1 | from pychology.behavior_trees import Action 2 | from pychology.behavior_trees import Priorities 3 | from pychology.behavior_trees import Chain 4 | from pychology.behavior_trees import DoneOnPrecondition 5 | from pychology.behavior_trees import FailOnPrecondition 6 | 7 | import wecs 8 | 9 | from wecs.panda3d.behavior_trees import DoneTimer 10 | from wecs.panda3d.behavior_trees import IdleWhenDoneTree 11 | 12 | 13 | def idle(): 14 | return IdleWhenDoneTree( 15 | Chain( 16 | DoneTimer( 17 | wecs.panda3d.behavior_trees.timeout(3.0), 18 | Action(wecs.panda3d.behavior_trees.turn(1.0)), 19 | ), 20 | DoneTimer( 21 | wecs.panda3d.behavior_trees.timeout(3.0), 22 | Action(wecs.panda3d.behavior_trees.turn(-1.0)), 23 | ), 24 | ), 25 | ) 26 | 27 | 28 | def walk_to_entity(): 29 | return IdleWhenDoneTree( 30 | Priorities( 31 | FailOnPrecondition( 32 | wecs.panda3d.behavior_trees.is_pointable, 33 | DoneOnPrecondition( 34 | wecs.panda3d.behavior_trees.distance_smaller(1.5), 35 | Action(wecs.panda3d.behavior_trees.walk_to_entity), 36 | ), 37 | ), 38 | DoneOnPrecondition( 39 | wecs.panda3d.behavior_trees.distance_smaller(0.01), 40 | Action(wecs.panda3d.behavior_trees.walk_to_entity), 41 | ), 42 | ), 43 | ) 44 | -------------------------------------------------------------------------------- /examples/panda3d-cutting-edge/game.py: -------------------------------------------------------------------------------- 1 | import assetcoop 2 | 3 | import wecs 4 | 5 | import aspects 6 | import avatar_ui 7 | 8 | 9 | # Each frame, run these systems. This defines the game itself. 10 | system_types = [ 11 | # Set up newly added models/camera, tear down removed ones 12 | wecs.panda3d.prototype.ManageModels, 13 | wecs.panda3d.spawnpoints.Spawn, 14 | wecs.panda3d.camera.PrepareCameras, 15 | # Update clocks 16 | wecs.mechanics.clock.DetermineTimestep, 17 | # Interface interactions 18 | wecs.panda3d.mouseover.MouseOverOnEntity, 19 | wecs.panda3d.mouseover.UpdateMouseOverUI, 20 | avatar_ui.AvatarUI, 21 | # Set inputs to the character controller 22 | wecs.panda3d.ai.Think, 23 | wecs.panda3d.ai.BehaviorInhibitsDirectCharacterControl, 24 | wecs.panda3d.character.UpdateCharacter, 25 | # Fudge the inputs to achieve the kind of control that you want 26 | wecs.panda3d.character.ReorientInputBasedOnCamera, 27 | # Character controller 28 | wecs.panda3d.character.Floating, 29 | wecs.panda3d.character.Walking, 30 | wecs.panda3d.character.Inertiing, 31 | wecs.panda3d.character.Bumping, 32 | wecs.panda3d.character.Falling, 33 | wecs.panda3d.character.Jumping, 34 | wecs.panda3d.character.DirectlyIndicateDirection, 35 | wecs.panda3d.character.TurningBackToCamera, 36 | wecs.panda3d.character.AutomaticallyTurnTowardsDirection, 37 | wecs.panda3d.character.ExecuteMovement, 38 | # Animation 39 | wecs.panda3d.animation.AnimateCharacter, 40 | wecs.panda3d.animation.Animate, 41 | # Camera 42 | wecs.panda3d.camera.ReorientObjectCentricCamera, 43 | wecs.panda3d.camera.ZoomObjectCentricCamera, 44 | wecs.panda3d.camera.CollideCamerasWithTerrain, 45 | # Debug keys (`escape` to close, etc.) 46 | wecs.panda3d.debug.DebugTools, 47 | ] 48 | 49 | 50 | aspects.game_map.add( 51 | base.ecs_world.create_entity(name="Level geometry"), 52 | overrides={ 53 | wecs.panda3d.prototype.Geometry: dict( 54 | #file='models/scenes/lona.bam', 55 | file='rectangle_map.bam', 56 | ), 57 | }, 58 | ) 59 | 60 | 61 | #aspects.observer.add( 62 | aspects.player_character.add( 63 | base.ecs_world.create_entity(name="Playerbecca"), 64 | overrides={ 65 | wecs.panda3d.spawnpoints.SpawnAt: dict( 66 | #name='spawn_player_a', 67 | name='spawn_point_a_10', 68 | ), 69 | **aspects.rebecca, 70 | }, 71 | ) 72 | 73 | 74 | for i in range(0, 21, 9): 75 | aspects.non_player_character.add( 76 | base.ecs_world.create_entity(name="NonPlayerbecca_{i}"), 77 | overrides={ 78 | wecs.panda3d.spawnpoints.SpawnAt: dict( 79 | name=f'spawn_point_b_{i}', 80 | ), 81 | **aspects.rebecca, 82 | }, 83 | ) 84 | 85 | 86 | aspects.prop.add( 87 | base.ecs_world.create_entity(name="Park bench"), 88 | overrides={ 89 | wecs.panda3d.spawnpoints.SpawnAt: dict( 90 | name=f'spawn_point_a_2', 91 | ), 92 | **aspects.bench, 93 | }, 94 | ) 95 | 96 | # aspects.non_player_character.add( 97 | # base.ecs_world.create_entity(name="NonPlayerbecca_1"), 98 | # overrides={ 99 | # wecs.panda3d.spawnpoints.SpawnAt: dict( 100 | # name='spawn_player_a', 101 | # ), 102 | # **aspects.rebecca, 103 | # }, 104 | # ) 105 | # 106 | # 107 | # aspects.non_player_character.add( 108 | # base.ecs_world.create_entity(name="NonPlayerbecca_2"), 109 | # overrides={ 110 | # wecs.panda3d.spawnpoints.SpawnAt: dict( 111 | # name='spawn_player_b', 112 | # ), 113 | # **aspects.rebecca, 114 | # }, 115 | # ) 116 | # 117 | # 118 | # aspects.non_player_character.add( 119 | # base.ecs_world.create_entity(name="NonPlayerbecca_3"), 120 | # overrides={ 121 | # wecs.panda3d.spawnpoints.SpawnAt: dict( 122 | # name='spawn_player_c', 123 | # ), 124 | # **aspects.rebecca, 125 | # }, 126 | # ) 127 | -------------------------------------------------------------------------------- /examples/panda3d-cutting-edge/keybindings.config: -------------------------------------------------------------------------------- 1 | context debug 2 | trigger quit 3 | keyboard escape 4 | trigger console 5 | keyboard f9 6 | trigger frame_rate_meter 7 | keyboard f10 8 | trigger pdb 9 | keyboard f11 10 | trigger pstats 11 | keyboard f12 12 | context character_movement 13 | axis2d direction 14 | gamepad left_x:deadzone=0.02 left_y:deadzone=0.02 15 | keyboard a d s w 16 | axis2d rotation 17 | gamepad dpad_left dpad_right dpad_down dpad_up 18 | trigger jump 19 | gamepad face_a 20 | keyboard space 21 | button crouch 22 | gamepad face_y 23 | keyboard c 24 | button sprints 25 | gamepad ltrigger 26 | keyboard e 27 | context camera_movement 28 | axis2d rotation 29 | gamepad right_x:deadzone=0.02 right_y:deadzone=0.02:flip 30 | keyboard arrow_left arrow_right arrow_down arrow_up 31 | context camera_zoom 32 | axis zoom 33 | keyboard u o 34 | context character_direction 35 | axis2d direction 36 | gamepad right_x:deadzone=0.02 right_y:deadzone=0.02 37 | keyboard arrow_left arrow_right arrow_down arrow_up 38 | context clock_control 39 | axis time_zoom 40 | keyboard - + 41 | context select_entity 42 | trigger select 43 | keyboard mouse1 44 | trigger command 45 | keyboard mouse3 46 | trigger embody 47 | keyboard 1 48 | -------------------------------------------------------------------------------- /examples/panda3d-cutting-edge/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from wecs import boilerplate 6 | 7 | 8 | if __name__ == '__main__': 9 | boilerplate.run_game( 10 | keybindings=True, 11 | simplepbr=True, 12 | simplepbr_kwargs=dict( 13 | msaa_samples=1, 14 | max_lights=8, 15 | use_emission_maps=True, 16 | use_occlusion_maps=True, 17 | use_normal_maps=False, # FIXME: get a GPU that can do this 18 | enable_shadows=False, # FIXME: get a GPU that can do this 19 | ), 20 | ) 21 | -------------------------------------------------------------------------------- /examples/panda3d-cutting-edge/make_lab_map.py: -------------------------------------------------------------------------------- 1 | from panda3d.core import NodePath 2 | from panda3d.core import CardMaker 3 | from panda3d.core import Material 4 | from panda3d.core import PNMImage 5 | from panda3d.core import Texture 6 | from panda3d.core import TextureStage 7 | from panda3d.core import DirectionalLight 8 | from panda3d.core import AmbientLight 9 | 10 | 11 | cm = CardMaker('card') 12 | cm.set_frame(-31, 31, -31, 31) 13 | 14 | map_np = NodePath("map") 15 | card = map_np.attach_new_node(cm.generate()) 16 | card.set_p(-90) 17 | 18 | mat = Material() 19 | mat.set_base_color((1.0, 1.0, 1.0, 1)) 20 | mat.set_emission((1.0, 1.0, 1.0, 1)) 21 | mat.set_metallic(1.0) 22 | mat.set_roughness(1.0) 23 | card.set_material(mat) 24 | 25 | 26 | texture_size = 256 27 | 28 | base_color_pnm = PNMImage(texture_size, texture_size) 29 | base_color_pnm.fill(0.72, 0.45, 0.2) # Copper 30 | base_color_tex = Texture("BaseColor") 31 | base_color_tex.load(base_color_pnm) 32 | ts = TextureStage('BaseColor') # a.k.a. Modulate 33 | ts.set_mode(TextureStage.M_modulate) 34 | card.set_texture(ts, base_color_tex) 35 | 36 | 37 | # Emission; Gets multiplied with mat.emission 38 | emission_pnm = PNMImage(texture_size, texture_size) 39 | emission_pnm.fill(0.0, 0.0, 0.0) 40 | emission_tex = Texture("Emission") 41 | emission_tex.load(emission_pnm) 42 | ts = TextureStage('Emission') 43 | ts.set_mode(TextureStage.M_emission) 44 | card.set_texture(ts, emission_tex) 45 | 46 | 47 | # Ambient Occlusion, Roughness, Metallicity 48 | # R: Ambient Occlusion 49 | # G: Roughness, a.k.a. gloss map (if inverted); Gets multiplied with mat.roughness 50 | # B: Metallicity; Gets multiplied with mat.metallic 51 | metal_rough_pnm = PNMImage(texture_size, texture_size) 52 | ambient_occlusion = 1.0 53 | roughness = 0.18 54 | metallicity = 0.12 # 0.98 55 | metal_rough_pnm.fill(ambient_occlusion, roughness, metallicity) 56 | metal_rough_tex = Texture("MetalRoughness") 57 | metal_rough_tex.load(metal_rough_pnm) 58 | ts = TextureStage('MetalRoughness') # a.k.a. Selector 59 | ts.set_mode(TextureStage.M_selector) 60 | card.set_texture(ts, metal_rough_tex) 61 | 62 | 63 | # Normals 64 | # RGB is the normalized normal vector (z is perpendicular to 65 | # the surface) * 0.5 + 0.5 66 | normal_pnm = PNMImage(texture_size, texture_size) 67 | normal_pnm.fill(0.5, 0.5, 0.1) 68 | normal_tex = Texture("Normals") 69 | normal_tex.load(normal_pnm) 70 | ts = TextureStage('Normals') 71 | ts.set_mode(TextureStage.M_normal) 72 | card.set_texture(ts, normal_tex) 73 | 74 | 75 | for i in range(21): 76 | sp = map_np.attach_new_node(f'spawn_point_a_{i}') 77 | sp.set_pos(-30 + i*3, -30, 0) 78 | sp = map_np.attach_new_node(f'spawn_point_b_{i}') 79 | sp.set_pos(-30 + i*3, 30, 0) 80 | sp.set_h(180) 81 | 82 | 83 | # Lights 84 | ambient_light = map_np.attach_new_node(AmbientLight('ambient')) 85 | ambient_light.node().set_color((.1, .1, .1, 1)) 86 | map_np.set_light(ambient_light) 87 | 88 | 89 | direct_light = map_np.attach_new_node(DirectionalLight('light')) 90 | direct_light.node().set_color((.8, .8, .8, 1)) 91 | map_np.set_light(direct_light) 92 | direct_light.set_pos(0, 0, 10) 93 | direct_light.look_at(0, 0, 0) 94 | 95 | 96 | # Save it 97 | map_np.write_bam_file("rectangle_map.bam") 98 | 99 | 100 | # octaves = 2 101 | # 102 | # vdata = GeomVertexData('name', format, Geom.UHStatic) 103 | # vdata.setNumRows(4) 104 | # 105 | # vertex = GeomVertexWriter(vdata, 'vertex') 106 | # color = GeomVertexWriter(vdata, 'color') 107 | # 108 | # vertex.addData3(1, 0, 0) 109 | # color.addData4(0, 0, 1, 1) 110 | -------------------------------------------------------------------------------- /examples/panda3d-cutting-edge/rectangle_map.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-cutting-edge/rectangle_map.bam -------------------------------------------------------------------------------- /examples/panda3d-cutting-edge/simplepbr_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from math import sin 3 | from math import cos 4 | from math import sqrt 5 | from random import gauss 6 | 7 | from panda3d.core import Vec2 8 | from panda3d.core import Vec3 9 | from panda3d.core import CardMaker 10 | from panda3d.core import Material 11 | from panda3d.core import PNMImage 12 | from panda3d.core import Texture 13 | from panda3d.core import TextureStage 14 | from panda3d.core import DirectionalLight 15 | from panda3d.core import AmbientLight 16 | 17 | from direct.showbase.ShowBase import ShowBase 18 | from direct.filter.CommonFilters import CommonFilters 19 | 20 | import simplepbr 21 | 22 | 23 | ShowBase() 24 | base.accept('escape', sys.exit) 25 | def debug(): 26 | import pdb 27 | pdb.set_trace() 28 | base.accept('f11', debug) 29 | base.set_frame_rate_meter(True) 30 | base.cam.set_pos(0, -5, 0) 31 | base.cam.look_at(0, 0, 0) 32 | global pipeline 33 | pipeline = simplepbr.init( 34 | msaa_samples=1, 35 | max_lights=8, 36 | #enable_shadows=True, 37 | ) 38 | pipeline.use_normal_maps = False 39 | pipeline.use_emission_maps = True 40 | pipeline.use_occlusion_maps = True 41 | 42 | 43 | cm = CardMaker('card') 44 | 45 | 46 | cm.set_frame(-1, 1, -1, 1) 47 | card_np = base.render.attach_new_node(cm.generate()) 48 | 49 | 50 | mat = Material() 51 | mat.set_base_color((1.0, 1.0, 1.0, 1)) 52 | mat.set_emission((1.0, 1.0, 1.0, 1)) 53 | mat.set_metallic(1.0) 54 | mat.set_roughness(1.0) 55 | card_np.set_material(mat) 56 | 57 | 58 | texture_size = 256 59 | texture_bands_x = 2 60 | texture_bands_y = 2 61 | 62 | 63 | # Base color, a.k.a. Modulate, a.k.a. albedo map 64 | # Gets multiplied with mat.base_color 65 | base_color_pnm = PNMImage(texture_size, texture_size) 66 | base_color_pnm.fill(0.72, 0.45, 0.2) # Copper 67 | base_color_tex = Texture("BaseColor") 68 | base_color_tex.load(base_color_pnm) 69 | ts = TextureStage('BaseColor') # a.k.a. Modulate 70 | ts.set_mode(TextureStage.M_modulate) 71 | card_np.set_texture(ts, base_color_tex) 72 | 73 | 74 | # Emission; Gets multiplied with mat.emission 75 | emission_pnm = PNMImage(texture_size, texture_size) 76 | emission_pnm.fill(0.0, 0.0, 0.0) 77 | emission_tex = Texture("Emission") 78 | emission_tex.load(emission_pnm) 79 | ts = TextureStage('Emission') 80 | ts.set_mode(TextureStage.M_emission) 81 | card_np.set_texture(ts, emission_tex) 82 | 83 | 84 | # Ambient Occlusion, Roughness, Metallicity 85 | # R: Ambient Occlusion 86 | # G: Roughness, a.k.a. gloss map (if inverted); Gets multiplied with mat.roughness 87 | # B: Metallicity; Gets multiplied with mat.metallic 88 | metal_rough_pnm = PNMImage(texture_size, texture_size) 89 | for x in range(texture_size): 90 | x_band = int(float(x) / float(texture_size) * (texture_bands_x)) 91 | x_band_base = float(x_band) / float(texture_bands_x - 1) 92 | for y in range(texture_size): 93 | y_band = int(float(y) / float(texture_size) * (texture_bands_y)) 94 | y_band_base = float(y_band) / float(texture_bands_y - 1) 95 | ambient_occlusion = 1.0 96 | roughness = x_band_base * 0.18 + 0.12 # 0.12 - 0.3 97 | metallicity = y_band_base * 0.1 + 0.88 # 0.1 - 0.98 98 | metal_rough_pnm.set_xel(x, y, (ambient_occlusion, roughness, metallicity)) 99 | metal_rough_tex = Texture("MetalRoughness") 100 | metal_rough_tex.load(metal_rough_pnm) 101 | ts = TextureStage('MetalRoughness') # a.k.a. Selector 102 | ts.set_mode(TextureStage.M_selector) 103 | card_np.set_texture(ts, metal_rough_tex) 104 | 105 | 106 | # Normals 107 | # RGB is the normalized normal vector (z is perpendicular to 108 | # the surface) * 0.5 + 0.5 109 | normal_pnm = PNMImage(texture_size, texture_size) 110 | for x in range(texture_size): 111 | for y in range(texture_size): 112 | v = Vec3(gauss(0, 0.2), gauss(0, 0.2), 1) 113 | v.normalize() 114 | normal_pnm.set_xel(x, y, v * 0.5 + 0.5) 115 | normal_tex = Texture("Normals") 116 | normal_tex.load(normal_pnm) 117 | ts = TextureStage('Normals') 118 | ts.set_mode(TextureStage.M_normal) 119 | card_np.set_texture(ts, normal_tex) 120 | 121 | 122 | # cm.set_frame(-50, 50, -50, 50) 123 | # map_np = base.render.attach_new_node(cm.generate()) 124 | # map_np.set_p(-90) 125 | # map_np.set_z(-2) 126 | # map_np.set_material(mat) 127 | 128 | 129 | ambient_light = base.render.attach_new_node(AmbientLight('ambient')) 130 | ambient_light.node().set_color((.1, .1, .1, 1)) 131 | base.render.set_light(ambient_light) 132 | 133 | 134 | direct_light = base.render.attach_new_node(DirectionalLight('light')) 135 | direct_light.node().set_color((.2, .2, .2, 1)) 136 | base.render.set_light(direct_light) 137 | direct_light.set_pos(0, -100, 100) 138 | direct_light.look_at(0, 0, 0) 139 | 140 | 141 | def rotate_card(task): 142 | card_np.set_p(-22.5 + sin(task.time / 3.0 * 3) * 5.0) 143 | card_np.set_h(cos(task.time / 3.0) * 5.0) 144 | return task.cont 145 | base.taskMgr.add(rotate_card) 146 | 147 | base.run() 148 | -------------------------------------------------------------------------------- /examples/panda3d-physics/ball.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-physics/ball.bam -------------------------------------------------------------------------------- /examples/panda3d-point-and-click/keybindings.config: -------------------------------------------------------------------------------- 1 | context debug 2 | trigger quit 3 | keyboard escape 4 | trigger console 5 | keyboard f9 6 | trigger frame_rate_meter 7 | keyboard f10 8 | trigger pdb 9 | keyboard f11 10 | trigger pstats 11 | keyboard f12 12 | context interface 13 | trigger take 14 | keyboard mouse1 15 | -------------------------------------------------------------------------------- /examples/panda3d-point-and-click/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from wecs import boilerplate 6 | 7 | 8 | if __name__ == '__main__': 9 | boilerplate.run_game( 10 | keybindings=True, 11 | ) 12 | -------------------------------------------------------------------------------- /examples/panda3d-point-and-click/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-point-and-click/table.png -------------------------------------------------------------------------------- /examples/panda3d-pong/ball.py: -------------------------------------------------------------------------------- 1 | from random import randrange 2 | 3 | from panda3d.core import KeyboardButton 4 | from panda3d.core import Vec3 5 | from panda3d.core import Point3 6 | 7 | from wecs.core import Component 8 | from wecs.core import System 9 | from wecs.core import and_filter 10 | from wecs.core import Proxy 11 | 12 | from movement import Movement 13 | from movement import Players 14 | from paddles import Paddle 15 | 16 | 17 | @Component() 18 | class Ball: 19 | pass 20 | 21 | 22 | @Component() 23 | class Resting: 24 | pass 25 | 26 | 27 | class BallTouchesBoundary(System): 28 | """ 29 | BallTouchesBoundary ensures the balls is bounced of the top and bottom 30 | of the court. 31 | """ 32 | entity_filters = { 33 | 'ball': and_filter([ 34 | Proxy('model'), 35 | Movement, 36 | Ball, 37 | ]), 38 | } 39 | 40 | def update(self, entities_by_filter): 41 | for entity in entities_by_filter['ball']: 42 | model_proxy = self.proxies['model'] 43 | model_node = model_proxy.field(entity) 44 | movement = entity[Movement] 45 | 46 | # The ball's size is assumed to be 0.1, and if it moved over 47 | # the upper or lower boundary (1 / -1), we reflect it. 48 | z = model_node.get_z() 49 | if z > 0.9: 50 | model_node.set_z(0.9 - (z - 0.9)) 51 | movement.direction.z = -movement.direction.z 52 | if z < -0.9: 53 | model_node.set_z(-0.9 - (z + 0.9)) 54 | movement.direction.z = -movement.direction.z 55 | 56 | 57 | class BallTouchesPaddleLine(System): 58 | """ 59 | BallTouchesPaddleLine takes care what happens when the ball 60 | reaches the line of one of the paddles: 61 | Either the paddle is in reach, and the ball reflects off it, 62 | or the other player has scored, and the game is reset to its 63 | starting state. 64 | """ 65 | entity_filters = { 66 | 'ball': and_filter([ 67 | Proxy('model'), 68 | Movement, 69 | Ball, 70 | ]), 71 | 'paddles': and_filter([ 72 | Proxy('model'), 73 | Paddle, 74 | ]), 75 | } 76 | 77 | def update(self, entities_by_filter): 78 | """ 79 | The Update goes over all the relevant Entities (which should be 80 | only the ball) and check whether it reached each paddle's line. 81 | A ball always has a Position component so it has so we can check 82 | it's x position. Same goes for the paddles. 83 | If x is touching the paddle's line, we check whether the ball 84 | hit the paddle or not. 85 | If it hit the paddle it would bounce back. 86 | If it misses the paddle, it would print "SCORE" and stop the 87 | balls movement. 88 | Note that to stop the ball's movement the update deletes the 89 | Movement component from the ball's Entity. It also adds the 90 | Resting component to ensure that the StartBallMotion System will 91 | return the ball to a moving state when the game restarts. 92 | """ 93 | paddles = { 94 | p[Paddle].player: p for p in entities_by_filter['paddles'] 95 | } 96 | 97 | for entity in set(entities_by_filter['ball']): 98 | model_proxy = self.proxies['model'] 99 | ball_node = model_proxy.field(entity) 100 | movement = entity[Movement] 101 | 102 | # Whose line are we behind, if any? 103 | ball_x = ball_node.get_x() 104 | if ball_x < -1: 105 | player = Players.LEFT 106 | elif ball_x > 1: 107 | player = Players.RIGHT 108 | else: 109 | continue 110 | 111 | ball_z = ball_node.get_z() 112 | 113 | paddle = paddles[player] 114 | paddle_node = model_proxy.field(paddle) 115 | paddle_paddle = paddle[Paddle] 116 | 117 | paddle_z = paddle_node.get_z() 118 | paddle_size = paddle_paddle.size 119 | 120 | if abs(paddle_z - ball_z) > paddle_size: 121 | # The paddle is too far away, a point is scored. 122 | print("SCORE!") 123 | del entity[Movement] 124 | entity[Resting] = Resting() 125 | ball_node.set_pos(0, 0, 0) 126 | else: 127 | # Reverse left-right direction 128 | movement.direction.x *= -1 129 | # Adjust up-down speed based on where the paddle was hit 130 | dist_to_center = paddle_z - ball_z 131 | normalized_dist = dist_to_center / paddle_size 132 | speed = abs(movement.direction.x) 133 | movement.direction.z -= normalized_dist * speed 134 | 135 | 136 | class StartBallMotion(System): 137 | """ 138 | StartBallMotion ensures that the game restarts after the start key is pressed. 139 | """ 140 | entity_filters = { 141 | 'ball': and_filter([ 142 | Proxy('model'), 143 | Resting, 144 | Ball, 145 | ]), 146 | } 147 | start_key = KeyboardButton.space() 148 | 149 | def update(self, entities_by_filter): 150 | """ 151 | Check whether the resting ball should be started? 152 | Note that restarting the ball's movement means removing the Entity's 153 | Resting Component, and adding it's Movement component with the desired 154 | direction. 155 | """ 156 | 157 | start_key_is_pressed = base.mouseWatcherNode.is_button_down(StartBallMotion.start_key) 158 | 159 | if start_key_is_pressed: 160 | for entity in set(entities_by_filter['ball']): 161 | del entity[Resting] 162 | entity[Movement] = Movement(direction=Vec3(-0.1*randrange(5, 10), 0, 0)) 163 | -------------------------------------------------------------------------------- /examples/panda3d-pong/game.py: -------------------------------------------------------------------------------- 1 | from panda3d.core import Vec3 2 | from panda3d.core import Point3 3 | 4 | import wecs 5 | from wecs.core import ProxyType 6 | 7 | # These modules contain the actual game mechanics, which we are tying 8 | # together into an application in this file: 9 | 10 | import movement 11 | import paddles 12 | import ball 13 | 14 | 15 | model_proxies = { 16 | 'model': ProxyType(wecs.panda3d.prototype.Model, 'node'), 17 | } 18 | 19 | 20 | system_types = [ 21 | # Attach the entity's Model. This gives an entity a node as 22 | # presence in the scene graph. 23 | # Attach Geometry to the Model's node. 24 | wecs.panda3d.prototype.ManageModels(), 25 | # If the Paddle's size has changed, apply it to the Model. 26 | paddles.ResizePaddles(proxies=model_proxies), 27 | # Read player input and store it on Movement 28 | paddles.GivePaddlesMoveCommands(proxies=model_proxies), 29 | # Apply the Movement 30 | movement.MoveObject(proxies=model_proxies), 31 | # Did the paddle move too far? Back to the boundary with it! 32 | paddles.PaddleTouchesBoundary(), 33 | # If the Ball has hit the edge, it reflects off it. 34 | ball.BallTouchesBoundary(proxies=model_proxies), 35 | # If the ball is on a player's paddle's line, two things can 36 | # happen: 37 | # * The paddle is in reach, and the ball reflects off it. 38 | # * The other player has scored, and the game is reset to its 39 | # starting state. 40 | ball.BallTouchesPaddleLine(proxies=model_proxies), 41 | # If the ball is in its Resting state, and the players indicate 42 | # that the game should start, the ball is set in motion. 43 | ball.StartBallMotion(proxies=model_proxies), 44 | ] 45 | 46 | 47 | # left paddle 48 | base.ecs_world.create_entity( 49 | wecs.panda3d.prototype.Model( 50 | parent=base.aspect2d, 51 | post_attach=wecs.panda3d.prototype.transform( 52 | pos=Vec3(-1.1, 0, 0), 53 | ), 54 | ), 55 | wecs.panda3d.prototype.Geometry(file='resources/paddle.bam'), 56 | movement.Movement(direction=Vec3(0, 0, 0)), 57 | paddles.Paddle(player=paddles.Players.LEFT), 58 | ) 59 | 60 | # right paddle 61 | base.ecs_world.create_entity( 62 | wecs.panda3d.prototype.Model( 63 | parent=base.aspect2d, 64 | post_attach=wecs.panda3d.prototype.transform( 65 | pos=Vec3(1.1, 0, 0), 66 | ), 67 | ), 68 | wecs.panda3d.prototype.Geometry(file='resources/paddle.bam'), 69 | movement.Movement(direction=Vec3(0, 0, 0)), 70 | paddles.Paddle(player=paddles.Players.RIGHT), 71 | ) 72 | 73 | # ball 74 | base.ecs_world.create_entity( 75 | wecs.panda3d.prototype.Model(parent=base.aspect2d), 76 | wecs.panda3d.prototype.Geometry(file='resources/ball.bam'), 77 | ball.Ball(), 78 | ball.Resting(), 79 | ) 80 | -------------------------------------------------------------------------------- /examples/panda3d-pong/main_with_boilerplate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from wecs import boilerplate 6 | 7 | 8 | if __name__ == '__main__': 9 | boilerplate.run_game( 10 | module_name=os.path.dirname(__file__), 11 | keybindings=False, 12 | debug_keys=True 13 | ) 14 | -------------------------------------------------------------------------------- /examples/panda3d-pong/main_without_boilerplate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import logging 5 | 6 | from panda3d.core import Vec3 7 | from panda3d.core import Point3 8 | 9 | import wecs 10 | from wecs.core import ProxyType 11 | from wecs.panda3d import ECSShowBase 12 | 13 | # These modules contain the actual game mechanics, which we are tying 14 | # together into an application in this file: 15 | 16 | import movement 17 | import paddles 18 | import ball 19 | 20 | 21 | logging.getLogger().setLevel(logging.DEBUG) 22 | 23 | 24 | if __name__ == '__main__': 25 | ECSShowBase() # ShowBase + base.ecs_world + base.add_system() 26 | base.disable_mouse() 27 | base.accept('escape', sys.exit) 28 | 29 | model_proxies = { 30 | 'model': ProxyType(wecs.panda3d.prototype.Model, 'node'), 31 | } 32 | systems = [ 33 | # Attach the entity's Model. This gives an entity a node as 34 | # presence in the scene graph. 35 | # Attach Geometry to the Model's node. 36 | wecs.panda3d.prototype.ManageModels(), 37 | # If the Paddle's size has changed, apply it to the Model. 38 | paddles.ResizePaddles(proxies=model_proxies), 39 | # Read player input and store it on Movement 40 | paddles.GivePaddlesMoveCommands(proxies=model_proxies), 41 | # Apply the Movement 42 | movement.MoveObject(proxies=model_proxies), 43 | # Did the paddle move too far? Back to the boundary with it! 44 | paddles.PaddleTouchesBoundary(), 45 | # If the Ball has hit the edge, it reflects off it. 46 | ball.BallTouchesBoundary(proxies=model_proxies), 47 | # If the ball is on a player's paddle's line, two things can 48 | # happen: 49 | # * The paddle is in reach, and the ball reflects off it. 50 | # * The other player has scored, and the game is reset to its 51 | # starting state. 52 | ball.BallTouchesPaddleLine(), 53 | # If the ball is in its Resting state, and the players indicate 54 | # that the game should start, the ball is set in motion. 55 | ball.StartBallMotion(), 56 | ] 57 | 58 | # base.add_system(system) adds the system to the world *and* creates 59 | # a task that will trigger updates. This is how WECS integrates into 60 | # Panda3D's task manager. 61 | for sort, system in enumerate(systems): 62 | base.add_system(system, sort) 63 | 64 | # All systems are set up now, so all that's missing are the 65 | # entities. 66 | 67 | # left paddle 68 | base.ecs_world.create_entity( 69 | wecs.panda3d.prototype.Model( 70 | parent=base.aspect2d, 71 | post_attach=wecs.panda3d.prototype.transform( 72 | pos=Vec3(-1.1, 0, 0), 73 | ), 74 | ), 75 | wecs.panda3d.prototype.Geometry(file='resources/paddle.bam'), 76 | movement.Movement(direction=Vec3(0, 0, 0)), 77 | paddles.Paddle(player=paddles.Players.LEFT), 78 | ) 79 | 80 | # right paddle 81 | base.ecs_world.create_entity( 82 | wecs.panda3d.prototype.Model( 83 | parent=base.aspect2d, 84 | post_attach=wecs.panda3d.prototype.transform( 85 | pos=Vec3(1.1, 0, 0), 86 | ), 87 | ), 88 | wecs.panda3d.prototype.Geometry(file='resources/paddle.bam'), 89 | movement.Movement(direction=Vec3(0, 0, 0)), 90 | paddles.Paddle(player=paddles.Players.RIGHT), 91 | ) 92 | 93 | # ball 94 | base.ecs_world.create_entity( 95 | wecs.panda3d.prototype.Model(parent=base.aspect2d), 96 | wecs.panda3d.prototype.Geometry(file='resources/ball.bam'), 97 | ball.Ball(), 98 | ball.Resting(), 99 | ) 100 | 101 | base.run() 102 | -------------------------------------------------------------------------------- /examples/panda3d-pong/movement.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple movement System and component. 3 | """ 4 | from enum import Enum 5 | 6 | from panda3d.core import Vec3 7 | 8 | import wecs 9 | from wecs.core import Component 10 | from wecs.core import System 11 | from wecs.core import and_filter 12 | from wecs.core import Proxy 13 | from wecs.core import ProxyType 14 | 15 | 16 | class Players(Enum): 17 | LEFT = 0 18 | RIGHT = 1 19 | 20 | 21 | @Component() 22 | class Movement: 23 | """ 24 | The :class:Movement Component holds a 3D vector which represents the direction of the 25 | component which uses it, for example, the model of the ball, or the paddles. 26 | It's the 3D change that should happen during one second,so it serves as a "speed" 27 | element as well. 28 | """ 29 | direction: Vec3 30 | 31 | 32 | class MoveObject(System): 33 | """ 34 | :class:MoveObject update the position of the Entity's :class:Model according to it's 35 | movement direction. 36 | """ 37 | entity_filters = { 38 | 'movable': and_filter([ 39 | Proxy('model'), 40 | Movement, 41 | ]), 42 | } 43 | 44 | def update(self, entities_by_filter): 45 | """ 46 | On update, iterate all 'movable' entities. For each: 47 | - Get its position 48 | - Get its movement(direction) 49 | - Get its model 50 | - finally, update its model according to position and direction 51 | 52 | Note the position is update by the direction multiplied by dt, which is the deltaTime 53 | since the previous update, as the update function is called several times per second. 54 | 55 | :param entities_by_filter: 56 | """ 57 | for entity in entities_by_filter['movable']: 58 | movement = entity[Movement] 59 | model_proxy = self.proxies['model'] 60 | model = entity[model_proxy.component_type] 61 | 62 | movement = movement.direction * globalClock.dt 63 | model.node.set_pos(model.node, movement) 64 | -------------------------------------------------------------------------------- /examples/panda3d-pong/paddles.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Paddle Component and System 3 | 4 | Each Paddle has information about it's player, size and speed. 5 | """ 6 | from panda3d.core import KeyboardButton 7 | 8 | from wecs.core import Component 9 | from wecs.core import System 10 | from wecs.core import and_filter 11 | from wecs.core import Proxy 12 | from wecs.core import ProxyType 13 | 14 | from movement import Movement 15 | from movement import Players 16 | 17 | 18 | @Component() 19 | class Paddle: 20 | """ 21 | The Paddle Component holds: an int representing the player 22 | controlling it, a its speed. 23 | """ 24 | 25 | player: int 26 | size: float = 0.3 27 | 28 | 29 | class ResizePaddles(System): 30 | """ 31 | ResizePaddles ensures that the paddle's size stays updated. 32 | The idea is that other systems may influence the size by changing 33 | the paddle's Component state. ResizePaddles will make the actual 34 | change to the Model. 35 | """ 36 | entity_filters = { 37 | 'paddle': and_filter([ 38 | Proxy('model'), 39 | Paddle, 40 | ]), 41 | } 42 | 43 | def update(self, entities_by_filter): 44 | """ 45 | Update the paddle size by setting the scale of the paddle's 46 | Model. 47 | """ 48 | for entity in entities_by_filter['paddle']: 49 | model_proxy = self.proxies['model'] 50 | paddle = entity[Paddle] 51 | 52 | model_proxy.field(entity).set_scale(paddle.size) 53 | 54 | 55 | class GivePaddlesMoveCommands(System): 56 | entity_filters = { 57 | 'paddle': and_filter([ 58 | Proxy('model'), 59 | Movement, 60 | Paddle, 61 | ]), 62 | } 63 | 64 | def update(self, entities_by_filter): 65 | for entity in entities_by_filter['paddle']: 66 | paddle = entity[Paddle] 67 | movement = entity[Movement] 68 | 69 | # What keys does the player use? 70 | if paddle.player == Players.LEFT: 71 | up_key = KeyboardButton.ascii_key(b'w') 72 | down_key = KeyboardButton.ascii_key(b's') 73 | elif paddle.player == Players.RIGHT: 74 | up_key = KeyboardButton.up() 75 | down_key = KeyboardButton.down() 76 | 77 | # Read player input 78 | delta = 0 79 | if base.mouseWatcherNode.is_button_down(up_key): 80 | delta += 1 81 | if base.mouseWatcherNode.is_button_down(down_key): 82 | delta -= 1 83 | 84 | # Store movement 85 | movement.direction.z = delta 86 | 87 | 88 | class PaddleTouchesBoundary(System): 89 | entity_filters = { 90 | 'paddles': and_filter([ 91 | Proxy('model'), 92 | Paddle, 93 | ]), 94 | } 95 | 96 | def update(self, entities_by_filter): 97 | for entity in set(entities_by_filter['paddles']): 98 | model = entity[Model] 99 | position = entity[Position] 100 | paddle = entity[Paddle] 101 | 102 | z = position.value.z 103 | size = paddle.size 104 | 105 | if z + size > 1: 106 | position.value.z = 1 - size 107 | elif (z - size) < -1: 108 | position.value.z = -1 + size 109 | model.node.set_z(position.value.z) 110 | -------------------------------------------------------------------------------- /examples/panda3d-pong/resources/ball.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-pong/resources/ball.bam -------------------------------------------------------------------------------- /examples/panda3d-pong/resources/ball.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-pong/resources/ball.blend -------------------------------------------------------------------------------- /examples/panda3d-pong/resources/paddle.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-pong/resources/paddle.bam -------------------------------------------------------------------------------- /examples/panda3d-pong/resources/paddle.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/examples/panda3d-pong/resources/paddle.blend -------------------------------------------------------------------------------- /examples/panda3d-twinstick/behaviors.py: -------------------------------------------------------------------------------- 1 | from pychology.behavior_trees import Action 2 | from pychology.behavior_trees import Priorities 3 | from pychology.behavior_trees import Chain 4 | from pychology.behavior_trees import DoneOnPrecondition 5 | from pychology.behavior_trees import FailOnPrecondition 6 | 7 | import wecs 8 | 9 | from wecs.panda3d.behavior_trees import DoneTimer 10 | from wecs.panda3d.behavior_trees import IdleWhenDoneTree 11 | 12 | 13 | def idle(): 14 | return IdleWhenDoneTree( 15 | Chain( 16 | DoneTimer( 17 | wecs.panda3d.behavior_trees.timeout(3.0), 18 | Action(wecs.panda3d.behavior_trees.turn(1.0)), 19 | ), 20 | DoneTimer( 21 | wecs.panda3d.behavior_trees.timeout(3.0), 22 | Action(wecs.panda3d.behavior_trees.turn(-1.0)), 23 | ), 24 | ), 25 | ) 26 | 27 | 28 | def walk_to_entity(): 29 | return IdleWhenDoneTree( 30 | Priorities( 31 | FailOnPrecondition( 32 | wecs.panda3d.behavior_trees.is_pointable, 33 | DoneOnPrecondition( 34 | wecs.panda3d.behavior_trees.distance_smaller(1.5), 35 | Action(wecs.panda3d.behavior_trees.walk_to_entity), 36 | ), 37 | ), 38 | DoneOnPrecondition( 39 | wecs.panda3d.behavior_trees.distance_smaller(0.01), 40 | Action(wecs.panda3d.behavior_trees.walk_to_entity), 41 | ), 42 | ), 43 | ) 44 | -------------------------------------------------------------------------------- /examples/panda3d-twinstick/game.py: -------------------------------------------------------------------------------- 1 | import wecs 2 | 3 | import aspects 4 | 5 | 6 | # Each frame, run these systems. This defines the game itself. 7 | system_types = [ 8 | # Set up newly added models/camera, tear down removed ones 9 | wecs.panda3d.prototype.ManageModels, 10 | wecs.panda3d.camera.PrepareCameras, 11 | # Update clocks 12 | wecs.mechanics.clock.DetermineTimestep, 13 | # Interface interactions 14 | wecs.panda3d.mouseover.MouseOverOnEntity, 15 | wecs.panda3d.mouseover.UpdateMouseOverUI, 16 | # Set inputs to the character controller 17 | wecs.panda3d.ai.Think, 18 | wecs.panda3d.ai.BehaviorInhibitsDirectCharacterControl, 19 | wecs.panda3d.character.UpdateCharacter, 20 | # Fudge the inputs to achieve the kind of control that you want 21 | wecs.panda3d.character.ReorientInputBasedOnCamera, 22 | # Character controller 23 | wecs.panda3d.character.Floating, 24 | wecs.panda3d.character.Walking, 25 | wecs.panda3d.character.Inertiing, 26 | wecs.panda3d.character.Bumping, 27 | wecs.panda3d.character.Falling, 28 | wecs.panda3d.character.Jumping, 29 | wecs.panda3d.character.DirectlyIndicateDirection, 30 | wecs.panda3d.character.TurningBackToCamera, 31 | wecs.panda3d.character.AutomaticallyTurnTowardsDirection, 32 | wecs.panda3d.character.ExecuteMovement, 33 | # Animation 34 | wecs.panda3d.animation.AnimateCharacter, 35 | wecs.panda3d.animation.Animate, 36 | # Camera 37 | wecs.panda3d.camera.ReorientObjectCentricCamera, 38 | wecs.panda3d.camera.ZoomObjectCentricCamera, 39 | wecs.panda3d.camera.CollideCamerasWithTerrain, 40 | # Debug keys (`escape` to close, etc.) 41 | wecs.panda3d.debug.DebugTools, 42 | ] 43 | 44 | 45 | # Now let's create Rebeccas at the spawn points: 46 | 47 | aspects.player_character.add( 48 | base.ecs_world.create_entity(name="Playerbecca"), 49 | overrides={ 50 | **aspects.rebecca, 51 | **aspects.spawn_point_1, 52 | }, 53 | ) 54 | 55 | aspects.non_player_character.add( 56 | base.ecs_world.create_entity(name="Rebecca 1"), 57 | overrides={ 58 | **aspects.rebecca, 59 | **aspects.spawn_point_2, 60 | }, 61 | ) 62 | -------------------------------------------------------------------------------- /examples/panda3d-twinstick/keybindings.config: -------------------------------------------------------------------------------- 1 | context debug 2 | trigger quit 3 | keyboard escape 4 | trigger console 5 | keyboard f9 6 | trigger frame_rate_meter 7 | keyboard f10 8 | trigger pdb 9 | keyboard f11 10 | trigger pstats 11 | keyboard f12 12 | context character_movement 13 | axis2d direction 14 | gamepad left_x:deadzone=0.02 left_y:deadzone=0.02 15 | keyboard a d s w 16 | axis2d rotation 17 | gamepad dpad_left dpad_right dpad_down dpad_up 18 | trigger jump 19 | gamepad face_a 20 | keyboard space 21 | button crouch 22 | gamepad face_y 23 | keyboard c 24 | button sprints 25 | gamepad ltrigger 26 | keyboard e 27 | context camera_movement 28 | axis2d rotation 29 | gamepad right_x:deadzone=0.02 right_y:deadzone=0.02:flip 30 | context character_direction 31 | axis2d direction 32 | gamepad right_x:deadzone=0.02 right_y:deadzone=0.02 33 | keyboard arrow_left arrow_right arrow_down arrow_up 34 | -------------------------------------------------------------------------------- /examples/panda3d-twinstick/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from wecs import boilerplate 6 | 7 | 8 | if __name__ == '__main__': 9 | boilerplate.run_game( 10 | keybindings=True, 11 | ) 12 | -------------------------------------------------------------------------------- /examples/rpg/aging.py: -------------------------------------------------------------------------------- 1 | from wecs.core import Component, System, and_filter 2 | 3 | from lifecycle import Alive 4 | from lifecycle import Health 5 | 6 | 7 | @Component() 8 | class Age: 9 | age: int 10 | age_of_frailty: int 11 | 12 | 13 | class Aging(System): 14 | entity_filters = { 15 | 'has_age': and_filter([Age]), 16 | 'grows_frail': and_filter([Age, Alive, Health]), 17 | } 18 | 19 | def update(self, filtered_entities): 20 | for entity in filtered_entities['has_age']: 21 | entity.get_component(Age).age += 1 22 | for entity in filtered_entities['grows_frail']: 23 | age = entity.get_component(Age).age 24 | age_of_frailty = entity.get_component(Age).age_of_frailty 25 | if age >= age_of_frailty: 26 | entity.get_component(Health).health -= 1 27 | -------------------------------------------------------------------------------- /examples/rpg/character.py: -------------------------------------------------------------------------------- 1 | from wecs.core import Component 2 | 3 | 4 | # Characters and items can have names, producing prettier output. 5 | @Component() 6 | class Name: 7 | name: str 8 | -------------------------------------------------------------------------------- /examples/rpg/dialogue.py: -------------------------------------------------------------------------------- 1 | from wecs.core import Component, UID, System, and_filter, or_filter 2 | 3 | from character import Name 4 | from lifecycle import Dead 5 | 6 | 7 | # Trivial monologue. 8 | @Component() 9 | class TalkAction: 10 | talker: UID 11 | 12 | 13 | @Component() 14 | class Dialogue: 15 | phrase: str 16 | 17 | 18 | class HaveDialogue(System): 19 | entity_filters = { 20 | 'act': and_filter([TalkAction]) 21 | } 22 | 23 | def update(self, filtered_entities): 24 | for entity in filtered_entities['act']: 25 | talker = self.world.get_entity( 26 | entity.get_component(TalkAction).talker, 27 | ) 28 | entity.remove_component(TalkAction) 29 | 30 | if talker.has_component(Dead): 31 | print("Dead people don't talk.") 32 | return False 33 | 34 | # FIXME: Are they in the same room? 35 | 36 | if talker.has_component(Dialogue): 37 | if talker.has_component(Name): 38 | print("> {} says: \"{}\"".format( 39 | talker.get_component(Name).name, 40 | talker.get_component(Dialogue).phrase, 41 | )) 42 | else: 43 | print("> " + talker.get_component(Dialogue).phrase) 44 | else: 45 | print("> \"...\"") 46 | entity.remove_component(TalkAction) 47 | -------------------------------------------------------------------------------- /examples/rpg/lifecycle.py: -------------------------------------------------------------------------------- 1 | from wecs.core import Component, System, and_filter 2 | 3 | 4 | @Component() 5 | class Health: 6 | max_health: int 7 | health: int 8 | 9 | 10 | # Character life states. A character is one of Alive, Dying, Dead, or 11 | # Undead. If a character has none of them, he... it? Well, that 12 | # character is beyond the mortal coil in terms of that mortal coil 13 | # being defined by the life states. Probably it's just simply a thing. 14 | 15 | @Component() 16 | class Alive: 17 | pass 18 | 19 | 20 | @Component() 21 | class Dying: # Transitional state between Alive and... others. 22 | pass 23 | 24 | 25 | @Component() 26 | class Dead: 27 | pass 28 | 29 | 30 | @Component() 31 | class Undead: 32 | pass 33 | 34 | 35 | class DieFromHealthLoss(System): 36 | entity_filters = { 37 | 'is_living': and_filter([Health, Alive]), 38 | } 39 | 40 | def update(self, filtered_entities): 41 | for entity in set(filtered_entities['is_living']): 42 | if entity.get_component(Health).health <= 0: 43 | entity.remove_component(Alive) 44 | entity.add_component(Dying()) 45 | 46 | 47 | class Die(System): 48 | entity_filters = { 49 | 'is_dying': and_filter([Dying]), 50 | } 51 | 52 | def update(self, filtered_entities): 53 | for entity in set(filtered_entities['is_dying']): 54 | entity.remove_component(Dying) 55 | entity.add_component(Dead()) 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | panda3d 2 | panda3d-keybindings 3 | setuptools 4 | sphinx 5 | sphinx-autoapi 6 | sphinx_rtd_theme 7 | crayons 8 | graphviz 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | """ 3 | 4 | from setuptools import setup, find_packages 5 | from os import path 6 | 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | with open("README.md", "r") as fh: 10 | long_description = fh.read() 11 | 12 | 13 | setup( 14 | name='wecs', 15 | version='0.2.0a', 16 | description='An ECS (entity component system)', 17 | long_description=long_description, 18 | long_description_content_type="text/markdown", 19 | url='https://github.com/TheCheapestPixels/wecs', 20 | author='TheCheapestPixels', 21 | author_email='TheCheapestPixels@gmail.com', 22 | classifiers=[ 23 | 'Development Status :: 3 - Alpha', 24 | 'Intended Audience :: Developers', 25 | ], 26 | keywords='ecs panda3d', 27 | packages=find_packages(exclude=['tests', 'examples']), 28 | python_requires='>=3.7, <4', 29 | install_requires=[], 30 | extras_require={ 31 | 'panda3d': ['panda3d'], 32 | 'graphviz': ['graphviz'], 33 | 'bobthewizard': ['crayons'], 34 | }, 35 | ) 36 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wecs.core import Entity, System, Component, World 4 | from wecs.core import and_filter 5 | 6 | 7 | # Absolute basics 8 | 9 | @pytest.fixture 10 | def world(): 11 | return World() 12 | 13 | 14 | @pytest.fixture 15 | def entity(world): 16 | return world.create_entity() 17 | 18 | 19 | # Null stuff 20 | 21 | @Component() 22 | class NullComponent: 23 | pass 24 | 25 | 26 | @pytest.fixture 27 | def null_component(): 28 | return NullComponent() 29 | 30 | 31 | @pytest.fixture 32 | def null_entity(world, null_component): 33 | entity = world.create_entity(null_component) 34 | world._flush_component_updates() 35 | return entity 36 | 37 | 38 | class NullSystem(System): 39 | entity_filters = { 40 | "null": and_filter([NullComponent]) 41 | } 42 | 43 | def __init__(self, *args, **kwargs): 44 | super().__init__(*args, **kwargs) 45 | self.entries = [] 46 | self.exits = [] 47 | self.updates = [] 48 | 49 | def enter_filters(self, filters, entity): 50 | self.entries.append((filters, entity)) 51 | 52 | def exit_filters(self, filters, entity): 53 | self.exits.append((filters, entity)) 54 | 55 | def update(self, entities_by_filter): 56 | self.updates.append(entities_by_filter) 57 | 58 | 59 | class BareNullSystem(NullSystem): 60 | entity_filters = { 61 | "null": NullComponent, 62 | } 63 | 64 | 65 | @pytest.fixture 66 | def null_system(): 67 | return NullSystem() 68 | 69 | 70 | @pytest.fixture 71 | def bare_null_system(): 72 | return BareNullSystem() 73 | 74 | 75 | @pytest.fixture 76 | def null_system_world(world, null_system): 77 | world.add_system(null_system, 0) 78 | return world 79 | 80 | 81 | @pytest.fixture 82 | def bare_null_world(world, bare_null_system): 83 | world.add_system(bare_null_system, 0) 84 | return world 85 | -------------------------------------------------------------------------------- /tests/test_aspects.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wecs.core import Component 4 | from wecs.aspects import Aspect 5 | from wecs.aspects import factory 6 | # from wecs.aspects import MetaAspect 7 | 8 | from fixtures import world 9 | 10 | 11 | @Component() 12 | class Component_A: 13 | i: int = 0 14 | 15 | 16 | @Component() 17 | class Component_B: 18 | i: int = 0 19 | 20 | 21 | @Component() 22 | class Component_C: 23 | pass 24 | 25 | 26 | def test_name(): 27 | aspect = Aspect([], name="foo") 28 | assert repr(aspect) == "foo" 29 | 30 | 31 | def test_no_name(): 32 | aspect = Aspect([]) 33 | assert repr(aspect).startswith("<") 34 | 35 | 36 | def test_aspect_from_components(): 37 | aspect = Aspect([Component_A]) 38 | assert Component_A in aspect 39 | 40 | 41 | def test_aspect_from_aspect(): 42 | aspect_1 = Aspect([Component_A]) 43 | aspect_2 = Aspect([aspect_1]) 44 | assert Component_A in aspect_2 45 | 46 | 47 | def test_aspect_from_multiple_components_same_dict(): 48 | aspect = Aspect([Component_A, Component_B]) 49 | assert Component_A in aspect 50 | assert Component_B in aspect 51 | 52 | 53 | def test_aspect_from_multiple_components_different_dicts(): 54 | aspect = Aspect([Component_A, Component_B]) 55 | assert Component_A in aspect 56 | assert Component_B in aspect 57 | 58 | 59 | def test_aspect_from_multiple_aspects(): 60 | aspect_1 = Aspect([Component_A]) 61 | aspect_2 = Aspect([Component_B]) 62 | aspect_3 = Aspect([aspect_1, aspect_2]) 63 | assert Component_A in aspect_3 64 | assert Component_B in aspect_3 65 | 66 | 67 | def test_aspect_from_multiple_components_different_dicts(): 68 | aspect = Aspect([Component_A, Component_B]) 69 | assert Component_A in aspect 70 | assert Component_B in aspect 71 | 72 | 73 | def test_aspect_from_mixed_args(): 74 | aspect_1 = Aspect([Component_A]) 75 | aspect_2 = Aspect([Component_B, aspect_1]) 76 | assert Component_A in aspect_2 77 | assert Component_B in aspect_2 78 | 79 | 80 | def test_clashing_args(): 81 | with pytest.raises(ValueError): 82 | aspect = Aspect([Component_A, Component_A]) 83 | 84 | def test_clashing_aspects(): 85 | aspect_1 = Aspect([Component_A, Component_B]) 86 | aspect_2 = Aspect([Component_B]) 87 | with pytest.raises(ValueError): 88 | aspect_3 = Aspect([aspect_1, aspect_2]) 89 | 90 | 91 | def test_create_components(): 92 | aspect = Aspect([Component_A]) 93 | [components_a] = aspect() 94 | [components_b] = [Component_A()] 95 | assert components_a.i == components_b.i 96 | 97 | 98 | def test_create_with_overrides_on_aspect(): 99 | aspect = Aspect( 100 | [Component_A], 101 | overrides={ 102 | Component_A: dict(i=1), 103 | }, 104 | ) 105 | [component] = aspect() 106 | assert component.i == 1 107 | 108 | 109 | def test_create_with_overrides_on_creation(): 110 | aspect = Aspect([Component_A]) 111 | [component] = aspect(overrides={Component_A: dict(i=1)}) 112 | assert component.i == 1 113 | 114 | def test_create_with_override_for_missing_component(): 115 | with pytest.raises(ValueError): 116 | Aspect( 117 | [Component_A], 118 | overrides={ 119 | Component_B: dict(i=1), 120 | }, 121 | ) 122 | 123 | 124 | def test_adding_aspect_to_entity(world): 125 | aspect = Aspect([Component_A]) 126 | entity = world.create_entity() 127 | aspect.add(entity) 128 | world._flush_component_updates() 129 | assert Component_A in entity 130 | 131 | 132 | def test_adding_clashing_aspect_to_entity(world): 133 | aspect = Aspect([Component_A]) 134 | entity = world.create_entity(Component_A()) 135 | with pytest.raises(KeyError): 136 | aspect.add(entity) 137 | 138 | 139 | def test_aspect_in_entity(world): 140 | aspect = Aspect([Component_A]) 141 | entity = world.create_entity() 142 | aspect.add(entity) 143 | world._flush_component_updates() 144 | assert aspect.in_entity(entity) 145 | 146 | 147 | def test_aspect_not_in_entity(world): 148 | entity = world.create_entity() 149 | aspect = Aspect([Component_A]) 150 | assert not aspect.in_entity(entity) 151 | 152 | 153 | def test_remove_aspect_from_entity(world): 154 | aspect = Aspect([Component_A]) 155 | entity = world.create_entity(*aspect()) 156 | world._flush_component_updates() 157 | assert aspect.in_entity(entity) 158 | components = aspect.remove(entity) 159 | world._flush_component_updates() 160 | assert not aspect.in_entity(entity) 161 | assert Component_A not in entity 162 | assert len(components) == 1 163 | assert isinstance(components[0], Component_A) 164 | 165 | 166 | def test_remove_aspect_from_entity_that_does_not_have_it(world): 167 | aspect = Aspect([Component_A]) 168 | entity = world.create_entity() 169 | with pytest.raises(ValueError): 170 | aspect.remove(entity) 171 | 172 | 173 | def test_factory_func(): 174 | class Foo: 175 | pass 176 | f = factory(lambda:Foo()) 177 | a = f() 178 | b = f() 179 | assert isinstance(a, Foo) 180 | assert isinstance(b, Foo) 181 | assert a is not b 182 | 183 | 184 | def test_create_aspect_with_factory_function_defaults(world): 185 | class Foo: 186 | pass 187 | aspect = Aspect([Component_A], 188 | overrides={ 189 | Component_A: dict(i=factory(lambda:1)), 190 | }, 191 | ) 192 | [a] = aspect() 193 | assert a.i == 1 194 | -------------------------------------------------------------------------------- /tests/test_clock.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wecs.mechanics import Clock 4 | from wecs.mechanics import SettableClock 5 | from wecs.mechanics import DetermineTimestep 6 | 7 | from fixtures import world, entity 8 | 9 | 10 | def test_basic_clock(world): 11 | world.add_system(DetermineTimestep(), sort=0) 12 | dt = 0.01 13 | clock = SettableClock(dt) 14 | entity = world.create_entity(Clock(clock=clock)) 15 | world._flush_component_updates() 16 | 17 | assert dt < entity[Clock].max_timestep 18 | 19 | world.update() 20 | assert entity[Clock].timestep == dt 21 | 22 | 23 | @pytest.fixture 24 | def clock(world, entity): 25 | dt = 0.01 26 | clock = SettableClock(dt) 27 | entity[Clock] = Clock(clock=clock) 28 | world._flush_component_updates() 29 | assert dt < entity[Clock].max_timestep 30 | return clock 31 | 32 | 33 | def test_clock_max_timestep(world, entity, clock): 34 | world.add_system(DetermineTimestep(), sort=0) 35 | dt = 0.1 36 | assert dt > entity[Clock].max_timestep 37 | clock.set(dt) 38 | 39 | world.update() 40 | assert entity[Clock].timestep == entity[Clock].max_timestep 41 | 42 | 43 | def test_clock_scaling(world, entity, clock): 44 | world.add_system(DetermineTimestep(), sort=0) 45 | dt = 0.01 46 | factor = 0.5 47 | clock.set(dt) 48 | entity[Clock].scaling_factor = factor 49 | 50 | world.update() 51 | assert entity[Clock].game_time == dt * factor 52 | 53 | 54 | def test_clock_cascade(world, entity, clock): 55 | world.add_system(DetermineTimestep(), sort=0) 56 | dt = 0.01 57 | clock.set(dt) 58 | 59 | # Child clock 60 | factor = 0.5 61 | child = world.create_entity( 62 | Clock( 63 | parent=entity._uid, 64 | scaling_factor=factor, 65 | ), 66 | ) 67 | 68 | world.update() 69 | assert child[Clock].frame_time == dt 70 | assert child[Clock].game_time == dt * factor 71 | -------------------------------------------------------------------------------- /tests/test_core/test_ecs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wecs.core import Entity, System, Component, World 4 | from wecs.core import and_filter 5 | 6 | from fixtures import world 7 | from fixtures import entity 8 | from fixtures import NullComponent 9 | from fixtures import null_component 10 | from fixtures import NullSystem 11 | from fixtures import null_system 12 | from fixtures import null_system_world 13 | from fixtures import null_entity 14 | 15 | 16 | def test_create_world(): 17 | world = World() 18 | 19 | 20 | def test_create_entity(world): 21 | entity = world.create_entity() 22 | 23 | 24 | def test_add_component(world, entity): 25 | component = NullComponent() 26 | entity[NullComponent] = component 27 | assert entity in world._addition_pool 28 | with pytest.raises(KeyError): 29 | entity[NullComponent] 30 | 31 | world._flush_component_updates() 32 | assert entity not in world._addition_pool 33 | assert entity[NullComponent] is component 34 | 35 | 36 | def test_remove_component(world, entity): 37 | component = NullComponent() 38 | entity[NullComponent] = component 39 | world._flush_component_updates() 40 | 41 | del entity[NullComponent] 42 | assert entity in world._removal_pool 43 | assert entity[NullComponent] is component 44 | 45 | world._flush_component_updates() 46 | assert entity not in world._removal_pool 47 | with pytest.raises(KeyError): 48 | entity[NullComponent] 49 | 50 | 51 | def test_addition_to_system__system_first(world, null_system): 52 | world.add_system(null_system, 0) 53 | entity = world.create_entity(NullComponent()) 54 | world._flush_component_updates() 55 | assert entity in null_system.entities["null"] 56 | assert len(null_system.entries) == 1 57 | assert null_system.entries[0] == (['null'], entity) 58 | 59 | 60 | def test_addition_to_system__entity_first(world, null_system): 61 | entity = world.create_entity(NullComponent()) 62 | world._flush_component_updates() 63 | world.add_system(null_system, 0) 64 | assert entity in null_system.entities['null'] 65 | assert len(null_system.entries) == 1 66 | assert null_system.entries[0] == (['null'], entity) 67 | 68 | 69 | def test_entity_dropped_from_system_filter(null_system_world, null_system, null_entity): 70 | # Preconditions 71 | assert null_entity in null_system.entities['null'] 72 | assert len(null_system.entries) == 1 73 | assert null_system.entries[0] == (['null'], null_entity) 74 | 75 | # Change 76 | del null_entity[NullComponent] 77 | null_system_world._flush_component_updates() 78 | 79 | # Postconditions 80 | assert null_system.entities['null'] == set() 81 | assert len(null_system.exits) == 1 82 | assert null_system.exits[0] == (['null'], null_entity) 83 | 84 | 85 | def test_remove_system(world, null_system): 86 | entity = world.create_entity(NullComponent()) 87 | world._flush_component_updates() 88 | world.add_system(null_system, 0) 89 | 90 | world.remove_system(NullSystem) 91 | assert null_system.entities['null'] == set() 92 | assert len(null_system.exits) == 1 93 | assert null_system.entries[0] == (['null'], entity) 94 | assert world.systems == {} 95 | assert not world.has_system(NullSystem) 96 | 97 | 98 | def test_system_update(null_system_world, null_system, null_entity): 99 | null_system_world.update() 100 | assert len(null_system.updates) == 1 101 | assert null_system.updates[0] == {'null': set([null_entity])} 102 | 103 | 104 | def test_system_filters_from_bare_component(world): 105 | class BareSystem(System): 106 | entity_filters = { 107 | 'bare': NullComponent, 108 | } 109 | 110 | # Edge Cases 111 | 112 | def test_can_not_get_nonexistent_component(entity): 113 | with pytest.raises(KeyError): 114 | entity[NullComponent] 115 | 116 | 117 | def test_can_not_remove_nonexistent_component(entity): 118 | with pytest.raises(KeyError): 119 | del entity[NullComponent] 120 | 121 | 122 | def test_can_not_add_component_type_multiple_times(entity, null_component): 123 | entity.add_component(null_component) 124 | with pytest.raises(KeyError): 125 | entity.add_component(null_component) 126 | 127 | 128 | def test_can_not_get_nonexistent_system(world): 129 | with pytest.raises(KeyError): 130 | world.get_system(NullSystem) 131 | 132 | 133 | def test_can_not_remove_nonexistent_system(world): 134 | with pytest.raises(KeyError): 135 | world.remove_system(NullSystem) 136 | 137 | 138 | def test_can_not_add_system_type_multiple_times(world): 139 | world.add_system(NullSystem, 0) 140 | with pytest.raises(KeyError): 141 | world.add_system(NullSystem, 1) 142 | -------------------------------------------------------------------------------- /tests/test_core/test_entity_dunders.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fixtures import world 4 | from fixtures import entity 5 | from fixtures import null_component 6 | from fixtures import null_entity 7 | from fixtures import NullComponent 8 | 9 | 10 | def test_set(world, entity, null_component): 11 | entity[NullComponent] = null_component 12 | world._flush_component_updates() 13 | 14 | 15 | def test_get(world, entity, null_component): 16 | entity.add_component(null_component) 17 | world._flush_component_updates() 18 | assert entity[NullComponent] is null_component 19 | assert entity.get(NullComponent) is null_component 20 | assert entity.get("missing") is None 21 | assert entity.get("missing", "default_value") is "default_value" 22 | 23 | 24 | def test_contains(world, entity, null_component): 25 | entity.add_component(null_component) 26 | assert NullComponent not in entity 27 | 28 | world._flush_component_updates() 29 | assert NullComponent in entity 30 | 31 | 32 | def test_del(world, null_entity, null_component): 33 | del null_entity[NullComponent] 34 | assert NullComponent in null_entity 35 | assert null_entity[NullComponent] is null_component 36 | 37 | world._flush_component_updates() 38 | assert NullComponent not in null_entity 39 | -------------------------------------------------------------------------------- /tests/test_core/test_reference.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fixtures import world 4 | 5 | from wecs.core import UID 6 | from wecs.core import NoSuchUID 7 | from wecs.core import Component 8 | 9 | 10 | @Component() 11 | class Reference: 12 | uid: UID 13 | 14 | 15 | def test_user_defined_names(world): 16 | entity = world.create_entity(name="foo") 17 | assert entity._uid.name == "foo" 18 | 19 | 20 | def test_automatic_names(world): 21 | entity = world.create_entity() 22 | assert entity._uid.name 23 | 24 | 25 | def test_automatic_unique_names(world): 26 | entity_1 = world.create_entity() 27 | entity_2 = world.create_entity() 28 | assert entity_1._uid.name != entity_2._uid.name 29 | 30 | 31 | # This test feels silly... More on it when serialization comes knocking. 32 | def test_uid(): 33 | uid_1 = UID() 34 | uid_2 = UID() 35 | assert uid_1 is not uid_2 36 | assert uid_1 != uid_2 37 | 38 | 39 | def test_reference(): 40 | c = Reference(uid=UID()) 41 | 42 | 43 | def test_resolving_reference(world): 44 | to_entity = world.create_entity() 45 | from_entity = world.create_entity() 46 | from_entity.add_component(Reference(uid=to_entity._uid)) 47 | world._flush_component_updates() 48 | reference = world.get_entity(from_entity.get_component(Reference).uid) 49 | assert reference is to_entity 50 | 51 | 52 | def test_resolving_dangling_reference(world): 53 | to_entity = world.create_entity() 54 | from_entity = world.create_entity() 55 | from_entity.add_component(Reference(uid=to_entity._uid)) 56 | world.destroy_entity(to_entity) 57 | world._flush_component_updates() 58 | with pytest.raises(NoSuchUID): 59 | world.get_entity(from_entity.get_component(Reference).uid) 60 | -------------------------------------------------------------------------------- /tests/test_panda3d/test_ai.py: -------------------------------------------------------------------------------- 1 | from wecs.panda3d.ai import BehaviorTree 2 | 3 | 4 | def test_complex_behavior(): 5 | tree = BehaviorTree( 6 | Priority( 7 | Sequence 8 | ), 9 | ) 10 | -------------------------------------------------------------------------------- /tests/test_panda3d/test_behavior_trees.py: -------------------------------------------------------------------------------- 1 | from wecs.panda3d.behavior_trees import BehaviorTree as BehaviorTreeBase 2 | from wecs.panda3d.behavior_trees import ACTIVE 3 | from wecs.panda3d.behavior_trees import DONE 4 | from wecs.panda3d.behavior_trees import Action 5 | from wecs.panda3d.behavior_trees import Priorities 6 | from wecs.panda3d.behavior_trees import DoneOnPostcondition 7 | from wecs.panda3d.behavior_trees import FailOnPrecondition 8 | 9 | 10 | class BehaviorTree(BehaviorTreeBase): 11 | def done(self): 12 | return DONE 13 | 14 | 15 | def set_foo_to_true(entity): 16 | entity['foo'] = True 17 | return DONE 18 | 19 | 20 | def increment_foo(entity): 21 | entity['foo'] += 1 22 | return ACTIVE 23 | 24 | 25 | def foo_is_five(entity): 26 | return entity['foo'] == 5 27 | 28 | 29 | def test_tree_1(): 30 | blackboard = dict(foo=False) 31 | 32 | tree = BehaviorTree(Action(set_foo_to_true)) 33 | 34 | assert tree(blackboard) is DONE 35 | assert blackboard['foo'] 36 | 37 | 38 | def test_tree_2(): 39 | blackboard = dict(foo=0) 40 | 41 | tree = BehaviorTree( 42 | DoneOnPostcondition( 43 | foo_is_five, 44 | Action(increment_foo), 45 | ), 46 | ) 47 | 48 | assert tree(blackboard) is ACTIVE 49 | assert tree(blackboard) is ACTIVE 50 | assert tree(blackboard) is ACTIVE 51 | assert tree(blackboard) is ACTIVE 52 | assert tree(blackboard) is DONE 53 | assert blackboard['foo'] == 5 54 | 55 | 56 | def test_tree_3(): 57 | blackboard = dict(foo=0) 58 | 59 | tree = BehaviorTree( 60 | Priorities( 61 | FailOnPrecondition( 62 | foo_is_five, 63 | Action(increment_foo), 64 | ), 65 | Action(set_foo_to_true), 66 | ), 67 | ) 68 | 69 | assert tree(blackboard) is ACTIVE 70 | assert tree(blackboard) is ACTIVE 71 | assert tree(blackboard) is ACTIVE 72 | assert tree(blackboard) is ACTIVE 73 | assert tree(blackboard) is ACTIVE 74 | assert tree(blackboard) is DONE 75 | assert blackboard['foo'] is True 76 | -------------------------------------------------------------------------------- /tests/test_panda3d/test_core.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from panda3d.core import load_prc_file_data 4 | 5 | from wecs.core import World 6 | from wecs.core import System 7 | from wecs.panda3d.core import ECSShowBase 8 | 9 | 10 | load_prc_file_data("", "window-type none"); 11 | 12 | 13 | def test_panda3d_setup(): 14 | ECSShowBase() 15 | base.destroy() 16 | 17 | 18 | @pytest.fixture 19 | def ecs_base(): 20 | ECSShowBase() 21 | yield 22 | assert isinstance(base.ecs_world, World) 23 | base.destroy() 24 | 25 | 26 | def test_adding_system(ecs_base): 27 | class DemoSystem(System): 28 | entity_filters = {} 29 | task = base.add_system(DemoSystem(), 23) 30 | assert task.sort == 23 31 | assert task.priority == 0 32 | assert task in base.task_mgr.getAllTasks() 33 | -------------------------------------------------------------------------------- /tests/test_panda3d/test_model.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wecs.core import World 4 | from wecs.panda3d.core import ECSShowBase 5 | from wecs.panda3d.model import Model 6 | from wecs.panda3d.model import Geometry 7 | 8 | from wecs.panda3d.model import SetupModels 9 | from wecs.panda3d.model import UpdateSprites 10 | from wecs.panda3d.model import ManageGeometry 11 | from wecs.panda3d.model import Sprite 12 | from wecs.mechanics.clock import Clock 13 | 14 | from panda3d.core import NodePath 15 | 16 | 17 | @pytest.fixture 18 | def ecs_base(): 19 | ECSShowBase() 20 | base.ecs_world.add_system(SetupModels(), 0) 21 | base.ecs_world.add_system(UpdateSprites(), 1) 22 | base.ecs_world.add_system(ManageGeometry(), 3) 23 | yield 24 | assert isinstance(base.ecs_world, World) 25 | base.destroy() 26 | 27 | 28 | @pytest.fixture 29 | def entity(ecs_base): 30 | entity = base.ecs_world.create_entity( 31 | Clock(), 32 | Model(), 33 | Geometry(), 34 | Sprite(), 35 | ) 36 | return entity 37 | 38 | 39 | def test_manage_geometry(entity): 40 | base.ecs_world.update() 41 | assert entity[Geometry].node.get_parent() == entity[Model].node 42 | assert entity[Sprite].node.get_parent() == entity[Geometry].node 43 | entity[Geometry].nodes.remove(entity[Sprite].node) 44 | base.ecs_world.update() 45 | assert entity[Sprite].node.get_parent() != entity[Geometry].node 46 | assert entity[Sprite].node.has_parent() == False 47 | -------------------------------------------------------------------------------- /wecs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCheapestPixels/wecs/d60deb4a4cf8a244012b50a37e64ec5b37eab8bb/wecs/__init__.py -------------------------------------------------------------------------------- /wecs/aspects.py: -------------------------------------------------------------------------------- 1 | """Aspects""" 2 | 3 | 4 | def factory(factory_function): 5 | def func(): 6 | return factory_function() 7 | 8 | return func 9 | 10 | 11 | class Aspect: 12 | """ 13 | An aspect is a set of 14 | :class:`wecs.core.Component` types (and values diverging from the 15 | defaults) and parent aspects. When you create an entity from a set 16 | of aspects, all component types get pooled. 17 | """ 18 | 19 | def __init__(self, aspects_or_components, overrides=None, name=None): 20 | self.name = name 21 | self.components = {} 22 | for aoc in aspects_or_components: 23 | if isinstance(aoc, Aspect): 24 | if any(key in aoc.components for key in self.components.keys()): 25 | raise ValueError("Aspect {} has clashing components".format(aoc)) 26 | self.components.update(aoc.components) 27 | else: 28 | if aoc in self.components: 29 | raise ValueError("Component {} is already present in Aspect".format(aoc)) 30 | self.components[aoc] = {} 31 | if overrides is not None: 32 | if not all(key in self.components for key in overrides.keys()): 33 | raise ValueError("Not all override keys in aspect.") 34 | self.components.update(overrides) 35 | 36 | def in_entity(self, entity): 37 | return all([component_type in entity for component_type in self.components]) 38 | 39 | def add(self, entity, overrides=None): 40 | if any(component_type in entity for component_type in self.components): 41 | raise ValueError("Clashing components with entity.") 42 | for component in self(overrides=overrides): 43 | entity.add_component(component) 44 | 45 | def remove(self, entity): 46 | if not self.in_entity(entity): 47 | raise ValueError("Aspect not in entity.") 48 | components = [] 49 | if self.in_entity(entity): 50 | for component_type in self.components: 51 | components.append(entity[component_type]) 52 | del entity[component_type] 53 | return components 54 | 55 | def __call__(self, overrides=None): 56 | components = [] 57 | if overrides is None: 58 | overrides = {} 59 | for component_type, defaults in self.components.items(): 60 | # Shallow copy, since we will replace values through 61 | # overrides and factories 62 | arguments = self.components[component_type].copy() 63 | # Overrides 64 | if component_type in overrides: 65 | arguments.update(overrides[component_type]) 66 | # Call factories 67 | for argument in arguments.keys(): 68 | if callable(arguments[argument]): 69 | arguments[argument] = arguments[argument]() 70 | # Create the component 71 | component = component_type(**arguments) 72 | components.append(component) 73 | return components 74 | 75 | def __contains__(self, component_type): 76 | return component_type in self.components 77 | 78 | def __repr__(self): 79 | if self.name is not None: 80 | return self.name 81 | else: 82 | return super().__repr__() 83 | -------------------------------------------------------------------------------- /wecs/boilerplate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Boilerplate code to run a WECS-based game. A game's typical ``main.py`` 3 | looks like this: 4 | 5 | .. code-block:: python 6 | 7 | #!/usr/bin/env python 8 | 9 | import os 10 | 11 | from wecs import boilerplate 12 | 13 | 14 | if __name__ == '__main__': 15 | boilerplate.run_game( 16 | keybindings=True, 17 | ) 18 | 19 | To write your game, implement the module ``game`` (either in a 20 | ``game.py`` or ``game/__init__.py``), 21 | 22 | """ 23 | 24 | import sys 25 | import importlib 26 | 27 | from panda3d.core import PStatClient 28 | from panda3d.core import loadPrcFileData 29 | 30 | # We want the time of collision traversal to be added to systems that 31 | # run them. 32 | loadPrcFileData('', 'pstats-active-app-collisions-ctrav false') 33 | 34 | from wecs.core import System 35 | from wecs.panda3d import ECSShowBase 36 | 37 | 38 | def run_game(game_module='game', simplepbr=False, 39 | simplepbr_kwargs=None, keybindings=False, 40 | debug_keys=False, 41 | config=None, config_file=None, debug=None, assigner=None): 42 | """ 43 | This function... 44 | 45 | - starts a Panda3D instance, 46 | - sets it up for use with WECS, 47 | - imports the module ``game`` (or whatever name is passed as 48 | ``game_module``), 49 | - adds systems of types specified in ``game.system_types`` (if 50 | present) to ``base.ecs_world``, 51 | - runs Panda3D's main loop. 52 | 53 | 54 | :param game_module: The name of the game module 55 | :param simplepbr: Initialize ``panda3d-simplepbr``. 56 | :param simplepbr_kwargs: key word argument to pass to 57 | ``simplepbr.init()`` (if :param simplepbr: is True.) 58 | :param keybindings: Set up ``panda3d-keybindings`` listener. 59 | :param debug_keys: The boilerplate will use Panda3D's key press 60 | events to make four functions available: 61 | 62 | - ``Escape``: Close the application by calling ``sys.exit()``. 63 | - ``F10``: show / hide the frame rate meter. 64 | - ``F11``: Start a ``pdb`` session in the underlying terminal. 65 | - ``F12``: Connect to ``pstats``. 66 | """ 67 | # Application Basics 68 | ECSShowBase() 69 | sky_color = (0.3, 0.5, 0.95, 1) 70 | base.win.setClearColor(sky_color) 71 | base.disable_mouse() 72 | 73 | if keybindings: 74 | from keybindings.device_listener import add_device_listener 75 | from keybindings.device_listener import SinglePlayerAssigner 76 | dev_lis_args = {} 77 | if config is not None: 78 | dev_lis_args.update(dict(config=config)) 79 | if config_file is not None: 80 | dev_lis_args.update(dict(config_file=config_file)) 81 | if debug is not None: 82 | dev_lis_args.update(dict(debug=debug)) 83 | if assigner is not None: 84 | dev_lis_args.update(dict(assigner=assigner)) 85 | else: 86 | dev_lis_args.update(dict(assigner=SinglePlayerAssigner())) 87 | add_device_listener(**dev_lis_args) 88 | 89 | if simplepbr is True: 90 | import simplepbr 91 | if simplepbr_kwargs is None: 92 | simplepbr_kwargs = {} # i.e. dict(max_lights=1) 93 | simplepbr.init(**simplepbr_kwargs) 94 | 95 | if debug_keys: 96 | base.accept('escape', sys.exit) 97 | base.frame_rate_meter_visible = False 98 | base.set_frame_rate_meter(base.frame_rate_meter_visible) 99 | 100 | def toggle_frame_rate_meter(): 101 | base.frame_rate_meter_visible = not base.frame_rate_meter_visible 102 | base.set_frame_rate_meter(base.frame_rate_meter_visible) 103 | 104 | base.accept('f10', toggle_frame_rate_meter) 105 | 106 | def debug(): 107 | import pdb 108 | pdb.set_trace() 109 | 110 | base.accept('f11', debug) 111 | 112 | def pstats(): 113 | base.pstats = True 114 | PStatClient.connect() 115 | 116 | base.accept('f12', pstats) 117 | 118 | # Set up the world: 119 | game = importlib.import_module(game_module) 120 | # FIXME: system_types is a bad name, since the allowed specs are now 121 | # more complicated (see add_systems' code). system_specs would be 122 | # better. 123 | if hasattr(game, 'system_types'): 124 | add_systems(game.system_types) 125 | 126 | # And here we go... 127 | base.run() 128 | 129 | 130 | def add_systems(system_specs): 131 | """ 132 | Registers additional systems to the world. Each system specification 133 | must be in one of these formats: 134 | 135 | .. code-block:: python 136 | 137 | system_types = [ 138 | SystemType, 139 | system_instance, 140 | (sort, priority, SystemType), 141 | (sort, priority, system_instance), 142 | ] 143 | 144 | Each ``SystemType`` is instantiated with no arguments. 145 | 146 | ``sort`` and ``priority`` refer to the same parameter's in 147 | Panda3D's task manager. If they are not provided, a best effort 148 | guess is made: Starting at sort 1 and priority 0, priority is 149 | counted down. If the values are provided, then the next system 150 | for which they are *not* specified will continue counting down 151 | from the last provided values. 152 | 153 | Registered systems will be activated in every update. 154 | 155 | :param system_specs: An iterable containing system specifications. 156 | """ 157 | sort, priority = 1, 1 158 | 159 | for spec in system_specs: 160 | # Figure out the full_specs 161 | if isinstance(spec, System): 162 | system = spec 163 | priority -= 1 164 | elif issubclass(spec, System): 165 | system = spec() 166 | priority -= 1 167 | else: 168 | sort, priority, system_spec = spec 169 | if isinstance(system_spec, System): 170 | system = system_spec 171 | elif issubclass(system_spec, System): 172 | system = system_spec() 173 | else: 174 | raise ValueError 175 | 176 | base.add_system(system, sort, priority=priority) 177 | -------------------------------------------------------------------------------- /wecs/equipment.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from dataclasses import field 3 | 4 | from wecs.core import Component 5 | from wecs.core import System 6 | from wecs.core import UID 7 | from wecs.core import and_filter 8 | from wecs.rooms import Room 9 | from wecs.rooms import RoomPresence 10 | from wecs.rooms import is_in_room 11 | from wecs.inventory import Inventory 12 | from wecs.inventory import is_in_inventory 13 | 14 | 15 | @Component() 16 | class Equipment: 17 | # Contains entities with Slot component 18 | slots: List[UID] = field(default_factory=list) 19 | 20 | 21 | @Component() 22 | class Slot: 23 | # An entity must have a component of this type to be equippable 24 | # in this slot 25 | type: type 26 | content: UID # The actual item 27 | 28 | 29 | @Component() 30 | class Equippable: 31 | # Identifies the kind of slot that this item can be equipped in 32 | type: type 33 | 34 | 35 | @Component() 36 | class EquipAction: 37 | item: UID 38 | slot: UID 39 | 40 | 41 | @Component() 42 | class UnequipAction: 43 | slot: UID 44 | target: UID 45 | 46 | 47 | def is_equippable_in_slot(item, slot, entity): 48 | # If the item is equippable... 49 | if not item.has_component(Equippable): 50 | return False 51 | 52 | # ...and the item can be picked up... 53 | if not is_in_inventory(item, entity) and not is_in_room(item, entity): 54 | return False 55 | 56 | # ...and the avatar has this slot in his equipment... 57 | # FIXME: Implement 58 | slot_cmpt = slot.get_component(Slot) 59 | 60 | # ...and the slot is empty... 61 | if slot_cmpt.content is not None: 62 | return False 63 | 64 | # ...of corresponding type,... 65 | slot_type = slot_cmpt.type 66 | item_type = item.get_component(Equippable).type 67 | if slot_type is not item_type: 68 | return False 69 | 70 | # ...then the item is equippable. 71 | return True 72 | 73 | 74 | def can_equip(item, slot, entity): 75 | if not is_equippable_in_slot(item, slot, entity): 76 | return False 77 | 78 | if not is_in_room(item, entity) and not is_in_inventory(item, entity): 79 | return False 80 | 81 | return True 82 | 83 | 84 | def can_unequip(slot, entity): 85 | has_inventory = entity.has_component(Inventory) 86 | is_in_room = entity.has_component(RoomPresence) 87 | if not has_inventory and not is_in_room: 88 | return False 89 | return True 90 | 91 | 92 | def equip(item, slot, entity): 93 | # FIXME!!! Just run the check once all tests have exc=True 94 | if not is_equippable_in_slot(item, slot, entity): 95 | return 96 | slot_cmpt = slot.get_component(Slot) 97 | 98 | if is_in_room(item, entity): 99 | item.remove_component(RoomPresence) 100 | slot_cmpt.content = item._uid 101 | elif is_in_inventory(item, entity): 102 | inventory = entity.get_component(Inventory) 103 | del inventory.contents[inventory.contents.index(item._uid)] 104 | slot_cmpt.content = item._uid 105 | 106 | 107 | # FIXME: No world arg once getting UID fields produces entities 108 | def unequip(slot, target, entity, world): 109 | slot_cmpt = slot.get_component(Slot) 110 | item_uid = slot_cmpt.content 111 | 112 | if target.has_component(Room): 113 | slot_cmpt.content = None 114 | item = world.get_entity(item_uid) 115 | item.add_component(RoomPresence(room=target._uid)) 116 | elif target.has_component(Inventory): 117 | inventory = target.get_component(Inventory) 118 | slot_cmpt.content = None 119 | inventory.contents.append(item_uid) 120 | else: 121 | print("Unequipping failed.") 122 | 123 | 124 | class EquipOrUnequip(System): 125 | entity_filters = { 126 | 'equip': and_filter([EquipAction]), 127 | 'unequip': and_filter([UnequipAction]), 128 | } 129 | 130 | def update(self, entities_by_filter): 131 | for entity in entities_by_filter['unequip']: 132 | action = entity.get_component(UnequipAction) 133 | entity.remove_component(UnequipAction) 134 | 135 | slot = self.world.get_entity(action.slot) 136 | target = self.world.get_entity(action.target) 137 | unequip(slot, target, entity, self.world) 138 | 139 | for entity in entities_by_filter['equip']: 140 | action = entity.get_component(EquipAction) 141 | entity.remove_component(EquipAction) 142 | 143 | item = self.world.get_entity(action.item) 144 | slot = self.world.get_entity(action.slot) 145 | equip(item, slot, entity) 146 | -------------------------------------------------------------------------------- /wecs/graphviz.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from graphviz import Graph 4 | 5 | 6 | # Examples of use from Bob the Wizard: 7 | # 8 | # from wecs.graphviz import system_component_dependency 9 | # system_component_dependency( 10 | # world, 11 | # omit_systems=[ 12 | # systems.PrintOutput, 13 | # systems.ReadInput, 14 | # ], 15 | # ) 16 | # 17 | # system_component_dependency( 18 | # world, 19 | # systems_groups={ 20 | # 'magic': [ 21 | # systems.BecomeLich, 22 | # systems.RegenerateMana, 23 | # systems.ReadySpells, 24 | # systems.CastRejuvenationSpell, 25 | # systems.CastRestoreHealthSpell, 26 | # systems.CastLichdomSpell, 27 | # ], 28 | # 'io': [ 29 | # systems.PrintOutput, 30 | # systems.ReadInput, 31 | # ], 32 | # 'lifecycle': [ 33 | # systems.Aging, 34 | # systems.DieFromHealthLoss, 35 | # systems.Die, 36 | # ], 37 | # } 38 | # ) 39 | 40 | 41 | def random_color(): 42 | hue = random.random() 43 | saturation = 1 44 | value = 1 45 | return "{:1.3f} {:1.3f} {:1.3f}".format(hue, saturation, value) 46 | 47 | 48 | def system_component_dependency(world, filename=None, 49 | omit_systems=None, systems_groups=None): 50 | assert omit_systems is None or systems_groups is None 51 | if systems_groups is not None: 52 | assert filename is None 53 | if filename is None and systems_groups is None: 54 | filename = "system_component_dependencies" 55 | if omit_systems is None: 56 | omit_systems = [] 57 | 58 | dependency_graph = world.get_system_component_dependencies() 59 | if systems_groups is not None: 60 | for filename, system_types in systems_groups.items(): 61 | systems = [s for s in dependency_graph.keys() 62 | if type(s) in system_types] 63 | draw_graph(filename, world, systems) 64 | else: 65 | systems = [s for s in dependency_graph.keys() 66 | if type(s) not in omit_systems] 67 | if filename is None: 68 | filename = "system_component_dependencies.gv" 69 | else: 70 | filename += '.gv' 71 | print(filename) 72 | draw_graph(filename, world, systems) 73 | 74 | 75 | def draw_graph(filename, world, systems): 76 | dependency_graph = world.get_system_component_dependencies() 77 | components = set() 78 | for s in dependency_graph.values(): 79 | components.update(s) 80 | 81 | # Assign colors for systems / components 82 | system_colors = {s: random_color() for s in systems} 83 | component_colors = {c: random_color() for c in components} 84 | 85 | # Create the graph 86 | dot = Graph( 87 | comment="System Component Dependencies", 88 | graph_attr=dict( 89 | rankdir='LR', 90 | ranksep='5', 91 | ), 92 | node_attr=dict( 93 | group='A', 94 | ), 95 | ) 96 | 97 | for system in systems: 98 | dot.node( 99 | repr(system), 100 | # color=system_colors[system], 101 | ) 102 | for component in components: 103 | dot.node( 104 | repr(component), 105 | color=component_colors[component], 106 | ) 107 | for system in systems: 108 | for component in dependency_graph[system]: 109 | dot.edge( 110 | repr(system), 111 | repr(component), 112 | color=component_colors[component], 113 | ) 114 | 115 | # Render the graph 116 | dot.render(filename=filename, format='png') 117 | -------------------------------------------------------------------------------- /wecs/inventory.py: -------------------------------------------------------------------------------- 1 | from dataclasses import field 2 | 3 | from wecs.core import Component, System, UID, NoSuchUID, and_filter 4 | from wecs.rooms import RoomPresence 5 | 6 | 7 | @Component() 8 | class Inventory: 9 | contents: list = field(default_factory=list) 10 | 11 | 12 | @Component() 13 | class Takeable: 14 | pass 15 | 16 | 17 | @Component() 18 | class TakeAction: 19 | item: UID 20 | 21 | 22 | @Component() 23 | class DropAction: 24 | item: UID 25 | 26 | 27 | class ItemNotInRoom(Exception): pass 28 | 29 | 30 | class ItemNotInInventory(Exception): pass 31 | 32 | 33 | class ActorHasNoInventory(Exception): pass 34 | 35 | 36 | class ActorNotInRoom(Exception): pass 37 | 38 | 39 | class NotTakeable(Exception): pass 40 | 41 | 42 | def is_in_inventory(item, entity, throw_exc=False): 43 | # If I have an inventory... 44 | if not entity.has_component(Inventory): 45 | if throw_exc: 46 | raise ActorHasNoInventory 47 | return False 48 | inventory = entity.get_component(Inventory).contents 49 | 50 | # ...and the item is in it... 51 | if item._uid not in inventory: 52 | if throw_exc: 53 | raise ItemNotInInventory 54 | return False 55 | 56 | # ...then it... is in the inventory. 57 | return True 58 | 59 | 60 | def can_take(item, entity, throw_exc=False): 61 | # If I have an inventory... 62 | if not entity.has_component(Inventory): 63 | if throw_exc: 64 | raise ActorHasNoInventory 65 | return False 66 | inventory = entity.get_component(Inventory).contents 67 | 68 | # ...and I am somewhere... 69 | if not entity.has_component(RoomPresence): 70 | if throw_exc: 71 | raise ActorNotInRoom 72 | return False 73 | presence = entity.get_component(RoomPresence) 74 | 75 | # ...and there is also an item there... 76 | if not item._uid in presence.presences: 77 | if throw_exc: 78 | raise ItemNotInRoom 79 | return False 80 | 81 | # ...that can be taken... 82 | if not item.has_component(Takeable): 83 | if throw_exc: 84 | raise NotTakeable 85 | return False 86 | 87 | # ...then the item can be taken. 88 | return True 89 | 90 | 91 | def can_drop(item, entity, throw_exc=False): 92 | # If I have an inventory... 93 | if not entity.has_component(Inventory): 94 | if throw_exc: 95 | raise ActorHasNoInventory 96 | return False 97 | inventory = entity.get_component(Inventory).contents 98 | 99 | # ...with an item... 100 | if item._uid not in inventory: 101 | if throw_exc: 102 | raise ItemNotInInventory 103 | return False 104 | 105 | # ...that can be dropped... 106 | if not item.has_component(Takeable): 107 | if throw_exc: 108 | raise NotTakeable 109 | return False 110 | 111 | # ...and there is somewhere to drop it into, ... 112 | if not entity.has_component(RoomPresence): 113 | if throw_exc: 114 | raise ActorNotInRoom 115 | return False 116 | 117 | # ...then drop it like it's hot, drop it like it's hot. 118 | return True 119 | 120 | 121 | def take(item, entity): 122 | item.remove_component(RoomPresence) 123 | entity.get_component(Inventory).contents.append(item._uid) 124 | 125 | 126 | def drop(item, entity): 127 | room_uid = entity.get_component(RoomPresence).room 128 | inventory = entity.get_component(Inventory).contents 129 | idx = inventory.index(item._uid) 130 | del inventory[idx] 131 | item.add_component(RoomPresence(room=room_uid)) 132 | 133 | 134 | class TakeOrDrop(System): 135 | entity_filters = { 136 | 'take': and_filter([TakeAction]), 137 | 'drop': and_filter([DropAction]), 138 | } 139 | 140 | def update(self, entities_by_filter): 141 | for entity in entities_by_filter['take']: 142 | action = entity.get_component(TakeAction) 143 | try: 144 | item = self.world.get_entity(action.item) 145 | if can_take(item, entity, self.throw_exc): 146 | take(item, entity) 147 | except NoSuchUID: 148 | if self.throw_exc: 149 | raise 150 | entity.remove_component(TakeAction) 151 | for entity in entities_by_filter['drop']: 152 | item = self.world.get_entity(entity.get_component(DropAction).item) 153 | entity.remove_component(DropAction) 154 | if can_drop(item, entity, self.throw_exc): 155 | drop(item, entity) 156 | -------------------------------------------------------------------------------- /wecs/mechanics/__init__.py: -------------------------------------------------------------------------------- 1 | from .clock import SettableClock 2 | from .clock import Clock 3 | from .clock import DetermineTimestep 4 | -------------------------------------------------------------------------------- /wecs/mechanics/clock.py: -------------------------------------------------------------------------------- 1 | """ 2 | A :class:`Clock` measures time for an entity. A typical use case is to 3 | measure a frame's time in real-time applications, or advancing a 4 | simulation by specific time steps. Clocks provide a mechanism to clamp 5 | time steps to a maximum (e.g. so as not to overwhelm a physics engine, 6 | and allow it to regain real-time performance). They also provide a 7 | mechanism to build a hierarchy of clocks, with children running at a 8 | settable speed relative to their parent. 9 | """ 10 | 11 | from types import FunctionType 12 | from collections import defaultdict 13 | 14 | from wecs.core import Component 15 | from wecs.core import System 16 | from wecs.core import and_filter 17 | from wecs.core import UID 18 | 19 | 20 | class SettableClock: 21 | def __init__(self, dt=0.0): 22 | self.dt = dt 23 | 24 | def set(self, dt): 25 | self.dt = dt 26 | 27 | def __call__(self): 28 | return self.dt 29 | 30 | 31 | def panda3d_clock(): 32 | return globalClock.dt 33 | 34 | 35 | @Component() 36 | class Clock: 37 | """ 38 | clock: A function that is called with no arguments and returns the 39 | elapsed time. 40 | timestep: Deprecated. Use wall_time, frame_time, or game_time 41 | instead. 42 | max_timestep: float = 1.0 / 30 43 | scaling_factor: Time dilation factor. This clock's game_time runs 44 | with a speed of `scaling_factor` relative to its parent. 45 | Default: 1.0 46 | parent: UID of the entity with the parent clock. `None` for root 47 | clocks. 48 | wall_time: The actual time delta. Set by :class:`DetermineTimestep` 49 | frame_time: The wall time, clamped to max_timestep. 50 | game_time: Frame time, scaled by scaling factor 51 | """ 52 | clock: FunctionType = None 53 | timestep: float = 0.0 # Deprecated 54 | max_timestep: float = 1.0 / 30 55 | scaling_factor: float = 1.0 56 | parent: UID = None 57 | wall_time: float = 0.0 58 | frame_time: float = 0.0 59 | game_time: float = 0.0 60 | 61 | 62 | class DetermineTimestep(System): 63 | """ 64 | Update clocks. 65 | """ 66 | entity_filters = { 67 | 'clock': and_filter([Clock]), 68 | } 69 | 70 | def update(self, entities_by_filter): 71 | clocks_by_parent = defaultdict(set) 72 | for entity in entities_by_filter['clock']: 73 | clocks_by_parent[entity[Clock].parent].add(entity) 74 | updated_parents = set() 75 | for entity in clocks_by_parent[None]: 76 | clock = entity[Clock] 77 | dt = clock.clock() 78 | # Wall time: The last frame's physical duration 79 | clock.wall_time = dt 80 | # Frame time: Wall time, capped to a maximum 81 | max_timestep = clock.max_timestep 82 | if dt > max_timestep: 83 | dt = max_timestep 84 | clock.frame_time = dt 85 | # FIXME: Provided for legacy purposes 86 | clock.timestep = dt 87 | # Game time: Time-dilated frame time 88 | clock.game_time = clock.frame_time * clock.scaling_factor 89 | # ...and to start the loop... 90 | updated_parents.add(entity._uid) 91 | while updated_parents: 92 | next_parents = set() 93 | for parent in updated_parents: 94 | if parent in clocks_by_parent: 95 | for entity in clocks_by_parent[parent]: 96 | child_clock = entity[Clock] 97 | parent_clock = entity.world[parent][Clock] 98 | child_clock.wall_time = parent_clock.wall_time 99 | # FIXME: Rip out timestep 100 | child_clock.timestep = parent_clock.frame_time 101 | child_clock.frame_time = parent_clock.frame_time 102 | child_clock.game_time = parent_clock.game_time * child_clock.scaling_factor 103 | next_parents.add(entity._uid) 104 | updated_parents = next_parents 105 | -------------------------------------------------------------------------------- /wecs/panda3d/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import ECSShowBase 2 | 3 | import wecs.panda3d.prototype 4 | import wecs.panda3d.camera 5 | import wecs.panda3d.ai 6 | import wecs.panda3d.animation 7 | import wecs.panda3d.debug 8 | import wecs.panda3d.spawnpoints 9 | import wecs.panda3d.mouseover 10 | import wecs.panda3d.avatar_ui 11 | import wecs.panda3d.gravity 12 | import wecs.panda3d.interaction 13 | -------------------------------------------------------------------------------- /wecs/panda3d/avatar_ui.py: -------------------------------------------------------------------------------- 1 | import wecs 2 | 3 | from wecs.core import System 4 | from wecs.core import Component 5 | from wecs.core import Proxy 6 | from wecs.core import ProxyType 7 | 8 | from wecs.panda3d.prototype import Model 9 | from wecs.panda3d.input import Input 10 | from wecs.panda3d.mouseover import MouseOveringCamera 11 | from wecs.panda3d.mouseover import UserInterface 12 | from wecs.panda3d.mouseover import Selectable 13 | from wecs.panda3d.ai import BehaviorAI 14 | 15 | 16 | @Component() 17 | class Embodiable: 18 | pass 19 | 20 | 21 | class AvatarUI(System): 22 | """ 23 | Command rules: 24 | if not embodied and not targeting_selection: 25 | selection(goto target) 26 | if not embodied and targeting_selection: 27 | selection(idle) 28 | if embodied and selecting and not targeting_selection: 29 | selection(goto target) 30 | if embodied and selecting and targeting_selection: 31 | selection(idle) 32 | if embodied and not selecting and targeting_selection: 33 | self(idle) 34 | if embodied and not selecting and not targeting_selection: 35 | self(goto target) 36 | """ 37 | entity_filters = { 38 | 'cursor': [Input, MouseOveringCamera, UserInterface], 39 | } 40 | proxies = { 41 | 'parent': ProxyType(Model, 'parent'), 42 | } 43 | input_context = 'select_entity' 44 | 45 | def update(self, entities_by_filter): 46 | for entity in entities_by_filter['cursor']: 47 | input = entity[Input] 48 | if self.input_context in input.contexts: 49 | context = base.device_listener.read_context(self.input_context) 50 | self.process_input(entity, context) 51 | 52 | def process_input(self, entity, context): 53 | ui = entity[UserInterface] 54 | mouseover = entity[MouseOveringCamera] 55 | 56 | mouseovered_entity = None 57 | if mouseover.entity is not None: 58 | mouseovered_entity = self.world.get_entity(mouseover.entity) 59 | targeting_self = mouseover.entity == entity._uid 60 | 61 | selected_entity = None 62 | if ui.selected_entity is not None: 63 | selected_entity = self.world.get_entity(ui.selected_entity) 64 | target_entity = None 65 | if ui.targeted_entity is not None: 66 | target_entity = self.world.get_entity(ui.targeted_entity) 67 | point_coordinates = ui.point_coordinates 68 | targeting_selection = False 69 | if selected_entity is not None and ui.selected_entity == ui.targeted_entity: 70 | targeting_selection = True 71 | 72 | embodied = Embodiable in entity 73 | targeting_embodiable = None 74 | if mouseovered_entity is not None: 75 | targeting_embodiable = Embodiable in mouseovered_entity 76 | 77 | # Now we can evaluate the player's input. First, he clicked to 78 | # select. 79 | if context.get('select', False): 80 | if target_entity is None or Selectable not in target_entity: 81 | # Selecting while not pointing at a valid target 82 | # unselects. 83 | ui.selected_entity = None 84 | ui.select_indicator.detach_node() 85 | else: 86 | # But selecting a selectable entity... selects it. 87 | if not embodied or mouseover.entity != entity._uid: 88 | ui.selected_entity = target_entity._uid 89 | # The player instead clicked to give a command, and there is a 90 | # valid target, ... 91 | elif context.get('command', False): 92 | # 3rd person mode, giving command with to selected entity 93 | if not embodied and selected_entity and target_entity and not targeting_selection: 94 | action = ['walk_to_entity', mouseover.entity] 95 | if point_coordinates: 96 | action.append(point_coordinates) 97 | self.command(selected_entity, *action) 98 | if not embodied and targeting_selection: 99 | self.command(selected_entity, 'idle') 100 | if embodied and selected_entity and target_entity and not targeting_selection: 101 | action = ['walk_to_entity', mouseover.entity] 102 | if point_coordinates: 103 | action.append(point_coordinates) 104 | self.command(selected_entity, *action) 105 | if embodied and ui.selected_entity and target_entity and not targeting_selection: 106 | self.command(entity, 'idle') 107 | if embodied and targeting_selection: 108 | self.command(selected_entity, 'idle') 109 | if embodied and not ui.selected_entity and targeting_selection: 110 | self.command(entity, 'idle') 111 | if embodied and not ui.selected_entity and target_entity and not targeting_self: 112 | action = ['walk_to_entity', mouseover.entity] 113 | if point_coordinates: 114 | action.append(point_coordinates) 115 | self.command(entity, *action) 116 | if embodied and targeting_self and not ui.selected_entity: 117 | self.command(entity, 'idle') 118 | # Now the player clicked to dis-/embody... 119 | elif context.get('embody', False): 120 | if not embodied and not targeting_embodiable and selected_entity: 121 | self.embody(entity, selected_entity) 122 | if not embodied and targeting_embodiable: 123 | self.embody(entity, target_entity) 124 | if embodied and targeting_embodiable and not targeting_self: 125 | self.jump_body(entity, target_entity) 126 | if embodied and not targeting_embodiable: 127 | self.disembody(entity) 128 | 129 | def command(self, entity, *action): 130 | ai = entity[BehaviorAI] 131 | ai.behavior = action 132 | 133 | def embody(self, entity, target): 134 | raise NotImplementedError 135 | 136 | def jump_body(self, entity, target): 137 | raise NotImplementedError 138 | 139 | def disembody(self, entity): 140 | raise NotImplementedError 141 | 142 | -------------------------------------------------------------------------------- /wecs/panda3d/behavior_trees.py: -------------------------------------------------------------------------------- 1 | from pychology.behavior_trees import BehaviorTree 2 | from pychology.behavior_trees import Decorator 3 | from pychology.behavior_trees import ActiveOnCondition 4 | from pychology.behavior_trees import FailOnCondition 5 | from pychology.behavior_trees import DoneOnCondition 6 | from pychology.behavior_trees import NodeState 7 | 8 | from wecs.panda3d.ai import BehaviorAI 9 | from wecs.panda3d.ai import CharacterController 10 | 11 | 12 | # Behavior Trees 13 | 14 | class IdleWhenDoneTree(BehaviorTree): 15 | """ 16 | A behavior tree that, when it is done, sets the entity's behavior 17 | to `['idle']`. 18 | """ 19 | def done(self, entity): 20 | entity[BehaviorAI].behavior = ['idle'] 21 | 22 | 23 | # Timer decorators 24 | 25 | class Timer(Decorator): 26 | """ 27 | Returns a defined NodeState if the wrapped tree has been run for 28 | more than a specified time, using the entity's Clock.game_time to 29 | increment its counter. Until then, it'll evaluate its subtree 30 | normally and return its state. For concrete implementations, use 31 | ActiveTimer, FailTimer, and DoneTimer. 32 | """ 33 | def __init__(self, condition, tree): 34 | self.condition = condition 35 | self.tree = tree 36 | self.timer = 0.0 37 | 38 | def __call__(self, entity, *args, **kwargs): 39 | self.timer += entity[wecs.mechanics.clock.Clock].game_time 40 | if self.condition(self.timer, entity, *args, **kwargs): 41 | return self.reaction() 42 | return self.tree(entity, *args, **kwargs) 43 | 44 | def reset(self): 45 | self.timer = 0.0 46 | super().reset() 47 | 48 | 49 | class ActiveTimer(Timer, ActiveOnCondition): pass 50 | class FailTimer(Timer, FailOnCondition): pass 51 | class DoneTimer(Timer, DoneOnCondition): pass 52 | 53 | 54 | def timeout(timeout): 55 | """ 56 | Condition to be used on Timers. True after the given amount of time 57 | has elapsed since this decorator became active. 58 | """ 59 | def inner(timer, *args, **kwargs): 60 | return timer >= timeout 61 | return inner 62 | 63 | 64 | import wecs 65 | from panda3d.core import Vec3 66 | 67 | 68 | def distance_smaller(target_distance): 69 | """ 70 | Condition: Distance between origin of "this" entity and a point in 71 | the other entity's space. 72 | """ 73 | def inner(entity_a, entity_b_uid, coords=None): 74 | if coords is None: 75 | coords = Vec3(0, 0, 0) 76 | 77 | node_a = entity_a[wecs.panda3d.prototype.Model].node 78 | entity_b = base.ecs_world.get_entity(entity_b_uid) 79 | node_b = entity_b[wecs.panda3d.prototype.Model].node 80 | distance = node_a.get_relative_point(node_b, coords).length() 81 | return distance <= target_distance 82 | return inner 83 | 84 | 85 | def is_pointable(entity, target_entity_uid, coords=None): 86 | """ 87 | Condition. Is the target entity mouseover-Pointable? 88 | """ 89 | target_entity = base.ecs_world.get_entity(target_entity_uid) 90 | return wecs.panda3d.mouseover.Pointable in target_entity 91 | 92 | 93 | def turn(speed): 94 | """ 95 | Atomic behavior 96 | """ 97 | def inner(entity): 98 | character = entity[CharacterController] 99 | character.move = Vec3(0, 0, 0) 100 | character.heading = speed 101 | character.pitch = 0.0 102 | character.sprints = False 103 | character.crouches = False 104 | character.jumps = False 105 | return NodeState.ACTIVE 106 | return inner 107 | 108 | 109 | def walk_to_entity(entity, target_entity_uid, coords=None): 110 | func = wecs.panda3d.ai.walk_to_entity 111 | if coords is None: 112 | func(entity, target_entity_uid) 113 | else: 114 | func(entity, target_entity_uid, coordinates=coords) 115 | return NodeState.ACTIVE 116 | -------------------------------------------------------------------------------- /wecs/panda3d/clock.py: -------------------------------------------------------------------------------- 1 | from wecs.core import and_filter 2 | from wecs.mechanics.clock import Clock 3 | from wecs.mechanics.clock import DetermineTimestep 4 | from wecs.panda3d.input import Input 5 | 6 | 7 | class UpdateClocks(DetermineTimestep): 8 | input_context = 'clock_control' 9 | 10 | def __init__(self, *args, **kwargs): 11 | self.entity_filters.update({ 12 | 'input': and_filter([ 13 | Clock, 14 | Input, 15 | ]), 16 | }) 17 | super().__init__(*args, **kwargs) 18 | 19 | def update(self, entities_by_filter): 20 | for entity in entities_by_filter['input']: 21 | input = entity[Input] 22 | if self.input_context in input.contexts: 23 | context = base.device_listener.read_context(self.input_context) 24 | clock = entity[Clock] 25 | clock.scaling_factor *= 1 + context['time_zoom'] * 0.01 26 | 27 | super().update(entities_by_filter) 28 | -------------------------------------------------------------------------------- /wecs/panda3d/constants.py: -------------------------------------------------------------------------------- 1 | # Solid for purposes of wecs.panda3d.character.Falling 2 | FALLING_MASK = 1 << 1 3 | # Solid for purposes of wecs.panda3d.character.Bumping 4 | BUMPING_MASK = 1 << 2 5 | # CollisionZoom.mask; The zooming 3rd person camera will not go behind 6 | # this object. 7 | CAMERA_MASK = 1 << 3 8 | # This object can be detected by putting the mouse pointer over it and 9 | # using wecs.panda3d.mouseover 10 | MOUSEOVER_MASK = 1 << 4 11 | # This object can interact with interactable objects, or is an object 12 | # that can be interacted with. 13 | INTERACTION_MASK = 1 << 5 14 | -------------------------------------------------------------------------------- /wecs/panda3d/core.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from panda3d.core import PStatCollector 4 | from panda3d.core import PythonTask 5 | 6 | from direct.showbase.ShowBase import ShowBase 7 | from direct.task import Task 8 | 9 | from wecs.core import World 10 | from wecs.core import System 11 | 12 | logging.getLogger().setLevel(logging.INFO) 13 | 14 | 15 | class ECSShowBase(ShowBase): 16 | def __init__(self, *args, **kwargs): 17 | super().__init__(self, *args, **kwargs) 18 | self.ecs_world = World() 19 | self.ecs_system_pstats = {} 20 | self.task_to_data = {} 21 | self.system_to_data = {} 22 | 23 | def add_system(self, system, sort, priority=None): 24 | """ 25 | Registers an additional system in the world. 26 | The world will use the standard panda3D taskManager to ensure the system 27 | is run on every tick. 28 | 29 | :param system: Instance of a :class:`wecs.core.System` 30 | :param sort: `sort` parameter for the task running the system 31 | :param priority: Optional `priority` parameter for the task running the system 32 | :return: Panda3D PythonTask 33 | 34 | """ 35 | logging.info(f"in {__name__} got {system}, {sort}, {priority}") 36 | if priority is None: 37 | priority = 0 38 | wecs_sort = (sort, -priority) 39 | 40 | self.ecs_world.add_system(system, wecs_sort) 41 | task = base.task_mgr.add( 42 | self.run_system, 43 | repr(system), 44 | extraArgs=[system], 45 | sort=sort, 46 | priority=priority, 47 | ) 48 | self.ecs_system_pstats[system] = PStatCollector('App:WECS:Systems:{}'.format(system)) 49 | 50 | system_type = type(system) 51 | data = (system_type, task, wecs_sort) 52 | self.task_to_data[task] = data 53 | self.system_to_data[system_type] = data 54 | return task 55 | 56 | def run_system(self, system): 57 | self.ecs_system_pstats[system].start() 58 | base.ecs_world._update_system(system) 59 | self.ecs_system_pstats[system].stop() 60 | return Task.cont 61 | 62 | def remove_system(self, task_or_system): 63 | if isinstance(task_or_system, PythonTask): 64 | data = self.task_to_data[task_or_system] 65 | elif issubclass(task_or_system, System): 66 | data = self.system_to_data[task_or_system] 67 | else: 68 | raise ValueError(task_or_system, type(task_or_system)) 69 | system_type, task, wecs_sort = data 70 | 71 | self.task_mgr.remove(task) 72 | self.ecs_world.remove_system(system_type) 73 | del self.task_to_data[task] 74 | del self.system_to_data[system_type] 75 | -------------------------------------------------------------------------------- /wecs/panda3d/debug.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from panda3d.core import PStatClient 4 | 5 | from wecs.core import Component 6 | from wecs.core import System 7 | from wecs.core import and_filter 8 | from wecs.panda3d.input import Input 9 | 10 | 11 | class DebugTools(System): 12 | entity_filters = {} 13 | input_context = 'debug' 14 | frame_rate_meter = False 15 | console_open = False 16 | 17 | def update(self, entities_by_filter): 18 | context = base.device_listener.read_context('debug') 19 | if context['quit']: 20 | sys.exit() 21 | if context['pdb']: 22 | import pdb 23 | pdb.set_trace() 24 | if context['pstats']: 25 | base.pstats = True 26 | PStatClient.connect() 27 | if context['frame_rate_meter']: 28 | self.frame_rate_meter = not self.frame_rate_meter 29 | base.set_frame_rate_meter(self.frame_rate_meter) 30 | if context['console']: 31 | if not hasattr(base, 'console'): 32 | print("No console present.") 33 | return 34 | self.console_open = not self.console_open 35 | if self.console_open: 36 | base.console.node().show() 37 | else: 38 | base.console.node().hide() 39 | -------------------------------------------------------------------------------- /wecs/panda3d/gravity.py: -------------------------------------------------------------------------------- 1 | import math 2 | from dataclasses import field 3 | import enum 4 | 5 | from panda3d.core import Vec3 6 | 7 | from wecs.core import System 8 | from wecs.core import Component 9 | from wecs.core import Proxy 10 | from wecs.core import ProxyType 11 | 12 | from wecs.panda3d.prototype import Model 13 | from wecs.panda3d.character import CharacterController 14 | 15 | 16 | class GravityType(enum.Enum): 17 | BASIC = 1 18 | CYLINDER = 2 19 | SPHERE = 3 20 | 21 | 22 | class GravityDirection(enum.Enum): 23 | INWARD = 1 24 | OUTWARD = 2 25 | 26 | 27 | @Component() 28 | class GravityMap: 29 | """ 30 | Either this entity's `Model` contains gravity nodes which will be 31 | looked up by name, or the nodes are given explicitly. 32 | """ 33 | nodes: dict = field(default_factory=dict) 34 | node_names: list = field(default_factory=lambda: ["gravity"]) 35 | geometry: GravityType = GravityType.BASIC 36 | direction: GravityDirection = GravityDirection.INWARD 37 | strength: float = 30.0 38 | 39 | 40 | @Component() 41 | class GravityMovement: 42 | """ 43 | Entity uses gravity nodes on maps to determine its gravity vector. 44 | 45 | :param:`node_names`: List of names of gravity nodes to use. 46 | """ 47 | node_names: list = field(default_factory=lambda: ["gravity"]) 48 | 49 | class AdjustGravity(System): 50 | entity_filters = { 51 | 'map': [ 52 | Proxy('model_node'), 53 | GravityMap, 54 | ], 55 | 'character': [ 56 | Proxy('model_node'), 57 | CharacterController, 58 | GravityMovement, 59 | ], 60 | } 61 | proxies = {'model_node': ProxyType(Model, 'node')} 62 | 63 | def __init__(self, *args, **kwargs): 64 | super().__init__(*args, **kwargs) 65 | self.known_maps = set() 66 | 67 | def enter_filter_map(self, entity): 68 | self.known_maps.add(entity) 69 | 70 | gravity_map = entity[GravityMap] 71 | model_node = self.proxies['model_node'].field(entity) 72 | 73 | for node_name in gravity_map.node_names: 74 | # FIXME: Respect multiple names 75 | node = model_node.find(f'**/{node_name}') 76 | # FIXME: Raise warning if node not found 77 | if not node.is_empty(): 78 | # FIXME: Maybe don't overwrite given nodes? 79 | gravity_map.nodes[node_name] = node 80 | 81 | def exit_filter_map(self, entity): 82 | self.known_maps.remove(entity) 83 | 84 | def update(self, entities_by_filter): 85 | for entity in entities_by_filter['character']: 86 | character = entity[CharacterController] 87 | gravity = entity[GravityMovement] 88 | model_node = self.proxies['model_node'].field(entity) 89 | 90 | # FIXME: We just use the first given name, and the first 91 | # node found. Both should deal with multiples. 92 | gravity_name = gravity.node_names[0] 93 | for map_entity in self.known_maps: 94 | gravity_map = map_entity[GravityMap] 95 | if gravity_name in gravity_map.nodes: 96 | gravity_node = gravity_map.nodes[gravity_name] 97 | attractor = model_node.get_pos(gravity_node) 98 | 99 | if gravity_map.geometry == GravityType.BASIC: 100 | attractor = Vec3(0, 0, -1) 101 | elif gravity_map.geometry == GravityType.CYLINDER: 102 | attractor.y = 0.0 103 | elif gravity_map.geometry == GravityType.SPHERE: 104 | pass 105 | else: 106 | # FIXME 107 | raise Exception 108 | 109 | attractor.normalize() 110 | attractor *= gravity_map.strength 111 | 112 | if gravity_map.direction == GravityDirection.INWARD: 113 | attractor *= -1 114 | elif gravity_map.direction == GravityDirection.OUTWARD: 115 | pass 116 | else: 117 | # FIXME 118 | raise Exception 119 | 120 | local_gravity = model_node.get_relative_vector( 121 | gravity_node, 122 | attractor, 123 | ) 124 | character.gravity = local_gravity 125 | break 126 | 127 | 128 | 129 | class ErectCharacter(System): 130 | entity_filters = { 131 | 'character': [ 132 | Proxy('model_node'), 133 | CharacterController, 134 | GravityMovement, 135 | ], 136 | } 137 | proxies = {'model_node': ProxyType(Model, 'node')} 138 | 139 | def update(self, entities_by_filter): 140 | for entity in entities_by_filter['character']: 141 | character = entity[CharacterController] 142 | model_node = self.proxies['model_node'].field(entity) 143 | up = character.gravity * -1 144 | 145 | roll = math.atan(character.gravity.x / character.gravity.z) / (2.0 * math.pi) * 360.0 146 | model_node.set_r(model_node, roll) 147 | 148 | # FIXME: We now shoud recalculate gravity by also rolling the vector. 149 | 150 | pitch = math.atan(character.gravity.y / character.gravity.z) / (2.0 * math.pi) * 360.0 151 | model_node.set_p(model_node, -pitch) 152 | 153 | character.gravity = Vec3(0, 0, -character.gravity.length()) 154 | -------------------------------------------------------------------------------- /wecs/panda3d/input.py: -------------------------------------------------------------------------------- 1 | from dataclasses import field 2 | 3 | from wecs.core import Component 4 | from wecs.core import System 5 | from wecs.core import and_filter 6 | 7 | 8 | @Component() 9 | class Input: 10 | contexts: list = field(default_factory=list) 11 | -------------------------------------------------------------------------------- /wecs/panda3d/interaction.py: -------------------------------------------------------------------------------- 1 | from dataclasses import field 2 | 3 | from panda3d.core import CollisionTraverser 4 | from panda3d.core import CollisionHandlerQueue 5 | 6 | from wecs.core import Component 7 | from wecs.core import Proxy 8 | from wecs.core import ProxyType 9 | from wecs.core import and_filter 10 | 11 | from wecs.panda3d.prototype import Model 12 | from wecs.panda3d.character import CollisionSystem 13 | 14 | from wecs.panda3d.constants import INTERACTION_MASK 15 | 16 | 17 | @Component() 18 | class Interactor: 19 | tag_name: str = 'interacting' 20 | node_name: str = 'interactor' 21 | solids: dict = None 22 | from_collide_mask: int = INTERACTION_MASK 23 | into_collide_mask: int = 0 24 | traverser: CollisionTraverser = field(default_factory=CollisionTraverser) 25 | queue: CollisionHandlerQueue = field(default_factory=CollisionHandlerQueue) 26 | debug: bool = False 27 | # List of possible active interactions 28 | interactions: list = field(default_factory=list) 29 | # What actions are available with which entity this frame? 30 | # Written by the Interacting system. 31 | action_options: list = field(default_factory=list) 32 | 33 | 34 | @Component() 35 | class Interactee: 36 | tag_name: str = 'interacting' 37 | node_name: str = 'interactee' 38 | solids: dict = None 39 | from_collide_mask: int = 0 40 | into_collide_mask: int = INTERACTION_MASK 41 | traverser: CollisionTraverser = None 42 | queue: CollisionHandlerQueue = None 43 | debug: bool = False 44 | # List of possible passive interactions 45 | interactions: list = field(default_factory=list) 46 | 47 | 48 | class Interacting(CollisionSystem): 49 | ''' 50 | Check for collisions between interactors and interactees. 51 | 52 | Components used :func:`wecs.core.and_filter` 53 | | :class:`wecs.panda3d.model.Model` 54 | ''' 55 | entity_filters = { 56 | 'interactor': and_filter([ 57 | Proxy('character_node'), 58 | Interactor, 59 | ]), 60 | 'interactee': and_filter([ 61 | Proxy('character_node'), 62 | Interactee, 63 | ]), 64 | } 65 | proxies = { 66 | 'character_node': ProxyType(Model, 'node'), 67 | 'scene_node': ProxyType(Model, 'parent'), 68 | } 69 | 70 | def enter_filter_interactor(self, entity): 71 | self.init_sensors(entity, entity[Interactor]) 72 | 73 | def enter_filter_interactee(self, entity): 74 | self.init_sensors(entity, entity[Interactee]) 75 | component = entity[Interactee] 76 | for solid in component.solids.values(): 77 | node = solid['node'] 78 | node.set_python_tag( 79 | 'interactions-entity', 80 | entity, 81 | ) 82 | 83 | def exit_filter_interactor(self, entity): 84 | print(f'FIXME: exit_filter_interactor {entity}') 85 | 86 | def exit_filter_interactee(self, entity): 87 | print(f'FIXME: exit_filter_interactee {entity}') 88 | 89 | def update(self, entities_by_filter): 90 | for entity in entities_by_filter['interactor']: 91 | self.run_sensors(entity, entity[Interactor]) 92 | entity[Interactor].action_options = [] 93 | self.check_action_options(entity) 94 | 95 | def check_action_options(self, entity): 96 | for contact in entity[Interactor].contacts: 97 | into_np = contact.get_into_node_path() 98 | into_entity = into_np.get_python_tag('interactions-entity') 99 | if into_entity == entity: 100 | pass 101 | else: 102 | from_actions = entity[Interactor].interactions 103 | into_actions = into_entity[Interactee].interactions 104 | matches = [fa for fa in from_actions 105 | if fa in into_actions] 106 | for match in matches: 107 | entity[Interactor].action_options.append( 108 | (match, entity), 109 | ) 110 | 111 | 112 | class PrintActionOptions(CollisionSystem): 113 | ''' 114 | Check for collisions between interactors and interactees. 115 | 116 | Components used :func:`wecs.core.and_filter` 117 | | :class:`wecs.panda3d.model.Model` 118 | ''' 119 | entity_filters = { 120 | 'interactor': and_filter([ 121 | Proxy('character_node'), 122 | Interactor, 123 | ]), 124 | } 125 | proxies = { 126 | 'character_node': ProxyType(Model, 'node'), 127 | } 128 | 129 | def update(self, entities_by_filter): 130 | print("### Interactions") 131 | for entity in entities_by_filter['interactor']: 132 | print(entity, entity[Interactor].action_options) 133 | print() 134 | -------------------------------------------------------------------------------- /wecs/panda3d/map_muncher.py: -------------------------------------------------------------------------------- 1 | def load_map(): 2 | map_node = base.loader.load_model(args.map_file) 3 | 4 | if not map_node.find('**/+GeomNode').is_empty(): 5 | # There's geometry in the map; It's actually a map! 6 | game_map.add( 7 | base.ecs_world.create_entity(name="Map"), 8 | overrides={ 9 | wecs.panda3d.prototype.Geometry: dict(node=map_node), 10 | } 11 | ) 12 | else: 13 | base.ecs_world.create_entity( 14 | wecs.panda3d.spawnpoints.SpawnMap(), 15 | wecs.panda3d.prototype.Model(node=map_node), 16 | name="Map", 17 | ) 18 | 19 | 20 | def create_character(model, spawn_point, aspect, name='Character'): 21 | # FIXME: There are a lot of constants here that should be drawn 22 | # from the model itself and the spawn point node. 23 | bumper_node = model.find('**/=bumper') 24 | bumper_spec = { 25 | 'bumper': dict( 26 | shape=CollisionSphere, 27 | center=bumper_node.get_pos(), 28 | radius=bumper_node.get_scale().x * 2, 29 | ), 30 | } 31 | lifter_node = model.find('**/=lifter') 32 | lifter_spec = { 33 | 'lifter': dict( 34 | shape=CollisionSphere, 35 | center=lifter_node.get_pos(), 36 | radius=lifter_node.get_scale().x * 2, 37 | ), 38 | } 39 | mouseover_node = model.find('**/=mouseover') 40 | pos = mouseover_node.get_pos() 41 | scale = mouseover_node.get_scale().x 42 | mouseover_spec = CollisionSphere(pos.x, pos.y, pos.z, scale) 43 | 44 | aspect.add( 45 | base.ecs_world.create_entity(name=name), 46 | overrides={ 47 | wecs.panda3d.prototype.Geometry: dict( 48 | file=model_file, 49 | ), 50 | wecs.panda3d.prototype.Actor: dict( 51 | file=model_file, 52 | ), 53 | wecs.panda3d.character.BumpingMovement: dict( 54 | solids=bumper_spec, 55 | ), 56 | wecs.panda3d.character.FallingMovement: dict( 57 | solids=lifter_spec, 58 | ), 59 | MouseOverable: dict( 60 | solid=mouseover_spec, 61 | ), 62 | wecs.panda3d.spawnpoints.SpawnAt: dict( 63 | name=spawn_point, 64 | ), 65 | }, 66 | ) 67 | 68 | 69 | def create_map(model): 70 | game_map.add( 71 | base.ecs_world.create_entity(name="Map"), 72 | overrides={ 73 | wecs.panda3d.prototype.Geometry: dict(node=model), 74 | }, 75 | ) 76 | 77 | 78 | for node in map_node.find_all_matches('**/spawn_point:*'): 79 | # This is Python 3.9+: 80 | # spawn_name = node.get_name().removeprefix('spawn_point:') 81 | spawn_point = node.get_name() 82 | spawn_name = spawn_point[len('spawn_point:'):] 83 | collection = node.get_tag('collection') 84 | model_file = '{}.bam'.format(collection) 85 | model = base.loader.load_model(model_file) 86 | entity_type = model.get_tag('entity_type') 87 | 88 | print("Creating {} from {} at {}".format(entity_type, collection, spawn_name)) 89 | if entity_type == 'character': 90 | character_type = node.get_tag('character_type') 91 | if character_type == 'player_character': 92 | create_character(model, spawn_point, player_character) 93 | elif character_type == 'non_player_character': 94 | create_character(model, spawn_point, non_player_character) 95 | elif entity_type == 'map': 96 | create_map(model) 97 | elif entity_type == 'nothing': 98 | pass 99 | else: 100 | print("Unknown entity type '{}'.".format(entity_type)) 101 | -------------------------------------------------------------------------------- /wecs/panda3d/spawnpoints.py: -------------------------------------------------------------------------------- 1 | from wecs.core import Component 2 | from wecs.core import System 3 | from wecs.core import Proxy 4 | from wecs.core import ProxyType 5 | 6 | from wecs.panda3d.prototype import Model 7 | 8 | 9 | @Component() 10 | class SpawnMap: 11 | pass 12 | 13 | 14 | @Component() 15 | class SpawnAt: 16 | """ 17 | Adding this component to an entity will make the `Spawn` system 18 | attach the entity's `model_node` (as defined in 19 | `Spawn.proxies['model_node']` 20 | """ 21 | name: str = None 22 | 23 | 24 | class Spawn(System): 25 | entity_filters = { 26 | 'map': [SpawnMap, Proxy('map_node')], 27 | 'spawners': [SpawnAt, Proxy('model_node')], 28 | } 29 | proxies = { 30 | 'map_node': ProxyType(Model, 'node'), 31 | 'model_node': ProxyType(Model, 'node'), 32 | } 33 | 34 | def update(self, entities_by_filter): 35 | """ 36 | Attach the entities to spawn to their spawn point. 37 | """ 38 | for entity in entities_by_filter['spawners']: 39 | spawn_point_name = entity[SpawnAt].name 40 | 41 | # Try finding a node with that name in all the maps. If 42 | # there should be multiple nodes of that name, then in the 43 | # first map with one, and the one with the shortest path 44 | 45 | # 46 | for map_entity in entities_by_filter['map']: 47 | map_node = self.proxies['map_node'].field(map_entity) 48 | search_pattern = '**/{}'.format(spawn_point_name) 49 | spawn_point = map_node.find(search_pattern) 50 | if not spawn_point.is_empty(): 51 | model_node = self.proxies['model_node'].field(entity) 52 | model_node.reparent_to(spawn_point) 53 | # We reparent to the first child so it inherrits the lights 54 | model_node.wrt_reparent_to(map_node.get_child(0)) 55 | break 56 | else: 57 | print("Spawn point '{}' not found".format(spawn_point_name)) 58 | del entity[SpawnAt] 59 | -------------------------------------------------------------------------------- /wecs/repl.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | 4 | from panda3d.core import Notify 5 | from panda3d.core import StringStream 6 | from io import StringIO 7 | from contextlib import redirect_stdout 8 | from code import InteractiveConsole 9 | 10 | 11 | class Interpreter(InteractiveConsole): 12 | def __init__(self, *args): 13 | InteractiveConsole.__init__(self, *args) 14 | self.stream = StringStream() 15 | self.stringio = StringIO() 16 | self.input_string = "" 17 | self.output_string = "" 18 | self.more = 0 19 | self.prompt = ">>> " 20 | 21 | def runline(self, line): 22 | self.output_string = "" 23 | self.input_string = self.prompt + line + "\n" 24 | self.write(self.input_string) 25 | 26 | if self.push(line): 27 | self.prompt = "... " 28 | else: 29 | self.prompt = ">>> " 30 | 31 | def showsyntaxerror(self, filename=None): 32 | type, value, tb = sys.exc_info() 33 | sys.last_type = type 34 | sys.last_value = value 35 | sys.last_traceback = tb 36 | if filename and type is SyntaxError: 37 | # Work hard to stuff the correct filename in the exception 38 | try: 39 | msg, (dummy_filename, lineno, offset, line) = value.args 40 | except ValueError: 41 | # Not the format we expect; leave it alone 42 | pass 43 | else: 44 | # Stuff in the right filename 45 | value = SyntaxError(msg, (filename, lineno, offset, line)) 46 | sys.last_value = value 47 | lines = traceback.format_exception_only(type, value) 48 | self.write(''.join(lines)) 49 | 50 | def runcode(self, code): 51 | # Empty buffers 52 | self.stringio.truncate(0) 53 | self.stringio.seek(0) 54 | self.stream.clearData() 55 | try: 56 | # Swap buffers 57 | notify = Notify.ptr() 58 | old_notify = notify.get_ostream_ptr() 59 | notify.set_ostream_ptr(self.stream, False) 60 | with redirect_stdout(self.stringio): 61 | # Exec, writing output to buffers 62 | exec(code, self.locals) 63 | # Write buffers to output string. 64 | io_data = self.stringio.getvalue() 65 | stream_data = self.stream.getData().decode("utf-8") 66 | self.write(io_data + stream_data) 67 | # Restore buffers 68 | notify.set_ostream_ptr(old_notify, False) 69 | except: 70 | self.showtraceback() 71 | 72 | def showtraceback(self): 73 | sys.last_type, sys.last_value, last_tb = ei = sys.exc_info() 74 | sys.last_traceback = last_tb 75 | try: 76 | # Normally, if someone has set sys.excepthook, we let that take 77 | # precedence over self.write; but cefpython sets sys.excepthook 78 | # making this behavior undesirable 79 | lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next) 80 | self.write(''.join(lines)) 81 | finally: 82 | last_tb = ei = None 83 | 84 | def write(self, output_string): 85 | self.output_string += output_string 86 | -------------------------------------------------------------------------------- /wecs/rooms.py: -------------------------------------------------------------------------------- 1 | from dataclasses import field 2 | 3 | from wecs.core import Component, System, UID, and_filter 4 | 5 | 6 | # Rooms, and being in a room 7 | @Component() 8 | class Room: 9 | # Neighboring room entities 10 | adjacent: list = field(default_factory=list) 11 | # Entities (thought to be) in the room 12 | presences: list = field(default_factory=list) 13 | # Presence entered the room 14 | arrived: list = field(default_factory=list) 15 | # Presence continues to be present 16 | continued: list = field(default_factory=list) 17 | # Presences that left the room 18 | gone: list = field(default_factory=list) 19 | 20 | 21 | @Component() 22 | class RoomPresence: 23 | room: UID 24 | # Entities perceived 25 | presences: list = field(default_factory=list) 26 | 27 | 28 | @Component() 29 | class ChangeRoomAction: 30 | room: UID # Room to change to 31 | 32 | 33 | class EntityNotInARoom(Exception): pass 34 | 35 | 36 | class ItemNotInARoom(Exception): pass 37 | 38 | 39 | class RoomsNotAdjacent(Exception): pass 40 | 41 | 42 | def is_in_room(item, entity, throw_exc=False): 43 | if not entity.has_component(RoomPresence): 44 | if throw_exc: 45 | raise EntityNotInARoom 46 | else: 47 | return False 48 | presence = entity.get_component(RoomPresence) 49 | 50 | if item._uid not in presence.presences: 51 | if throw_exc: 52 | raise ItemNotInARoom 53 | else: 54 | return False 55 | 56 | return True 57 | 58 | 59 | class PerceiveRoom(System): 60 | entity_filters = { 61 | 'room': and_filter([Room]), 62 | 'presences': and_filter([RoomPresence]), 63 | } 64 | 65 | def update(self, filtered_entities): 66 | # Clean the bookkeeping lists 67 | for entity in filtered_entities['room']: 68 | room = entity.get_component(Room) 69 | room.arrived = [] 70 | room.continued = [] 71 | room.gone = [] 72 | # New arrivals to rooms, and continued presences 73 | for entity in filtered_entities['presences']: 74 | room_uid = entity.get_component(RoomPresence).room 75 | room_entity = self.world.get_entity(room_uid) 76 | room = room_entity.get_component(Room) 77 | if entity._uid not in room.presences: 78 | room.arrived.append(entity._uid) 79 | else: 80 | room.continued.append(entity._uid) 81 | # Checking who is gone 82 | for entity in filtered_entities['room']: 83 | room = entity.get_component(Room) 84 | for presence in room.presences: 85 | if presence not in room.continued: 86 | room.gone.append(presence) 87 | # Rebuilding the presence lists 88 | for entity in filtered_entities['room']: 89 | room = entity.get_component(Room) 90 | room.presences = room.arrived + room.continued 91 | # Let the presences perceive the presences in the room 92 | for entity in filtered_entities['presences']: 93 | presence = entity.get_component(RoomPresence) 94 | room_entity = self.world.get_entity(presence.room) 95 | room = room_entity.get_component(Room) 96 | presence.presences = room.presences 97 | 98 | 99 | class ChangeRoom(System): 100 | entity_filters = { 101 | 'act': and_filter([ChangeRoomAction, RoomPresence]) 102 | } 103 | 104 | def update(self, filtered_entities): 105 | for entity in filtered_entities['act']: 106 | room = self.world.get_entity( 107 | entity.get_component(RoomPresence).room, 108 | ) 109 | target = entity.get_component(ChangeRoomAction).room 110 | 111 | if target not in room.get_component(Room).adjacent: 112 | if self.throw_exc: 113 | raise RoomsNotAdjacent 114 | else: 115 | entity.get_component(RoomPresence).room = target 116 | 117 | entity.remove_component(ChangeRoomAction) 118 | --------------------------------------------------------------------------------