├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── examples ├── add-delete.py ├── animated_test.py ├── color_change_test.py ├── compare.py ├── first.py ├── hero1.py ├── polygons.py ├── robin_animation.png └── second.py ├── notes.txt ├── numpy_ecs ├── __init__.py ├── accessors.py ├── components.py ├── global_allocator.py └── table.py ├── planning.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | #vim 2 | *.swp 3 | 4 | #profiling 5 | ??_profile* 6 | *.prof 7 | *.orig 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *,cover 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # profiling 65 | *.prof 66 | 67 | # PyBuilder 68 | target/ 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Numpy-ECS 2 | ==================== 3 | 4 | Data oriented programming with numpy through an Entity-Component-System model. 5 | 6 | This project was inspired by lack luster performance of rotating a bunch of 7 | polygons in tests towards a game engine. The concept was inspired by: 8 | http://gamesfromwithin.com/data-oriented-design 9 | 10 | Further information on Data Oriented Design: 11 | http://www.dataorienteddesign.com/dodmain/dodmain.html 12 | 13 | Details 14 | ======= 15 | 16 | The Allocator is the user's interface with Components, Entities and Systems. 17 | 18 | Components are numpy arrays of the attributes that will make up Entities. 19 | They have a shape and a datatype and once given to the Allocator, they are 20 | abstracted away. ie, an RGB color Component might be: 21 | 22 | colors = Component('color', (3,), np.float32) 23 | 24 | Entities are "instances" defined through composition of Components. 25 | Each instance is actually just an integer (guid) that the Allocator can 26 | use to look up the instance's slice of the Components. Thus Entities are 27 | constructed by allocating values to some subset of Components in the Allocator. 28 | ie, instead of having a class with an `__init__` functions, instances are 29 | created by calling `allocator.add` with some component values defined: 30 | 31 | def add_regular_polygon(n_sides,radius,pos,velocity, 32 | color=(.5,.5,.5),allocator=allocator): 33 | pts = polyOfN(n_sides,radius) 34 | poly_verts = wind_vertices(pts) 35 | pos = position 36 | polygon = { 37 | 'render_verts': [(x+pos[0],y+pos[1],pos[2]) for x,y in poly_verts], 38 | 'color' : [color]*len(poly_verts), 39 | 'position' : pos, 40 | 'velocity' : velocity, 41 | } 42 | guid = allocator.add(polygon) 43 | return guid 44 | 45 | The allocator groups all the entity instances that are composed of the same 46 | Components into "entity classes" so their attributes can be accessed as 47 | continuous slices. 48 | 49 | Systems are functions that operate on the Components entity classes. 50 | These can be implemented through Numpy ufuncs, cython, or 51 | numba.vectorize. Thus, Systems can be fast and CPU multithreaded without 52 | multiprocessing. To apply velocity to every entity that has a velocity: 53 | 54 | def apply_velocity(velocities,positions,dt=1./600): 55 | positions *= velocities*dt 56 | 57 | And a System to render everything that has verts and colors: 58 | 59 | def update_display(render_verts,colors): 60 | gl.glClearColor(0.2, 0.4, 0.5, 1.0) 61 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 62 | gl.glEnable (gl.GL_BLEND) 63 | gl.glEnable (gl.GL_LINE_SMOOTH); 64 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY) 65 | gl.glEnableClientState(gl.GL_COLOR_ARRAY) 66 | 67 | n = len(render_verts[:]) 68 | #TODO verts._buffer.ctypes.data is awkward 69 | gl.glVertexPointer(3, vert_dtype.gl, 0, render_verts[:].ctypes.data) 70 | gl.glColorPointer(3, color_dtype.gl, 0, colors[:].ctypes.data) 71 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n) 72 | 73 | And without any convinience functions, this can be called with the appropriate 74 | sections of the `render_verts` and `color` numpy arrays by doing: 75 | 76 | get_sections = allocator.selectors_from_component_query 77 | draw =('render_verts','color') 78 | sections = get_sections(draw) 79 | update_display(*(sections[name] for name in draw)) 80 | 81 | TODO discuss Accessors which examples/hero1.py demonstrates. One can get an 82 | "instance" of an entity that allows access to the underlying arrays for a 83 | single item. 84 | 85 | Performance 86 | =========== 87 | 88 | The difference between `examples/first.py` and `examples/polygons.py` increases 89 | with number of polygons drawn. On my Celeron 2 CPU laptop I get 250 vs 500 90 | FPS with 150 polygons, and 30 vs 150 FPS with 1000 polygons. 91 | 92 | In a more complete game example with pymunk running 2D physics, the original 93 | version ran at 50 FPS and the Numpy-ECS version at 150 FPS. Having pyglet 94 | treat positions and angles as write-only arrays, and rendering treat them as 95 | read-only, the components were created in shared memory and multiprocessing was 96 | used to run the physics and rendering as seperate processes. The shared 97 | memory made up most of the inter process communication (IPC). The result was 98 | 180 FPS for the physics and 600 FPS for the rendering. 99 | 100 | examples: https://vimeo.com/65989831 https://vimeo.com/66736654 101 | 102 | History 103 | ======= 104 | 105 | The examples directory shows the evolution of this concept: 106 | 107 | first.py shows my best attempt at using pure python, OOP, and pyglet batches. 108 | Rotation of the polygons consumes 90% of the run time. 109 | 110 | compare.py shows that using numpy on an array has some overhead, but that 111 | the size of the array has much less of an effect on execution time than 112 | with python lists. There is a break even point in the length of data to 113 | be processed. Below this, list comprehensions are faster. Above this, 114 | numpy quickly surpasses pure Python. 115 | 116 | second.py shows that by batching all of the rotation math into one array, 117 | there are substantial performance benefits. But it becomes cumbersome 118 | to interact with instances of the polygons once they are all thrown 119 | together. 120 | 121 | polygons.py implements this same example through the ECS. The overhead 122 | from the higher level interface compared to `second.py` is negligable. 123 | 124 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | allow mask to define fields to exclude. ie: translate if position but NOT angle. 2 | define a __not__ prefix 3 | 4 | improve defragmenting 5 | concatenate contiguous slices 6 | cythonize 7 | 8 | make Systems easier to call 9 | 10 | Create Accessor objects from GUID for object oriented interface when desired. 11 | 12 | Make shared memory IPC and multiprocessing easier and give an example. 13 | -------------------------------------------------------------------------------- /examples/add-delete.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Numpy-ECS example of adding and deleting entitiess 3 | 4 | Hard coded example of the following Entity_class_id table: 5 | 6 | entity_class_id, vertices, color, positions, rotator, 7 | 8 | 1110, 1, 1, 1, 0, 9 | 1111, 1, 1, 1, 1, 10 | 11 | This file is part of Numpy-ECS. 12 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 13 | 14 | Numpy-ECS is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU General Public License as published by 16 | the Free Software Foundation, either version 2 of the License, or 17 | (at your option) any later version. 18 | 19 | Data Oriented Python is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License for more details. 23 | 24 | You should have received a copy of the GNU General Public License 25 | along with this program. If not, see . 26 | ''' 27 | import numpy as np 28 | from numpy import sin, cos, pi, sqrt 29 | from math import atan2 30 | import random 31 | import pyglet 32 | from pyglet import gl 33 | from collections import namedtuple 34 | from operator import add 35 | 36 | from numpy_ecs.global_allocator import GlobalAllocator 37 | from numpy_ecs.components import DefraggingArrayComponent as Component 38 | 39 | dtype_tuple = namedtuple('Dtype',('np','gl')) 40 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 41 | color_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 42 | 43 | counter_type = np.dtype([('max_val', np.float32), 44 | ('min_val', np.float32), 45 | ('interval', np.float32), 46 | ('accumulator',np.float32)]) 47 | 48 | 49 | allocator = GlobalAllocator((Component('render_verts' , (3,), vert_dtype.np ), 50 | Component('poly_verts' , (5,), color_dtype.np), 51 | Component('color' , (3,), color_dtype.np), 52 | Component('position' , (3,), vert_dtype.np ), 53 | Component('rotator' , (1,), counter_type )), 54 | 55 | allocation_scheme = ( 56 | (1,0,1,1,0), 57 | (1,1,1,1,1), 58 | ) 59 | ) 60 | def polyOfN(n,radius): 61 | '''helper function for making polygons''' 62 | r=radius 63 | if n < 3: 64 | n=3 65 | da = 2*pi/(n) #angle between divisions 66 | return [[r*cos(da*x),r*sin(da*x)] for x in range(int(n))] 67 | 68 | def wind_vertices(pts): 69 | l = len(pts) 70 | #wind up the vertices, so we don't have to do it when speed counts. 71 | #I dont really know which is clock wise and the other counter clock wise, btw 72 | cw = pts[:l//2] 73 | ccw = pts[l//2:][::-1] 74 | flatverts = [None]*(l) 75 | flatverts[::2]=ccw 76 | flatverts[1::2]=cw 77 | wound = [flatverts[0]]+flatverts+[flatverts[-1]] 78 | #prewound vertices can be transformed without care for winding. 79 | #Now, store the vertices in a way that can be translated as efficiently as possible later 80 | #construct list of (x,y,r, x_helper, y_helper) 81 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)] 82 | #lets call the initial angle of a vert alpha 83 | #so later, at theta, we want to remember cos(alpha) and sin(alpha) 84 | #they are the helper values 85 | return [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2), 86 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound] 87 | 88 | def add_rotating_regular_polygon(n_sides,radius,position,rate, 89 | color=(.5,.5,.5),allocator=allocator): 90 | rot_max = 4*np.pi 91 | rot_min = -rot_max 92 | pts = polyOfN(n_sides,radius) 93 | poly_verts = wind_vertices(pts) 94 | n = len(poly_verts) 95 | polygon = { 96 | 'poly_verts': poly_verts, 97 | 'render_verts': [(0,0,position[2])]*n, 98 | 'color':[color]*n, 99 | 'position':position, 100 | 'rotator':(rot_max,rot_min,rate,0) } 101 | allocator.add(polygon) 102 | 103 | def add_regular_polygon(n_sides,radius,position, 104 | color=(.5,.5,.5),allocator=allocator): 105 | pts = polyOfN(n_sides,radius) 106 | poly_verts = wind_vertices(pts) 107 | n = len(poly_verts) 108 | polygon = { 109 | 'render_verts': [(x+position[0],y+position[1],position[2]) for x,y,_,_,_ in poly_verts], 110 | 'color':[color]*n, 111 | 'position':position,} 112 | allocator.add(polygon) 113 | 114 | def update_rotator(rotator): 115 | arr=rotator 116 | span = arr['max_val'] - arr['min_val'] 117 | arr['accumulator'] += arr['interval'] 118 | underflow = arr['accumulator'] <= arr['min_val'] 119 | arr['accumulator'][underflow] += span[underflow] 120 | overflow = arr['accumulator'] >= arr['max_val'] 121 | arr['accumulator'][overflow] -= span[overflow] 122 | 123 | 124 | def update_render_verts(render_verts,poly_verts,positions,rotator,indices=[]): 125 | '''Update vertices to render based on positions and angles communicated 126 | through the data accessors''' 127 | 128 | angles = rotator['accumulator'] 129 | 130 | cos_ts, sin_ts = cos(angles), sin(angles) 131 | cos_ts -= 1 132 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied 133 | #and simplified. work it out on paper if you don't believe me. 134 | xs, ys, rs, xhelpers, yhelpers = (poly_verts[:,x] for x in range(5)) 135 | pts = render_verts 136 | 137 | #print 'shapes:',angles.shape 138 | pts[:,0] = xhelpers*cos_ts[indices] 139 | pts[:,1] = yhelpers*sin_ts[indices] 140 | pts[:,0] -= pts[:,1] 141 | pts[:,0] *= rs 142 | pts[:,0] += xs 143 | pts[:,0] += positions[indices,0] 144 | 145 | pts[:,1] = yhelpers*cos_ts[indices] 146 | tmp = xhelpers*sin_ts[indices] 147 | pts[:,1] += tmp 148 | pts[:,1] *= rs 149 | pts[:,1] += ys 150 | pts[:,1] += positions[indices,1] 151 | 152 | def update_display(render_verts,colors): 153 | gl.glClearColor(0.2, 0.4, 0.5, 1.0) 154 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 155 | gl.glEnable (gl.GL_BLEND) 156 | gl.glEnable (gl.GL_LINE_SMOOTH); 157 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY) 158 | gl.glEnableClientState(gl.GL_COLOR_ARRAY) 159 | 160 | n = len(render_verts[:]) 161 | #TODO verts._buffer.ctypes.data is awkward 162 | gl.glVertexPointer(3, vert_dtype.gl, 0, render_verts[:].ctypes.data) 163 | gl.glColorPointer(3, color_dtype.gl, 0, colors[:].ctypes.data) 164 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n) 165 | 166 | 167 | def add_some(n,n2=None,allocator=allocator): 168 | '''add n random polygons to allocator 169 | 170 | There's two types of polygons. if only n is given, will distribute randomly. 171 | if n2 is given, distibute n to one type and n2 to the other 172 | ''' 173 | 174 | assert isinstance(n,int) and (1 if n2 is None else isinstance(n2,int)),\ 175 | "n1 and n2 (if given) must be integers" 176 | 177 | if n2 is None: 178 | a = random.random() 179 | n1 = int(n*a) 180 | n2 = n-n1 181 | else: 182 | n1 = n 183 | n2 = 0 184 | 185 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n1,3))] 186 | rs = [r*50 for r in np.random.random(n1)] 187 | ns = [int(m*10)+3 for m in np.random.random(n1)] 188 | colors = np.random.random((n1,3)).astype(color_dtype.np) 189 | rates = np.random.random(n1)*.01 190 | 191 | for n,r, position, color, rate in zip(ns,rs, positions, colors, rates): 192 | add_rotating_regular_polygon(n,r,position,rate,color) 193 | #from before indicies could be calculated 194 | #indices = np.array(reduce(add, [[x,]*7 for x in range(n1)], []),dtype=np.int) 195 | 196 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n2,3))] 197 | rs = [r*50 for r in np.random.random(n2)] 198 | ns = [int(m*10)+3 for m in np.random.random(n2)] 199 | colors = np.random.random((n2,3)).astype(color_dtype.np) 200 | 201 | for n_sides,radius, position, color in zip(ns,rs, positions, colors): 202 | add_regular_polygon(n_sides,radius,position,color) 203 | 204 | 205 | 206 | def delete_some(n,allocator=allocator): 207 | guids = random.sample(allocator.guids,n) 208 | for guid in guids: 209 | allocator.delete(guid) 210 | 211 | 212 | 213 | if __name__ == '__main__': 214 | 215 | width, height = 640,480 216 | window = pyglet.window.Window(width, height,vsync=False) 217 | #window = pyglet.window.Window(fullscreen=True,vsync=False) 218 | #width = window.width 219 | #height = window.height 220 | fps_display = pyglet.clock.ClockDisplay() 221 | text = """Numpy ECS""" 222 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10) 223 | 224 | #add some polygons 225 | add_some(100,50) 226 | allocator._defrag() 227 | 228 | get_sections = allocator.selectors_from_component_query 229 | 230 | @window.event 231 | def on_draw(): 232 | window.clear() 233 | 234 | allocator._defrag() 235 | 236 | rotator = ('rotator',) 237 | sections = get_sections(rotator) 238 | update_rotator(*(sections[name] for name in rotator)) 239 | 240 | render_verts =('render_verts','poly_verts','position','rotator') 241 | broadcast = ('position__to__poly_verts',) 242 | sections = get_sections(render_verts + broadcast) 243 | indices = sections.pop(broadcast[0]) 244 | update_render_verts(*(sections[name] for name in render_verts),indices=indices) 245 | 246 | draw =('render_verts','color') 247 | sections = get_sections(draw) 248 | update_display(*(sections[name] for name in draw)) 249 | 250 | fps_display.draw() 251 | 252 | pyglet.clock.schedule(lambda _: None) 253 | pyglet.clock.schedule_interval(lambda x,*y: add_some(*y),1,2) 254 | pyglet.clock.schedule_interval(lambda x,*y: delete_some(*y),2,4) 255 | pyglet.app.run() 256 | 257 | 258 | -------------------------------------------------------------------------------- /examples/animated_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Numpy-ECS example of animated sprites 3 | 4 | This file is part of Numpy-ECS. 5 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 6 | 7 | Numpy-ECS is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 2 of the License, or 10 | (at your option) any later version. 11 | 12 | Data Oriented Python is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ''' 20 | import numpy as np 21 | from numpy import sin, cos, pi, sqrt 22 | from math import atan2 23 | import pyglet 24 | from pyglet import gl 25 | from collections import namedtuple 26 | from operator import add 27 | 28 | from numpy_ecs.global_allocator import GlobalAllocator 29 | from numpy_ecs.components import DefraggingArrayComponent as Component 30 | 31 | #for reproduceable output 32 | seed = 123456789 33 | np.random.seed(seed) 34 | 35 | window_width, window_height = 640,480 36 | 37 | dtype_tuple = namedtuple('Dtype',('np','gl')) 38 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 39 | tex_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 40 | 41 | animator_type = np.dtype([('min_val', np.int32), 42 | ('max_val', np.int32), 43 | ('interval', np.int32), 44 | ('accumulator',np.int32)]) 45 | 46 | 47 | allocator = GlobalAllocator((Component('render_verts' , (3,), vert_dtype.np ), 48 | Component('tex_coords' , (3,), tex_dtype.np ), 49 | Component('poly_verts' , (5,), vert_dtype.np ), 50 | Component('position' , (3,), vert_dtype.np ), 51 | Component('velocity' , (1,), vert_dtype.np ), 52 | Component('animator' , (1,), animator_type )), 53 | 54 | allocation_scheme = ( 55 | (1,1,1,1,1,1), 56 | ) 57 | ) 58 | def wind_pts(pts,reshape=False): 59 | '''take clockwise or counter clockwise points and wind them as would 60 | be needed for rendering as triangle strips''' 61 | if reshape: 62 | pts = zip(pts[::3],pts[1::3],pts[2::3]) 63 | l = len(pts) 64 | cw = pts[:l//2] 65 | ccw = pts[l//2:][::-1] 66 | flatverts = [None]*(l) 67 | flatverts[::2]=ccw 68 | flatverts[1::2]=cw 69 | return [flatverts[0]]+flatverts+[flatverts[-1]] 70 | 71 | image_name='robin_animation.png' 72 | rows, cols = 5,5 73 | raw = pyglet.image.load(image_name) 74 | raw_seq = pyglet.image.ImageGrid(raw, rows, cols) 75 | items = raw_seq.get_texture_sequence().items[:] 76 | #select animation in order 77 | items = items[20:]+items[15:20]+items[10:15]+items[5:10]+items[0:1] 78 | bird_texture = items[0].texture 79 | animation_lookup = np.array([wind_pts(x.tex_coords,reshape=True) for x in items]) 80 | 81 | def wind_vertices(pts): 82 | '''wind pts and pre-compute data that is helpful for transformations''' 83 | wound = wind_pts(pts) 84 | #prewound vertices can be transformed without care for winding. 85 | #Now, store the vertices in a way that can be translated as efficiently as possible later 86 | #construct list of (x,y,r, x_helper, y_helper) 87 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)] 88 | #lets call the initial angle of a vert alpha 89 | #so later, at theta, we want to remember cos(alpha) and sin(alpha) 90 | #they are the helper values 91 | return [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2), 92 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound] 93 | 94 | def add_sprite(width,height,position,rate,anim_start=0, first_frame=0, 95 | anim_stop=1, anim_step=1, allocator=allocator): 96 | width /= 2. 97 | height /= 2. 98 | pts = ( (-width, -height), ( width, -height), 99 | ( width, height), (-width, height), ) 100 | 101 | poly_verts = wind_vertices(pts) 102 | 103 | n = len(poly_verts) 104 | polygon = { 105 | 'poly_verts': poly_verts, 106 | 'tex_coords': [(0,0,0)]*n, #allocate empty space. system will fill 107 | 'render_verts': [(0,0,position[2])]*n, 108 | 'velocity': rate, 109 | 'position': position, 110 | 'animator': (anim_start,anim_stop,anim_step,first_frame) } 111 | allocator.add(polygon) 112 | 113 | 114 | def update_position(velocity,position): 115 | global window_width 116 | position[:,0] -= velocity 117 | #wrap around 118 | position[:,0][position[:,0] < -20] = window_width + 20 119 | 120 | 121 | def update_animation(tex_coords,animator,animation_lookup=animation_lookup): 122 | arr = animator 123 | span = arr['max_val'] - arr['min_val'] 124 | arr['accumulator'] += arr['interval'] 125 | underflow = arr['accumulator'] <= arr['min_val'] 126 | arr['accumulator'][underflow] += span[underflow] 127 | overflow = arr['accumulator'] >= arr['max_val'] 128 | arr['accumulator'][overflow] -= span[overflow] 129 | 130 | temp = animation_lookup[arr['accumulator']] 131 | temp.shape = (temp.shape[0]*temp.shape[1],temp.shape[2]) 132 | tex_coords[:] = temp 133 | 134 | 135 | def update_render_verts(render_verts,poly_verts,positions,indices=[]): 136 | '''Update vertices to render based on positions''' 137 | 138 | render_verts[:,:2] = poly_verts[:,:2] 139 | render_verts[:,:2] += positions[:,:2][indices] 140 | #render_verts[:,2] = positions[indices][:,2] 141 | 142 | def update_display(verts,tex_coords,texture=bird_texture): 143 | gl.glClearColor(0.2, 0.4, 0.5, 1.0) 144 | 145 | gl.glEnable(texture.target) 146 | gl.glBindTexture(texture.target, texture.id) 147 | 148 | gl.glPushAttrib(gl.GL_COLOR_BUFFER_BIT) 149 | 150 | gl.glEnable(gl.GL_ALPHA_TEST) 151 | gl.glAlphaFunc (gl.GL_GREATER, .1) 152 | #gl.glEnable(gl.GL_BLEND) 153 | #gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 154 | gl.glEnable(gl.GL_DEPTH_TEST) 155 | 156 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY) 157 | gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY) 158 | 159 | n=len(verts[:]) 160 | #TODO verts._buffer.ctypes.data is awkward 161 | gl.glVertexPointer(3, vert_dtype.gl, 0, verts[:].ctypes.data) 162 | gl.glTexCoordPointer(3, tex_dtype.gl, 0, tex_coords[:].ctypes.data) 163 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n) 164 | #unset state 165 | gl.glPopAttrib() 166 | gl.glDisable(texture.target) 167 | 168 | 169 | if __name__ == '__main__': 170 | 171 | window = pyglet.window.Window(window_width, window_height,vsync=False) 172 | #window = pyglet.window.Window(fullscreen=True,vsync=False) 173 | #width = window.width 174 | #height = window.height 175 | fps_display = pyglet.clock.ClockDisplay() 176 | text = """Numpy ECS""" 177 | label = pyglet.text.HTMLLabel(text, x=10, y = window_height-10) 178 | 179 | n=50 180 | 181 | positions = np.random.random((n,3)) 182 | positions[:,0] *= window_width 183 | positions[:,1] *= window_height 184 | sizes = (positions[:,2])*200 185 | anim_starts = np.random.random_integers(0,20,size=n) 186 | 187 | for size, position, start in zip(sizes, positions, anim_starts): 188 | add_sprite(size,size,position,size*.001,first_frame=start,anim_stop=20) 189 | 190 | allocator._defrag() 191 | 192 | get_sections = allocator.selectors_from_component_query 193 | 194 | def apply_animation(this): 195 | animation =('tex_coords','animator') 196 | sections = get_sections(animation) 197 | update_animation(*(sections[name] for name in animation)) 198 | 199 | @window.event 200 | def on_draw(): 201 | window.clear() 202 | 203 | mover = ('velocity','position',) 204 | sections = get_sections(mover) 205 | update_position(*(sections[name] for name in mover)) 206 | 207 | render_verts =('render_verts','poly_verts','position') 208 | broadcast = ('position__to__poly_verts',) 209 | sections = get_sections(render_verts + broadcast) 210 | indices = sections.pop(broadcast[0]) 211 | update_render_verts(*(sections[name] for name in render_verts),indices=indices) 212 | 213 | draw =('render_verts','tex_coords') 214 | sections = get_sections(draw) 215 | update_display(*(sections[name] for name in draw)) 216 | 217 | fps_display.draw() 218 | 219 | pyglet.clock.schedule(lambda _: None) 220 | pyglet.clock.schedule_interval(apply_animation,.05) 221 | 222 | pyglet.app.run() 223 | 224 | 225 | -------------------------------------------------------------------------------- /examples/color_change_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Numpy-ECS example implementing rotation and color changing 3 | 4 | Hard coded example of the following Entity_class_id table: 5 | 6 | entity_class_id, vertices, color, positions, rotator, color_changer 7 | 8 | 11100, 1, 1, 1, 0, 0 9 | 11110, 1, 1, 1, 1, 0 10 | 11111, 1, 1, 1, 1, 1 11 | 11101, 1, 1, 1, 0, 1 12 | 13 | This file is part of Numpy-ECS. 14 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 15 | 16 | Data Oreinted Python is free software: you can redistribute it and/or modify 17 | it under the terms of the GNU General Public License as published by 18 | the Free Software Foundation, either version 2 of the License, or 19 | (at your option) any later version. 20 | 21 | Data Oriented Python is distributed in the hope that it will be useful, 22 | but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | GNU General Public License for more details. 25 | 26 | You should have received a copy of the GNU General Public License 27 | along with this program. If not, see . 28 | 29 | ''' 30 | 31 | import numpy as np 32 | from numpy import sin, cos, pi, sqrt 33 | from math import atan2 34 | import pyglet 35 | from pyglet import gl 36 | from collections import namedtuple 37 | from operator import add 38 | 39 | from numpy_ecs.global_allocator import GlobalAllocator 40 | from numpy_ecs.components import DefraggingArrayComponent as Component 41 | 42 | dtype_tuple = namedtuple('Dtype',('np','gl')) 43 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 44 | color_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 45 | 46 | counter_type = np.dtype([('max_val', np.float32), 47 | ('min_val', np.float32), 48 | ('interval', np.float32), 49 | ('accumulator',np.float32)]) 50 | 51 | color_type = np.dtype([(c,color_dtype.np) for c in "RGB"]) 52 | 53 | color_changer_type = np.dtype([('max_val', np.float32), 54 | ('min_val', np.float32), 55 | ('interval', np.float32), 56 | ('accumulator',np.float32), 57 | ('color', color_type)]) 58 | 59 | 60 | allocator = GlobalAllocator((Component('render_verts' , (3,), vert_dtype.np ), 61 | Component('poly_verts' , (5,), color_dtype.np), 62 | Component('color' , (3,), color_dtype.np), 63 | Component('position' , (3,), vert_dtype.np ), 64 | Component('rotator' , (1,), counter_type ), 65 | Component('color_changer', (1,), color_changer_type)), 66 | 67 | allocation_scheme = ( 68 | (1,0,1,1,0,0), 69 | (1,1,1,1,1,0), 70 | (1,1,1,1,1,1), 71 | (1,0,1,1,0,1), 72 | ) 73 | ) 74 | def polyOfN(n,radius): 75 | '''helper function for making polygons''' 76 | r=radius 77 | if n < 3: 78 | n=3 79 | da = 2*pi/(n) #angle between divisions 80 | return [[r*cos(da*x),r*sin(da*x)] for x in range(int(n))] 81 | 82 | def wind_vertices(pts): 83 | l = len(pts) 84 | #wind up the vertices, so we don't have to do it when speed counts. 85 | #I dont really know which is clock wise and the other counter clock wise, btw 86 | cw = pts[:l//2] 87 | ccw = pts[l//2:][::-1] 88 | flatverts = [None]*(l) 89 | flatverts[::2]=ccw 90 | flatverts[1::2]=cw 91 | wound = [flatverts[0]]+flatverts+[flatverts[-1]] 92 | #prewound vertices can be transformed without care for winding. 93 | #Now, store the vertices in a way that can be translated as efficiently as possible later 94 | #construct list of (x,y,r, x_helper, y_helper) 95 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)] 96 | #lets call the initial angle of a vert alpha 97 | #so later, at theta, we want to remember cos(alpha) and sin(alpha) 98 | #they are the helper values 99 | return [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2), 100 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound] 101 | 102 | def add_rotating_regular_polygon(n_sides,radius,position,rate, 103 | color=(.5,.5,.5),allocator=allocator): 104 | rot_max = 4*np.pi 105 | rot_min = -rot_max 106 | pts = polyOfN(n_sides,radius) 107 | poly_verts = wind_vertices(pts) 108 | n = len(poly_verts) 109 | polygon = { 110 | 'poly_verts': poly_verts, 111 | 'render_verts': [(0,0,position[2])]*n, 112 | 'color':[color]*n, 113 | 'position':position, 114 | 'rotator':(rot_max,rot_min,rate,0) } 115 | allocator.add(polygon) 116 | 117 | def add_rotating_color_changing_polygon(n_sides,radius,position,rate, 118 | color=(.5,.5,.5),allocator=allocator): 119 | rot_max = 4*np.pi 120 | rot_min = -rot_max 121 | pts = polyOfN(n_sides,radius) 122 | poly_verts = wind_vertices(pts) 123 | n = len(poly_verts) 124 | polygon = { 125 | 'poly_verts': poly_verts, 126 | 'render_verts': [(0,0,position[2])]*n, 127 | 'color':[color]*n, 128 | 'position':position, 129 | 'rotator':(rot_max,rot_min,rate,0), 130 | 'color_changer':(1,0,rate/10,1,color)} 131 | 132 | allocator.add(polygon) 133 | 134 | def add_regular_polygon(n_sides,radius,position, 135 | color=(.5,.5,.5),allocator=allocator): 136 | pts = polyOfN(n_sides,radius) 137 | poly_verts = wind_vertices(pts) 138 | n = len(poly_verts) 139 | polygon = { 140 | 'render_verts': [(x+position[0],y+position[1],position[2]) for x,y,_,_,_ in poly_verts], 141 | 'color':[color]*n, 142 | 'position':position,} 143 | allocator.add(polygon) 144 | 145 | def update_rotator(rotator): 146 | arr=rotator 147 | span = arr['max_val'] - arr['min_val'] 148 | arr['accumulator'] += arr['interval'] 149 | underflow = arr['accumulator'] <= arr['min_val'] 150 | arr['accumulator'][underflow] += span[underflow] 151 | overflow = arr['accumulator'] >= arr['max_val'] 152 | arr['accumulator'][overflow] -= span[overflow] 153 | 154 | def update_colors(color,color_changer,indices=[]): 155 | accumulator = color_changer['accumulator'] 156 | interval = color_changer['interval'] 157 | #ready = color_changer['ready_flag'] 158 | poly_color = color_changer['color'] 159 | max_value = color_changer['max_val'] 160 | min_value = color_changer['min_val'] 161 | 162 | 163 | accumulator += interval 164 | 165 | underflow = accumulator < min_value #must not be <= 166 | #ready[underflow] = True 167 | accumulator[underflow] = max_value[underflow] 168 | 169 | overflow = accumulator > max_value #must be >, not >= 170 | #ready[overflow] = True 171 | accumulator[overflow] = min_value[overflow] 172 | 173 | #TODO using record arrays is not compatible with pure multi dim arrays 174 | # whats a better idiom? 175 | R = poly_color['R'] * accumulator 176 | G = poly_color['G'] * accumulator 177 | B = poly_color['B'] * accumulator 178 | color[:,0] = R[indices] 179 | color[:,1] = G[indices] 180 | color[:,2] = B[indices] 181 | 182 | def update_render_verts(render_verts,poly_verts,positions,rotator,indices=[]): 183 | '''Update vertices to render based on positions and angles communicated 184 | through the data accessors''' 185 | 186 | angles = rotator['accumulator'] 187 | 188 | cos_ts, sin_ts = cos(angles), sin(angles) 189 | cos_ts -= 1 190 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied 191 | #and simplified. work it out on paper if you don't believe me. 192 | xs, ys, rs, xhelpers, yhelpers = (poly_verts[:,x] for x in range(5)) 193 | pts = render_verts 194 | 195 | #print 'shapes:',angles.shape 196 | pts[:,0] = xhelpers*cos_ts[indices] 197 | pts[:,1] = yhelpers*sin_ts[indices] 198 | pts[:,0] -= pts[:,1] 199 | pts[:,0] *= rs 200 | pts[:,0] += xs 201 | pts[:,0] += positions[indices,0] 202 | 203 | pts[:,1] = yhelpers*cos_ts[indices] 204 | tmp = xhelpers*sin_ts[indices] 205 | pts[:,1] += tmp 206 | pts[:,1] *= rs 207 | pts[:,1] += ys 208 | pts[:,1] += positions[indices,1] 209 | 210 | def update_display(render_verts,colors): 211 | gl.glClearColor(0.2, 0.4, 0.5, 1.0) 212 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 213 | gl.glEnable (gl.GL_BLEND) 214 | gl.glEnable (gl.GL_LINE_SMOOTH); 215 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY) 216 | gl.glEnableClientState(gl.GL_COLOR_ARRAY) 217 | 218 | n = len(render_verts[:]) 219 | #TODO verts._buffer.ctypes.data is awkward 220 | gl.glVertexPointer(3, vert_dtype.gl, 0, render_verts[:].ctypes.data) 221 | gl.glColorPointer(3, color_dtype.gl, 0, colors[:].ctypes.data) 222 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n) 223 | 224 | if __name__ == '__main__': 225 | 226 | 227 | ######################## 228 | # 229 | # Instatiate pyglet window 230 | # 231 | ####################### 232 | 233 | width, height = 640,480 234 | window = pyglet.window.Window(width, height,vsync=False) 235 | #window = pyglet.window.Window(fullscreen=True,vsync=False) 236 | #width = window.width 237 | #height = window.height 238 | fps_display = pyglet.clock.ClockDisplay() 239 | text = """Numpy ECS""" 240 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10) 241 | 242 | ######################## 243 | # 244 | # create polygons 245 | # 246 | ####################### 247 | n1=50 248 | 249 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n1,3))] 250 | rs = [r*50 for r in np.random.random(n1)] 251 | ns = [int(m*10)+3 for m in np.random.random(n1)] 252 | colors = np.random.random((n1,3)).astype(color_dtype.np) 253 | rates = np.random.random(n1)*.01 254 | 255 | for n,r, position, color, rate in zip(ns,rs, positions, colors, rates): 256 | add_rotating_regular_polygon(n,r,position,rate,color) 257 | 258 | n1=50 259 | 260 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n1,3))] 261 | rs = [r*50 for r in np.random.random(n1)] 262 | ns = [int(m*10)+3 for m in np.random.random(n1)] 263 | colors = np.random.random((n1,3)).astype(color_dtype.np) 264 | rates = np.random.random(n1)*.01 265 | 266 | for n,r, position, color, rate in zip(ns,rs, positions, colors, rates): 267 | add_rotating_color_changing_polygon(n,r,position,rate,color) 268 | 269 | n2=50 270 | 271 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n2,3))] 272 | rs = [r*50 for r in np.random.random(n2)] 273 | ns = [int(m*10)+3 for m in np.random.random(n2)] 274 | colors = np.random.random((n2,3)).astype(color_dtype.np) 275 | 276 | for n_sides,radius, position, color in zip(ns,rs, positions, colors): 277 | add_regular_polygon(n_sides,radius,position,color) 278 | 279 | allocator._defrag() 280 | 281 | ######################## 282 | # 283 | # Run game loop forever 284 | # 285 | ####################### 286 | get_sections = allocator.selectors_from_component_query 287 | 288 | @window.event 289 | def on_draw(): 290 | window.clear() 291 | 292 | rotator = ('rotator',) 293 | sections = get_sections(rotator) 294 | update_rotator(*(sections[name] for name in rotator)) 295 | 296 | color_changer =('color','color_changer') 297 | broadcast = ('color_changer__to__color',) 298 | sections = get_sections(color_changer + broadcast) 299 | indices = sections.pop(broadcast[0]) 300 | update_colors(*(sections[name] for name in color_changer),indices=indices) 301 | 302 | render_verts =('render_verts','poly_verts','position','rotator') 303 | broadcast = ('position__to__poly_verts',) 304 | sections = get_sections(render_verts + broadcast) 305 | indices = sections.pop(broadcast[0]) 306 | update_render_verts(*(sections[name] for name in render_verts),indices=indices) 307 | 308 | draw =('render_verts','color') 309 | sections = get_sections(draw) 310 | update_display(*(sections[name] for name in draw)) 311 | 312 | fps_display.draw() 313 | 314 | pyglet.clock.schedule(lambda _: None) 315 | 316 | pyglet.app.run() 317 | 318 | 319 | -------------------------------------------------------------------------------- /examples/compare.py: -------------------------------------------------------------------------------- 1 | ''' 2 | quick test to compare numpy math vs list comprehensions as a function 3 | of size of data. 4 | 5 | This file is part of Data Oriented Python. 6 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 7 | 8 | Data Oreinted Python is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | Data Oriented Python is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | ''' 22 | import numpy as np 23 | from math import pi, sin, cos,atan2,sqrt 24 | import time 25 | 26 | def gen_initiald(n): 27 | 28 | pts = np.random.random((n,2)).astype(np.float32) 29 | list_data = [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2),cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in pts] 30 | array_data = np.array(list_data) 31 | return list_data, array_data 32 | 33 | def list_rotate(initiald): 34 | px, py = (10,10) 35 | cost, sint = cos(.5), sin(.5) 36 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied 37 | #and simplified. work it out on paper if you don't believe me. 38 | pts = [(px+x+r*(xhelper*(cost-1)-sint*yhelper),py+y+r*(yhelper*(cost-1)+sint*xhelper)) for x,y,r,xhelper,yhelper in initiald] 39 | #flatten and return as a tuple for vertbuffer 40 | return tuple(map(int,[val for subl in pts for val in subl])) 41 | 42 | def array_rotate(initiald): 43 | px, py = (0,0) 44 | cost, sint = cos(.5), sin(.5) 45 | xs, ys, rs, xhelpers, yhelpers = (initiald[:,x] for x in range(5)) 46 | 47 | pts = np.empty((len(xs),2),dtype=np.float32) 48 | 49 | pts[:,0] = xhelpers*(cost-1) 50 | pts[:,1] = yhelpers*sint 51 | pts[:,0] -= pts[:,1] 52 | pts[:,0] *= rs 53 | pts[:,0] += xs 54 | pts[:,0] += px 55 | 56 | pts[:,1] = yhelpers*(cost-1) 57 | tmp = xhelpers*sint 58 | pts[:,1] += tmp 59 | pts[:,1] *= rs 60 | pts[:,1] += ys 61 | pts[:,1] += py 62 | 63 | #flatten and return as a tuple for vertbuffer 64 | #return tuple(map(int,[val for subl in pts for val in subl])) 65 | pts.shape = ( reduce(lambda xx,yy: xx*yy, pts.shape), ) 66 | return pts.astype(np.int32) 67 | 68 | for n in [5,10,25,50,100,500,1000]: 69 | lst,arr = gen_initiald(n) 70 | 71 | start = time.time() 72 | trash = list_rotate(lst) 73 | end = time.time() 74 | print "did list of %s in %s" % (n, end-start) 75 | 76 | start = time.time() 77 | trash = array_rotate(arr) 78 | end = time.time() 79 | print "did array of %s in %s" % (n, end-start) 80 | 81 | print "\n" 82 | -------------------------------------------------------------------------------- /examples/first.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Example using batches instead of Numpy-ECS 3 | 4 | Heavy math happens on each object one at a time. 5 | 6 | (object oriented rather than data oriented). 7 | 8 | This file is part of Numpy-ECS. 9 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 10 | 11 | Numpy-ECS is free software: you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation, either version 2 of the License, or 14 | (at your option) any later version. 15 | 16 | Data Oriented Python is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | 21 | You should have received a copy of the GNU General Public License 22 | along with this program. If not, see . 23 | 24 | ''' 25 | 26 | 27 | from __future__ import absolute_import, division, print_function, unicode_literals 28 | import numpy as np 29 | import pyglet 30 | from pyglet import gl 31 | from pyglet.graphics import Batch 32 | from math import pi, sin, cos,atan2,sqrt 33 | import time 34 | 35 | #Limit run time for profiling 36 | run_for = 15 #seconds to run test for 37 | def done_yet(duration = run_for, start=time.time()): 38 | return time.time()-start > duration 39 | 40 | #Set up window 41 | width, height = 640,480 42 | window = pyglet.window.Window(width, height,vsync=False) 43 | #window = pyglet.window.Window(fullscreen=True,vsync=False) 44 | #width = window.width 45 | #height = window.height 46 | fps_display = pyglet.clock.ClockDisplay() 47 | text = """Unoptimized""" 48 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10) 49 | 50 | main_batch = Batch() 51 | 52 | def draw(): 53 | global main_batch 54 | gl.glClearColor(0.2, 0.4, 0.5, 1.0) 55 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 56 | gl.glEnable (gl.GL_BLEND) 57 | gl.glEnable (gl.GL_LINE_SMOOTH); 58 | gl.glLineWidth (3) 59 | 60 | main_batch.draw() 61 | 62 | #helper hunction for making polygons 63 | def polyOfN(radius,n): 64 | r=radius 65 | if n < 3: 66 | n=3 67 | da = 2*pi/(n) #angle between divisions 68 | return [[r*cos(da*x),r*sin(da*x)] for x in range(n)] 69 | 70 | 71 | class Convex(object): 72 | '''Convex polygons for rendering''' 73 | global main_batch 74 | 75 | def __init__(self,pts, position=None, color=None, radius=0): 76 | if position is None: 77 | position = (width//2, height//2) 78 | self.position = position 79 | if color is None: 80 | color = [60,0,0] 81 | self.color = color 82 | self.pts=pts 83 | self.initializeVertices() 84 | 85 | def initializeVertices(self): 86 | pts = self.pts 87 | l = len(pts) 88 | #wind up the vertices, so we don't have to do it when speed counts. 89 | #I dont really know which is clock wise and the other counter clock wise, btw 90 | cw = pts[:l//2] 91 | ccw = pts[l//2:][::-1] 92 | flatverts = [None]*(l) 93 | flatverts[::2]=ccw 94 | flatverts[1::2]=cw 95 | wound = [flatverts[0]]+flatverts+[flatverts[-1]] 96 | #prewound vertices can be transformed without care for winding. 97 | #Now, store the vertices in a way that can be translated as efficiently as possible later 98 | #construct list of (x,y,r, x_helper, y_helper) 99 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)] 100 | #lets call the initial angle of a vert alpha 101 | #so later, at theta, we want to remember cos(alpha) and sin(alpha) 102 | #they are the helper values 103 | self.initial_data = [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2),cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound] 104 | verts = self.rotate(0) 105 | self.n=len(verts)//2 106 | self.set_colors() 107 | self.vertlist = main_batch.add(self.n, gl.GL_TRIANGLE_STRIP,None, 108 | ('v2i,',verts),('c3b',self.colors)) 109 | 110 | def set_colors(self): 111 | self.colors = self.color*self.n 112 | 113 | def rotate(self,theta): 114 | px, py = self.position 115 | initiald = self.initial_data 116 | cost, sint = cos(theta), sin(theta) 117 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied 118 | #and simplified. work it out on paper if you don't believe me. 119 | pts = [(px+x+r*(xhelper*(cost-1)-sint*yhelper),py+y+r*(yhelper*(cost-1)+sint*xhelper)) for x,y,r,xhelper,yhelper in initiald] 120 | #flatten and return as a tuple for vertbuffer 121 | return tuple(map(int,[val for subl in pts for val in subl])) 122 | 123 | def rotate_and_render(self,angle): 124 | self.vertlist.vertices = self.rotate(angle) 125 | 126 | def update_colors(self): 127 | ##print(self.vertlist.colors) 128 | self.vertlist.colors=self.colors 129 | 130 | 131 | n=1000 132 | positions = [(x*width,y*height) for x,y in np.random.random((n,2))] 133 | poly_args = [(r*50,int(m*10)+3) for r,m in np.random.random((n,2))] 134 | colors = [map(lambda x: int(x*255),vals) for vals in np.random.random((n,3))] 135 | 136 | ents = [Convex(polyOfN(*pargs),position=pos, color=col) for pargs,pos,col in zip(poly_args,positions,colors)] 137 | 138 | angles= [0]*n 139 | rates = list(np.random.random(n)*.02) 140 | @window.event 141 | def on_draw(): 142 | global angle 143 | 144 | if done_yet(): 145 | pyglet.app.exit() 146 | 147 | window.clear() 148 | for i, ent in enumerate(ents): 149 | angles[i]+=rates[i] 150 | ent.rotate_and_render(angles[i]) 151 | draw() 152 | fps_display.draw() 153 | 154 | 155 | #pyglet.clock.set_fps_limit(60) 156 | pyglet.clock.schedule(lambda _: None) 157 | 158 | pyglet.app.run() 159 | 160 | 161 | -------------------------------------------------------------------------------- /examples/hero1.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Numpy-ECS example of using an Accessor to manipulate an entity instance 3 | in an object oriented manner 4 | 5 | Hard coded example of the following Entity_class_id table: 6 | 7 | entity_class_id, vertices, color, positions, rotator, 8 | 9 | 1110, 1, 1, 1, 0, 10 | 1111, 1, 1, 1, 1, 11 | 12 | This file is part of Numpy-ECS. 13 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 14 | 15 | Numpy-ECS is free software: you can redistribute it and/or modify 16 | it under the terms of the GNU General Public License as published by 17 | the Free Software Foundation, either version 2 of the License, or 18 | (at your option) any later version. 19 | 20 | Data Oriented Python is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU General Public License for more details. 24 | 25 | You should have received a copy of the GNU General Public License 26 | along with this program. If not, see . 27 | ''' 28 | from __future__ import absolute_import, division, print_function 29 | #python version compatability 30 | import sys 31 | if sys.version_info < (3,0): 32 | from future_builtins import zip, map 33 | import numpy as np 34 | from numpy import sin, cos, pi, sqrt 35 | from math import atan2 36 | import pyglet 37 | from pyglet.window.key import LEFT, RIGHT, UP, DOWN 38 | from pyglet import gl 39 | from collections import namedtuple 40 | from operator import add 41 | from numpy_ecs.global_allocator import GlobalAllocator 42 | from numpy_ecs.components import DefraggingArrayComponent as Component 43 | 44 | #for reproduceable output 45 | seed = 123456789 46 | np.random.seed(seed) 47 | 48 | dtype_tuple = namedtuple('Dtype',('np','gl')) 49 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 50 | color_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 51 | 52 | counter_type = np.dtype([('max_val', np.float32), 53 | ('min_val', np.float32), 54 | ('interval', np.float32), 55 | ('accumulator',np.float32)]) 56 | 57 | 58 | allocator = GlobalAllocator((Component('render_verts' , (3,), vert_dtype.np ), 59 | Component('poly_verts' , (5,), color_dtype.np), 60 | Component('color' , (3,), color_dtype.np), 61 | Component('position' , (3,), vert_dtype.np ), 62 | Component('rotator' , (1,), counter_type )), 63 | 64 | allocation_scheme = ( 65 | (1,0,1,1,0), 66 | (1,1,1,1,1), 67 | ) 68 | ) 69 | def polyOfN(n,radius): 70 | '''helper function for making polygons''' 71 | r=radius 72 | if n < 3: 73 | n=3 74 | da = 2*pi/(n) #angle between divisions 75 | return [[r*cos(da*x),r*sin(da*x)] for x in range(int(n))] 76 | 77 | def wind_vertices(pts): 78 | l = len(pts) 79 | #wind up the vertices, so we don't have to do it when speed counts. 80 | #I dont really know which is clock wise and the other counter clock wise, btw 81 | cw = pts[:l//2] 82 | ccw = pts[l//2:][::-1] 83 | flatverts = [None]*(l) 84 | flatverts[::2]=ccw 85 | flatverts[1::2]=cw 86 | wound = [flatverts[0]]+flatverts+[flatverts[-1]] 87 | #prewound vertices can be transformed without care for winding. 88 | #Now, store the vertices in a way that can be translated as efficiently as possible later 89 | #construct list of (x,y,r, x_helper, y_helper) 90 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)] 91 | #lets call the initial angle of a vert alpha 92 | #so later, at theta, we want to remember cos(alpha) and sin(alpha) 93 | #they are the helper values 94 | return [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2), 95 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound] 96 | 97 | def add_rotating_regular_polygon(n_sides,radius,position,rate, 98 | color=(.5,.5,.5),allocator=allocator): 99 | rot_max = 4*np.pi 100 | rot_min = -rot_max 101 | pts = polyOfN(n_sides,radius) 102 | poly_verts = wind_vertices(pts) 103 | n = len(poly_verts) 104 | polygon = { 105 | 'poly_verts': poly_verts, 106 | 'render_verts': [(0,0,position[2])]*n, 107 | 'color':[color]*n, 108 | 'position':position, 109 | 'rotator':(rot_max,rot_min,rate,0) } 110 | guid = allocator.add(polygon) 111 | return guid 112 | 113 | def add_regular_polygon(n_sides,radius,position, 114 | color=(.5,.5,.5),allocator=allocator): 115 | pts = polyOfN(n_sides,radius) 116 | poly_verts = wind_vertices(pts) 117 | n = len(poly_verts) 118 | polygon = { 119 | 'render_verts': [(x+position[0],y+position[1],position[2]) for x,y,_,_,_ in poly_verts], 120 | 'color':[color]*n, 121 | 'position':position,} 122 | guid = allocator.add(polygon) 123 | return guid 124 | 125 | def update_rotator(rotator): 126 | arr=rotator 127 | span = arr['max_val'] - arr['min_val'] 128 | arr['accumulator'] += arr['interval'] 129 | underflow = arr['accumulator'] <= arr['min_val'] 130 | arr['accumulator'][underflow] += span[underflow] 131 | overflow = arr['accumulator'] >= arr['max_val'] 132 | arr['accumulator'][overflow] -= span[overflow] 133 | 134 | 135 | def update_render_verts(render_verts,poly_verts,positions,rotator,indices=[]): 136 | '''Update vertices to render based on positions and angles communicated 137 | through the data accessors''' 138 | 139 | angles = rotator['accumulator'] 140 | 141 | cos_ts, sin_ts = cos(angles), sin(angles) 142 | cos_ts -= 1 143 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied 144 | #and simplified. work it out on paper if you don't believe me. 145 | xs, ys, rs, xhelpers, yhelpers = (poly_verts[:,x] for x in range(5)) 146 | pts = render_verts 147 | 148 | #print 'shapes:',angles.shape 149 | pts[:,0] = xhelpers*cos_ts[indices] 150 | pts[:,1] = yhelpers*sin_ts[indices] 151 | pts[:,0] -= pts[:,1] 152 | pts[:,0] *= rs 153 | pts[:,0] += xs 154 | pts[:,0] += positions[indices,0] 155 | 156 | pts[:,1] = yhelpers*cos_ts[indices] 157 | tmp = xhelpers*sin_ts[indices] 158 | pts[:,1] += tmp 159 | pts[:,1] *= rs 160 | pts[:,1] += ys 161 | pts[:,1] += positions[indices,1] 162 | 163 | def update_display(render_verts,colors): 164 | gl.glClearColor(0.2, 0.4, 0.5, 1.0) 165 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 166 | gl.glEnable (gl.GL_BLEND) 167 | gl.glEnable (gl.GL_LINE_SMOOTH); 168 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY) 169 | gl.glEnableClientState(gl.GL_COLOR_ARRAY) 170 | gl.glEnable(gl.GL_DEPTH_TEST) 171 | 172 | n = len(render_verts[:]) 173 | #TODO verts._buffer.ctypes.data is awkward 174 | gl.glVertexPointer(3, vert_dtype.gl, 0, render_verts[:].ctypes.data) 175 | gl.glColorPointer(3, color_dtype.gl, 0, colors[:].ctypes.data) 176 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n) 177 | 178 | 179 | 180 | 181 | if __name__ == '__main__': 182 | 183 | width, height = 640,480 184 | window = pyglet.window.Window(width, height,vsync=False) 185 | #window = pyglet.window.Window(fullscreen=True,vsync=False) 186 | #width = window.width 187 | #height = window.height 188 | fps_display = pyglet.clock.ClockDisplay() 189 | text = """Numpy ECS""" 190 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10) 191 | 192 | n1=1000 193 | 194 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n1,3))] 195 | rs = [r*50 for r in np.random.random(n1)] 196 | ns = [int(m*10)+3 for m in np.random.random(n1)] 197 | colors = np.random.random((n1,3)).astype(color_dtype.np) 198 | rates = np.random.random(n1)*.01 199 | 200 | for n,r, position, color, rate in zip(ns,rs, positions, colors, rates): 201 | add_rotating_regular_polygon(n,r,position,rate,color) 202 | #from before indicies could be calculated 203 | #indices = np.array(reduce(add, [[x,]*7 for x in range(n1)], []),dtype=np.int) 204 | 205 | n2=50 206 | 207 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n2,3))] 208 | rs = [r*50 for r in np.random.random(n2)] 209 | ns = [int(m*10)+3 for m in np.random.random(n2)] 210 | colors = np.random.random((n2,3)).astype(color_dtype.np) 211 | 212 | for n_sides,radius, position, color in zip(ns,rs, positions, colors): 213 | add_regular_polygon(n_sides,radius,position,color) 214 | 215 | allocator._defrag() 216 | 217 | # Create our hero 218 | # white because it stands out against the random colors 219 | # rotating because that's the system that updates the render_verts 220 | hero_guid = add_rotating_regular_polygon(6,75,(width/2,height/2,1),0,color=(1,1,1)) 221 | allocator._defrag() 222 | 223 | # note that hero_guid does not exist until the allocator id defragged 224 | hero = allocator.accessor_from_guid(hero_guid) 225 | 226 | 227 | movement_vector = np.array([0,0],np.int) 228 | 229 | @window.event 230 | def on_key_press(symbol, modifiers): 231 | global movement_vector 232 | if symbol == UP: 233 | movement_vector[1] += 1 234 | elif symbol == DOWN: 235 | movement_vector[1] -= 1 236 | elif symbol == RIGHT: 237 | movement_vector[0] += 1 238 | elif symbol == LEFT: 239 | movement_vector[0] -= 1 240 | 241 | @window.event 242 | def on_key_release(symbol, modifiers): 243 | global movement_vector 244 | if symbol == UP: 245 | movement_vector[1] += -1 246 | elif symbol == DOWN: 247 | movement_vector[1] -= -1 248 | elif symbol == RIGHT: 249 | movement_vector[0] += -1 250 | elif symbol == LEFT: 251 | movement_vector[0] -= -1 252 | 253 | 254 | get_sections = allocator.selectors_from_component_query 255 | @window.event 256 | def on_draw(): 257 | window.clear() 258 | 259 | hero.position[0][:2] += movement_vector 260 | 261 | rotator = ('rotator',) 262 | sections = get_sections(rotator) 263 | update_rotator(*(sections[name] for name in rotator)) 264 | 265 | render_verts =('render_verts','poly_verts','position','rotator') 266 | broadcast = ('position__to__poly_verts',) 267 | sections = get_sections(render_verts + broadcast) 268 | indices = sections.pop(broadcast[0]) 269 | update_render_verts(*(sections[name] for name in render_verts),indices=indices) 270 | 271 | draw =('render_verts','color') 272 | sections = get_sections(draw) 273 | update_display(*(sections[name] for name in draw)) 274 | 275 | fps_display.draw() 276 | 277 | pyglet.clock.schedule(lambda _: None) 278 | 279 | pyglet.app.run() 280 | -------------------------------------------------------------------------------- /examples/polygons.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Numpy-ECS example of rotating and rendering polygons 3 | 4 | Hard coded example of the following Entity_class_id table: 5 | 6 | entity_class_id, vertices, color, positions, rotator, 7 | 8 | 1110, 1, 1, 1, 0, 9 | 1111, 1, 1, 1, 1, 10 | 11 | This file is part of Numpy-ECS. 12 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 13 | 14 | Numpy-ECS is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU General Public License as published by 16 | the Free Software Foundation, either version 2 of the License, or 17 | (at your option) any later version. 18 | 19 | Data Oriented Python is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License for more details. 23 | 24 | You should have received a copy of the GNU General Public License 25 | along with this program. If not, see . 26 | ''' 27 | import numpy as np 28 | from numpy import sin, cos, pi, sqrt 29 | from math import atan2 30 | import pyglet 31 | from pyglet import gl 32 | from collections import namedtuple 33 | from operator import add 34 | 35 | from numpy_ecs.global_allocator import GlobalAllocator 36 | from numpy_ecs.components import DefraggingArrayComponent as Component 37 | 38 | dtype_tuple = namedtuple('Dtype',('np','gl')) 39 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 40 | color_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 41 | 42 | counter_type = np.dtype([('max_val', np.float32), 43 | ('min_val', np.float32), 44 | ('interval', np.float32), 45 | ('accumulator',np.float32)]) 46 | 47 | 48 | allocator = GlobalAllocator((Component('render_verts' , (3,), vert_dtype.np ), 49 | Component('poly_verts' , (5,), color_dtype.np), 50 | Component('color' , (3,), color_dtype.np), 51 | Component('position' , (3,), vert_dtype.np ), 52 | Component('rotator' , (1,), counter_type )), 53 | 54 | allocation_scheme = ( 55 | (1,0,1,1,0), 56 | (1,1,1,1,1), 57 | ) 58 | ) 59 | 60 | 61 | ##################################### 62 | # 63 | # Entity creation helper functions 64 | # 65 | ###################################### 66 | 67 | def polyOfN(n,radius): 68 | '''helper function for making polygons''' 69 | r=radius 70 | if n < 3: 71 | n=3 72 | da = 2*pi/(n) #angle between divisions 73 | return [[r*cos(da*x),r*sin(da*x)] for x in range(int(n))] 74 | 75 | def wind_vertices(pts): 76 | l = len(pts) 77 | #wind up the vertices, so we don't have to do it when speed counts. 78 | #I dont really know which is clock wise and the other counter clock wise, btw 79 | cw = pts[:l//2] 80 | ccw = pts[l//2:][::-1] 81 | flatverts = [None]*(l) 82 | flatverts[::2]=ccw 83 | flatverts[1::2]=cw 84 | wound = [flatverts[0]]+flatverts+[flatverts[-1]] 85 | #prewound vertices can be transformed without care for winding. 86 | #Now, store the vertices in a way that can be translated as efficiently as possible later 87 | #construct list of (x,y,r, x_helper, y_helper) 88 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)] 89 | #lets call the initial angle of a vert alpha 90 | #so later, at theta, we want to remember cos(alpha) and sin(alpha) 91 | #they are the helper values 92 | return [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2), 93 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound] 94 | 95 | def add_rotating_regular_polygon(n_sides,radius,position,rate, 96 | color=(.5,.5,.5),allocator=allocator): 97 | rot_max = 4*np.pi 98 | rot_min = -rot_max 99 | pts = polyOfN(n_sides,radius) 100 | poly_verts = wind_vertices(pts) 101 | n = len(poly_verts) 102 | polygon = { 103 | 'poly_verts': poly_verts, 104 | 'render_verts': [(0,0,position[2])]*n, 105 | 'color':[color]*n, 106 | 'position':position, 107 | 'rotator':(rot_max,rot_min,rate,0) } 108 | allocator.add(polygon) 109 | 110 | def add_regular_polygon(n_sides,radius,position, 111 | color=(.5,.5,.5),allocator=allocator): 112 | pts = polyOfN(n_sides,radius) 113 | poly_verts = wind_vertices(pts) 114 | n = len(poly_verts) 115 | polygon = { 116 | 'render_verts': [(x+position[0],y+position[1],position[2]) for x,y,_,_,_ in poly_verts], 117 | 'color':[color]*n, 118 | 'position':position,} 119 | allocator.add(polygon) 120 | 121 | 122 | 123 | ############# 124 | # 125 | # Systems 126 | # 127 | ############# 128 | 129 | def update_rotator(rotator): 130 | arr=rotator 131 | span = arr['max_val'] - arr['min_val'] 132 | arr['accumulator'] += arr['interval'] 133 | underflow = arr['accumulator'] <= arr['min_val'] 134 | arr['accumulator'][underflow] += span[underflow] 135 | overflow = arr['accumulator'] >= arr['max_val'] 136 | arr['accumulator'][overflow] -= span[overflow] 137 | 138 | 139 | def update_render_verts(render_verts,poly_verts,positions,rotator,indices=[]): 140 | '''Update vertices to render based on positions and angles communicated 141 | through the data accessors''' 142 | 143 | angles = rotator['accumulator'] 144 | 145 | cos_ts, sin_ts = cos(angles), sin(angles) 146 | cos_ts -= 1 147 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied 148 | #and simplified. work it out on paper if you don't believe me. 149 | xs, ys, rs, xhelpers, yhelpers = (poly_verts[:,x] for x in range(5)) 150 | pts = render_verts 151 | 152 | #print 'shapes:',angles.shape 153 | pts[:,0] = xhelpers*cos_ts[indices] 154 | pts[:,1] = yhelpers*sin_ts[indices] 155 | pts[:,0] -= pts[:,1] 156 | pts[:,0] *= rs 157 | pts[:,0] += xs 158 | pts[:,0] += positions[indices,0] 159 | 160 | pts[:,1] = yhelpers*cos_ts[indices] 161 | tmp = xhelpers*sin_ts[indices] 162 | pts[:,1] += tmp 163 | pts[:,1] *= rs 164 | pts[:,1] += ys 165 | pts[:,1] += positions[indices,1] 166 | 167 | def update_display(render_verts,colors): 168 | gl.glClearColor(0.2, 0.4, 0.5, 1.0) 169 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 170 | gl.glEnable (gl.GL_BLEND) 171 | gl.glEnable (gl.GL_LINE_SMOOTH); 172 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY) 173 | gl.glEnableClientState(gl.GL_COLOR_ARRAY) 174 | 175 | n = len(render_verts[:]) 176 | #TODO verts._buffer.ctypes.data is awkward 177 | gl.glVertexPointer(3, vert_dtype.gl, 0, render_verts[:].ctypes.data) 178 | gl.glColorPointer(3, color_dtype.gl, 0, colors[:].ctypes.data) 179 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n) 180 | 181 | 182 | if __name__ == '__main__': 183 | 184 | width, height = 640,480 185 | window = pyglet.window.Window(width, height,vsync=False) 186 | #window = pyglet.window.Window(fullscreen=True,vsync=False) 187 | #width = window.width 188 | #height = window.height 189 | fps_display = pyglet.clock.ClockDisplay() 190 | text = """Numpy ECS""" 191 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10) 192 | 193 | n1=1000 194 | 195 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n1,3))] 196 | rs = [r*50 for r in np.random.random(n1)] 197 | ns = [int(m*10)+3 for m in np.random.random(n1)] 198 | colors = np.random.random((n1,3)).astype(color_dtype.np) 199 | rates = np.random.random(n1)*.01 200 | 201 | for n,r, position, color, rate in zip(ns,rs, positions, colors, rates): 202 | add_rotating_regular_polygon(n,r,position,rate,color) 203 | #from before indicies could be calculated 204 | #indices = np.array(reduce(add, [[x,]*7 for x in range(n1)], []),dtype=np.int) 205 | 206 | n2=50 207 | 208 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n2,3))] 209 | rs = [r*50 for r in np.random.random(n2)] 210 | ns = [int(m*10)+3 for m in np.random.random(n2)] 211 | colors = np.random.random((n2,3)).astype(color_dtype.np) 212 | 213 | for n_sides,radius, position, color in zip(ns,rs, positions, colors): 214 | add_regular_polygon(n_sides,radius,position,color) 215 | 216 | allocator._defrag() 217 | 218 | get_sections = allocator.selectors_from_component_query 219 | 220 | @window.event 221 | def on_draw(): 222 | window.clear() 223 | 224 | rotator = ('rotator',) 225 | sections = get_sections(rotator) 226 | update_rotator(*(sections[name] for name in rotator)) 227 | 228 | render_verts =('render_verts','poly_verts','position','rotator') 229 | broadcast = ('position__to__poly_verts',) 230 | sections = get_sections(render_verts + broadcast) 231 | indices = sections.pop(broadcast[0]) 232 | update_render_verts(*(sections[name] for name in render_verts),indices=indices) 233 | 234 | draw =('render_verts','color') 235 | sections = get_sections(draw) 236 | update_display(*(sections[name] for name in draw)) 237 | 238 | fps_display.draw() 239 | 240 | pyglet.clock.schedule(lambda _: None) 241 | 242 | pyglet.app.run() 243 | 244 | 245 | -------------------------------------------------------------------------------- /examples/robin_animation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Permafacture/data-oriented-pyglet/b0d54a8e0e25aec55b77c4df65b0e0c0d6940280/examples/robin_animation.png -------------------------------------------------------------------------------- /examples/second.py: -------------------------------------------------------------------------------- 1 | ''' 2 | First example using rough data orientation 3 | 4 | Although the same object oriented style is used for generating the data, 5 | it is aggregated together so that heavy maths can be applied en masse. 6 | 7 | This sloppy data aggregation comes at the price of being able to access 8 | individual shapes post creation. IE, all polygons have the same number 9 | of sides and turn at the same rate. 10 | 11 | This file is part of Numpy-ECS. 12 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 13 | 14 | Numpy-ECS Python is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU General Public License as published by 16 | the Free Software Foundation, either version 2 of the License, or 17 | (at your option) any later version. 18 | 19 | Data Oriented Python is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License for more details. 23 | 24 | You should have received a copy of the GNU General Public License 25 | along with this program. If not, see . 26 | 27 | 28 | ''' 29 | 30 | from __future__ import absolute_import, division, print_function, unicode_literals 31 | import numpy as np 32 | import pyglet 33 | from pyglet import gl 34 | from math import pi, sin, cos,atan2,sqrt 35 | import time 36 | from collections import namedtuple 37 | 38 | 39 | #Keep datatypes between numpy and gl consistent 40 | dtype_tuple = namedtuple('Dtype',('np_type','gl_type')) 41 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 42 | color_dtype = dtype_tuple(np.float32,gl.GL_FLOAT) 43 | 44 | #Limit run time for profiling 45 | run_for = 15 #seconds to run test for 46 | def done_yet(duration = run_for, start=time.time()): 47 | return time.time()-start > duration 48 | 49 | 50 | #Set up window 51 | width, height = 640,480 52 | window = pyglet.window.Window(width, height,vsync=False) 53 | #window = pyglet.window.Window(fullscreen=True,vsync=False) 54 | #width = window.width 55 | #height = window.height 56 | fps_display = pyglet.clock.ClockDisplay() 57 | text = """Optimized DOP""" 58 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10) 59 | 60 | 61 | def draw(verts,colors): 62 | '''draw the numpy arrays `verts` and `colors`.''' 63 | 64 | gl.glClearColor(0.2, 0.4, 0.5, 1.0) 65 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 66 | gl.glEnable (gl.GL_BLEND) 67 | gl.glEnable (gl.GL_LINE_SMOOTH); 68 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY) 69 | gl.glEnableClientState(gl.GL_COLOR_ARRAY) 70 | 71 | gl.glVertexPointer(2, vert_dtype.gl_type, 0, verts.ctypes.data) 72 | gl.glColorPointer(3, color_dtype.gl_type, 0, colors.ctypes.data) 73 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(verts)//2) 74 | fps_display.draw() 75 | 76 | #helper hunction for making polygons 77 | def polyOfN(radius,n): 78 | r=radius 79 | if n < 3: 80 | n=3 81 | da = 2*pi/(n) #angle between divisions 82 | return [[r*cos(da*x),r*sin(da*x)] for x in range(n)] 83 | 84 | 85 | class Convex(object): 86 | '''Convex polygons for rendering''' 87 | def __init__(self,pts, position=None, color=None, radius=0): 88 | if position is None: 89 | position = (width//2, height//2) 90 | self.position = position 91 | if color is None: 92 | color = [60,0,0] 93 | self.color = tuple(color) 94 | self.pts=pts 95 | self.initializeVertices() 96 | 97 | def initializeVertices(self): 98 | px,py = self.position 99 | pts = self.pts 100 | l = len(pts) 101 | #wind up the vertices, so we don't have to do it when speed counts. 102 | #I dont really know which is clock wise and the other counter clock wise, btw 103 | cw = pts[:l//2] 104 | ccw = pts[l//2:][::-1] 105 | flatverts = [None]*(l) 106 | flatverts[::2]=ccw 107 | flatverts[1::2]=cw 108 | wound = [flatverts[0]]+flatverts+[flatverts[-1]] 109 | #prewound vertices can be transformed without care for winding. 110 | #Now, store the vertices in a way that can be translated as efficiently as possible later 111 | #construct list of (x,y,r, x_helper, y_helper) 112 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)] 113 | #lets call the initial angle of a vert alpha 114 | #so later, at theta, we want to remember cos(alpha) and sin(alpha) 115 | #they are the helper values 116 | self.initial_data = [(px,py,pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2), 117 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound] 118 | self.n = len(self.initial_data) 119 | self.set_colors() 120 | 121 | def set_colors(self): 122 | self.colors = self.color*self.n 123 | 124 | 125 | 126 | 127 | ## Create shapes 128 | n=150 129 | size = 5 #number of sides 130 | positions = [(x*width,y*height) for x,y in np.random.random((n,2))] 131 | poly_args = [(r*50,size) for r,m in np.random.random((n,2))] 132 | colors = np.random.random((n,3)).astype(color_dtype.np_type) 133 | 134 | ents = [Convex(polyOfN(*pargs),position=pos, color=col) for pargs,pos,col in zip(poly_args,positions,colors)] 135 | 136 | 137 | #Create Data Oriented Arrays 138 | helpers = [] 139 | colors = [] 140 | for ent in ents: 141 | helpers.extend(ent.initial_data) 142 | colors.extend(ent.colors) 143 | 144 | helpers = np.array(helpers) 145 | colors = np.array(colors,dtype=color_dtype.np_type) 146 | 147 | def mass_rotate(initial_data,theta): 148 | initiald = initial_data 149 | cost, sint = cos(theta), sin(theta) 150 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied 151 | #and simplified. work it out on paper if you don't believe me. 152 | pxs, pys, xs, ys, rs, xhelpers, yhelpers = (initiald[:,x] for x in range(7)) 153 | 154 | pts = np.empty((len(pxs),2),dtype=vert_dtype.np_type) 155 | 156 | pts[:,0] = xhelpers*(cost-1) 157 | pts[:,1] = yhelpers*sint 158 | pts[:,0] -= pts[:,1] 159 | pts[:,0] *= rs 160 | pts[:,0] += xs 161 | pts[:,0] += pxs 162 | 163 | pts[:,1] = yhelpers*(cost-1) 164 | tmp = xhelpers*sint 165 | pts[:,1] += tmp 166 | pts[:,1] *= rs 167 | pts[:,1] += ys 168 | pts[:,1] += pys 169 | 170 | #flatten and return as correct type 171 | pts.shape = ( reduce(lambda xx,yy: xx*yy, pts.shape), ) 172 | return pts.astype(vert_dtype.np_type) 173 | 174 | 175 | #instatiate verts array 176 | verts = mass_rotate(helpers,0) 177 | 178 | #global state for polygon rotations 179 | angle = 0 180 | rate = .002 181 | 182 | @window.event 183 | def on_draw(): 184 | global angle,verts,colors 185 | 186 | if done_yet(): 187 | pyglet.app.exit() 188 | 189 | window.clear() 190 | angle+=rate 191 | verts[:] = mass_rotate(helpers,angle) 192 | draw(verts,colors) 193 | 194 | 195 | 196 | #pyglet.clock.set_fps_limit(60) 197 | pyglet.clock.schedule(lambda _: None) 198 | 199 | pyglet.app.run() 200 | 201 | 202 | -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | 2 | ##Buffer 3 | 4 | domain_map = self.group_map[group] 5 | 6 | key = (formats, mode, indexed) 7 | try: 8 | domain = domain_map[key] 9 | except KeyError: 10 | # Create domain 11 | 12 | A domain only knows its formats (and wether it is indexed or not by virtue of which type it is an instance of) 13 | modes are kept seperate through the key 14 | 15 | Formats are like `v3f/stream` that get parsed into attributes within the domain 16 | 17 | ##Domain 18 | 19 | instantiated with list of tuples (AbstractAttribute, usage(static/dynamic/etc), and vob(True/False) 20 | 21 | Domains have an alloc and dealloc through their allocator 22 | 23 | Each attribute is assigned a buffer, which is a vertexbuffer.MappableVertexBufferObject if 24 | possible (easily possible) and desired, or a vertexbuffer.VertexArray. 25 | 26 | (MappableVertexBufferObject has a system-memory backed store, while VertexArray does not.) 27 | 28 | These seperate attribute buffers are sized to all contain the same number of elements: 29 | size of ctype * number of dimensions in vector * allocator.capacity 30 | 31 | Domain stores these as domain.buffer_attributes = [(this_buffer, attribute)], and also in the dict: 32 | attribute.names, keyed by the attribute name string 33 | 34 | 35 | ## VertexList 36 | A vertex list gets its colors, vertices, etc through setters and getters by getting that 37 | attribute's buffer from it's domain and using attribute.get_region with it's own start and count. 38 | 39 | get_region gets data like: 40 | 41 | ''' 42 | notes from __init__ 43 | self.count = count #assert count in (1, 2, 3, 4), 'Component count out of range' 44 | self.align = ctypes.sizeof(self.c_type) 45 | self.size = count * self.align 46 | self.stride = self.size #probably different for interleaved... 47 | self.offset = 0 48 | ''' 49 | 50 | 51 | byte_start = self.stride * start 52 | byte_size = self.stride * count 53 | array_count = self.count * count # num of elements per vector * number of vectors requested == num of ctypes to get 54 | if self.stride == self.size or not array_count: 55 | # non-interleaved 56 | ptr_type = ctypes.POINTER(self.c_type * array_count) 57 | return buffer.get_region(byte_start, byte_size, ptr_type) 58 | else: 59 | # interleaved 60 | # Elliot: more complicated. Comments in VertexDomain __init__ lead me to assume this is only for static attributes. 61 | 62 | 63 | -------------------------------------------------------------------------------- /numpy_ecs/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of Numpy-ECS. 3 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 4 | 5 | Numpy-ECS is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | Data Oriented Python is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | -------------------------------------------------------------------------------- /numpy_ecs/accessors.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Accessors provide an object oriented interface for a specific 3 | entity instance (guid) by providing an object with attributes 4 | that access the array slices allocated to that guid under 5 | the hood 6 | 7 | This file is part of Numpy-ECS. 8 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 9 | 10 | Numpy-ECS is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 2 of the License, or 13 | (at your option) any later version. 14 | 15 | Data Oriented Python is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | ''' 23 | from __future__ import absolute_import, division, print_function 24 | import sys 25 | #python version compatability 26 | if sys.version_info < (3,0): 27 | from future_builtins import zip, map 28 | 29 | import numpy as np 30 | 31 | def build_references(allocator,guid): 32 | '''returns {component_name: slice or None, ...} for guid''' 33 | component_names = allocator.names 34 | get_slice = allocator._allocation_table.slices_from_guid 35 | slices = (s if s.start != s.stop else None for s in get_slice(guid)) 36 | return dict(zip(component_names,slices)) 37 | 38 | #TODO, I would like for methods of Accessor only called by Accessor to 39 | # be double underscored, but since they are used in the Factory, the 40 | # double underscore gets expanded to be the Factory. There might be 41 | # too much meta to make them private to a class that is created 42 | # just in time. 43 | 44 | class Accessor(object): 45 | ''' 46 | Accessors provide an object oriented interface for a specific 47 | entity instance (guid) by providing an object with attributes 48 | that access the array slices allocated to that guid under 49 | the hood.''' 50 | 51 | def __init__(self,guid): 52 | # self._allocator is provided by the factory 53 | self._guid = guid 54 | self._dirty = True #do slices need to be re-calculated? 55 | self._active = True #is this accessor deleteable? 56 | self._references = {} #{component_name:slice,...} 57 | #The factory gave us a property for every component in the Table 58 | # but any specific guid won't have values for all of those 59 | # so we delete them 60 | # TODO can't delete a class property from an instance :'( 61 | #for name,val in build_references(self._allocator,guid).items(): 62 | # if val is None: 63 | # print("delete {}".format(name)) 64 | # print(dir(self)) 65 | # delattr(self,name) 66 | 67 | def _rebuild_references(self,build_ref = build_references): 68 | '''Get the up to date slices for all the attributes''' 69 | # the is not None is not necessary, but not harmful 70 | self._references = {k:v for k,v in build_ref(self._allocator, 71 | self._guid).items() if v is not None} 72 | self._dirty = False 73 | 74 | #def resize(self,new_size): 75 | # self._domain.safe_realloc(self._id,new_size) 76 | 77 | #def close(self): 78 | # self.__closed = True 79 | # self._domain.safe_dealloc(self._id) 80 | 81 | #def __del__(self): 82 | # if not self.__closed: 83 | # self.close() 84 | 85 | def __repr__(self): 86 | return ""%(self._guid,) 87 | 88 | 89 | class AccessorFactory(object): 90 | 91 | def __init__(self,allocator): 92 | self.allocator=allocator 93 | 94 | def attribute_getter_factory(self, component_name): 95 | '''generate a getter for this component_name into the Component data array''' 96 | comp_dict = self.allocator.component_dict 97 | def getter(accessor, name=component_name, comp_dict=comp_dict): 98 | if accessor._dirty: 99 | accessor._rebuild_references() 100 | selector = accessor._references[name] 101 | return comp_dict[name][selector] 102 | return getter 103 | 104 | def attribute_setter_factory(self, component_name): 105 | '''generate a setter using this object's index to the domain arrays 106 | attr is the domain's list of this attribute''' 107 | comp_dict = self.allocator.component_dict 108 | def setter(accessor, data, name=component_name,comp_dict=comp_dict): 109 | if accessor._dirty: 110 | accessor._rebuild_references() 111 | selector = accessor._references[name] 112 | comp_dict[name][selector] = data 113 | return setter 114 | 115 | def generate_accessor(self): 116 | '''return a DataAccessor class that can be instatiated with a 117 | guid to provide an object oriented interface with the data 118 | associated with that guid''' 119 | NewAccessor = type('DataAccessor',(Accessor,),{}) 120 | 121 | allocator = self.allocator 122 | getter = self.attribute_getter_factory 123 | setter = self.attribute_setter_factory 124 | comp_dict = allocator.component_dict 125 | for name in allocator.names: 126 | print("adding property: {}".format(name)) 127 | setattr(NewAccessor,name,property(getter(name), setter(name))) 128 | NewAccessor._allocator = self.allocator 129 | return NewAccessor 130 | 131 | 132 | -------------------------------------------------------------------------------- /numpy_ecs/components.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Class that wraps a numpy array as a buffer, and provides for resizing, 3 | inserting, appending, and defragging. 4 | 5 | This file is part of Numpy-ECS. 6 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 7 | 8 | Numpy-ECS is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | Data Oriented Python is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | ''' 21 | import numpy as np 22 | 23 | def _nearest_pow2(v): 24 | # From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 25 | # Credit: Sean Anderson 26 | v -= 1 27 | v |= v >> 1 28 | v |= v >> 2 29 | v |= v >> 4 30 | v |= v >> 8 31 | v |= v >> 16 32 | return v + 1 33 | 34 | class DefraggingArrayComponent(object): 35 | '''holds a resize-able, re-allocateable, numpy array buffer''' 36 | 37 | def __init__(self,name,dim,dtype,size=0): 38 | ''' create a numpy array buffer of shape (size,dim) with dtype==dtype''' 39 | #TODO: might could alternatively instatiate with an existing numpy array? 40 | self.name = name 41 | self.datatype=dtype #calling this dtype would be confusing because this is not a numpy array! 42 | self._dim = dim 43 | self.capacity = size 44 | if dim == (1,): 45 | self._buffer = np.empty(size,dtype=dtype) #shape = (size,) not (size,1) 46 | self.resize = self._resize_singledim 47 | elif dim > 1: 48 | self._buffer = np.empty((size,)+dim,dtype=dtype) #shape = (size,dim) 49 | self.resize = self._resize_multidim 50 | else: 51 | raise ValueError('''ArrayComponent dim must be >= 1''') 52 | 53 | #def resize(self,count): 54 | # '''resize this component to new size = count''' 55 | # #This is a placeholder, __init__ decides what to overwrite this with 56 | # # ^ not true any more 57 | # self._buffer= self._buffer.resize((count,)+self._dim) 58 | 59 | 60 | def assert_capacity(self,new_capacity): 61 | '''make certain Component is atleast `new_capcpity` big. 62 | resizing if necessary.''' 63 | if self.capacity < new_capacity: 64 | #print "change capacity:",self.capacity,"->", new_capacity 65 | self.resize(_nearest_pow2(new_capacity)) 66 | self.capacity = new_capacity 67 | 68 | def realloc(self,old_selector,new_selector): 69 | self._buffer[new_selector] = self._buffer[old_selector] 70 | 71 | #def push_from_index(self,index,size): 72 | # '''push all data in buffer from start onward forward by size. 73 | # if size is negative, moves everything backwards. Assumes alloc 74 | # was already called, assuring that there is size empty values at 75 | # end of buffer.''' 76 | # end = self.capacity 77 | # if size == 0: 78 | # return 79 | # elif size > 0: 80 | # self._buffer[index+size:] = self._buffer[index:-size] 81 | # elif size < 0: 82 | # self._buffer[index+size:size] = self._buffer[index:] 83 | 84 | 85 | def __getitem__(self,selector): 86 | #assert self.datatype == self._buffer.dtype #bug if numpy changes dtype 87 | return self._buffer[selector] 88 | 89 | def __setitem__(self,selector,data): 90 | self._buffer[selector]=data 91 | assert self.datatype == self._buffer.dtype, 'numpy dtype may not change' 92 | 93 | def _resize_multidim(self,count): 94 | shape =(count,)+self._dim 95 | try: 96 | #this try is because empty record arrays cannot be resized. 97 | #should be fixed in a more recent numpy. 98 | self._buffer.resize(shape) 99 | except ValueError: 100 | self._buffer = np.resize(self._buffer,shape) 101 | 102 | def _resize_singledim(self,count): 103 | try: 104 | #this try is because empty record arrays cannot be resized. 105 | #should be fixed in a more recent numpy. 106 | self._buffer.resize(count) 107 | except ValueError: 108 | self._buffer = np.resize(self._buffer,count) 109 | 110 | def __repr__(self): 111 | return ""%self.name 112 | 113 | -------------------------------------------------------------------------------- /numpy_ecs/global_allocator.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Allocating Entity values in contiguous Component arrays, providing Systems 3 | with access to simple slices of components. 4 | 5 | This file is part of Numpy-ECS. 6 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 7 | 8 | Numpy-ECS is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | Data Oriented Python is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | ''' 21 | 22 | # Systems require slices, not index arrays or multiple slices, to access data 23 | # in components. Naively allocating spaces in arrays without regard for this 24 | # will not work. For instance: 25 | # 26 | # [ 27 | # 28 | # entity_class_id, vertices, animated, positions, velocity 29 | # 30 | # 01, 1, 0, 0, 0 31 | # 02, 1, 0, 1, 0 32 | # 03, 1, 0, 1, 1 33 | # 04, 1, 1, 0, 0 34 | # 05, 1, 1, 1, 0 35 | # 06, 1, 1, 1, 1 36 | # 37 | # ] 38 | # 39 | # Trying to apply update_verts(positions, velocities), one cannot get a single 40 | # slice on positions. This table can be re-arranged to work: 41 | # 42 | # [ 43 | # 44 | # entity_class_id, vertices, animated, positions, velocity 45 | # 46 | # 01, 1, 0, 0, 0 47 | # 02, 1, 0, 1, 0 48 | # 03, 1, 0, 1, 1 49 | # 06, 1, 1, 1, 1 50 | # 05, 1, 1, 1, 0 51 | # 04, 1, 1, 0, 0 52 | # ] 53 | # 54 | # Now velocities can update a single slice of positions, and postions can 55 | # update a single slice of vertices (including those that are also animated). 56 | # 57 | # If the entity_class_id is a binary representation of the components it 58 | # requires, then given an addition of any arbitrary collection of 59 | # attributes, we can determine what entity_class it belongs to and add 60 | # the values to sections of the array that maintain ideal contiguity. 61 | # 62 | # [ 63 | # 64 | # entity_class_id, vertices, animated, positions, velocity 65 | # 66 | # 1000, 1, 0, 0, 0 67 | # 1010, 1, 0, 1, 0 68 | # 1011, 1, 0, 1, 1 69 | # 1111, 1, 1, 1, 1 70 | # 1110, 1, 1, 1, 0 71 | # 1100, 1, 1, 0, 0 72 | # ] 73 | # 74 | # 75 | from table import Table, INDEX_SEPERATOR 76 | import numpy as np 77 | from .table import Table, INDEX_SEPERATOR 78 | from .accessors import AccessorFactory 79 | 80 | def verify_component_schema(allocation_schema): 81 | '''given an allocation schema as a list of lists, return True if the schema 82 | keeps all Component arrays contiguous. Else return False''' 83 | started = [False] * len(allocation_schema[0]) 84 | ended = [False] * len(allocation_schema[0]) 85 | for row in allocation_schema: 86 | for i,val in enumerate(row): 87 | if val: 88 | if not started[i]: 89 | started[i] = True 90 | else: 91 | if ended[i]: 92 | return False 93 | elif started[i]: 94 | ended[i] = True 95 | return True 96 | 97 | 98 | class GlobalAllocator(object): 99 | '''Decides how data is added or removed from Components, allocating and 100 | deallocting Entities/guids. Components better be allocated by only one 101 | of these. 102 | 103 | see comments at top of file for details''' 104 | 105 | def __init__(self,components,allocation_scheme): 106 | self.component_dict = {component.name:component for component in components} 107 | self._memoized = {} #for selectors_from_component_names 108 | 109 | names = tuple(comp.name for comp in components) 110 | self.names = names 111 | self._allocation_table = Table(names, tuple(allocation_scheme)) 112 | 113 | self._cached_adds = list() 114 | self._next_guid = 0 115 | self.__accessor_factory = AccessorFactory(self).generate_accessor() 116 | self.__accessors = [] #accessors that have been handed out 117 | 118 | def accessor_from_guid(self,guid): 119 | accessor = self.__accessor_factory(guid) 120 | self.__accessors.append(accessor) 121 | return accessor 122 | 123 | @property 124 | def next_guid(self): 125 | self._next_guid += 1 126 | return self._next_guid 127 | 128 | @property 129 | def guids(self): 130 | return tuple(self._allocation_table.guids) 131 | #def _class_id_from_guid(guid): 132 | # #assumes entity has non-zero size for every component that 133 | # # defines it's class. This is the definition of a class_id and is 134 | # # a result of how add works 135 | # row = row_for_guid(guid) 136 | # return int(''.join(x!=0 for x in row),base=2) 137 | 138 | 139 | #def allocation_schema_from_truth_table(self,truth_table): 140 | # ''' 141 | # an allocation schema may be defined as a list of lists. 142 | # 143 | # convert that to a list of integers where (0,0,1,1) -> 3, etc''' 144 | # return map(self.entity_class_from_tuple,truth_table) 145 | 146 | #def entity_class_from_dict(self,component_dict): 147 | # '''takes a component dict like {'comp_name1':value} 148 | 149 | # returns the integer that is this entity class''' 150 | # names = self._allocation_table.column_names 151 | # for name in component_dict.keys(): 152 | # assert name in names, "%s is not a known component name"% (name,) 153 | # entity_tuple = tuple(0 if (component_dict.get(name,None) is None) \ 154 | # else 1 for name in names) 155 | # return self.entity_class_from_tuple(entity_tuple) 156 | 157 | def add(self,values_dict,guid=None): 158 | component_dict = self.component_dict 159 | 160 | def convert_to_component_array(component,value): 161 | '''converts value to a numpy array with appropriate shape for 162 | component''' 163 | value = np.array(value,dtype = component.datatype) 164 | 165 | shape = value.shape or (1,) 166 | dim = component._dim 167 | assert shape == dim or (len(shape)>1 and shape[1:] == dim), \ 168 | "component '%s' expected shape %s, but got %s" % ( 169 | component.name,component._dim,value.shape) 170 | #len(shape) is a look before I leap. No exceptions please 171 | return value 172 | 173 | result = {'guid': guid or self.next_guid} 174 | for name, value in values_dict.items(): 175 | result[name] = convert_to_component_array(component_dict[name],value) 176 | assert result['guid'] not in {d['guid'] for d in self._cached_adds}, \ 177 | "cannot add a guid twice" 178 | self._cached_adds.append(result) 179 | return result['guid'] 180 | 181 | def delete(self,guid): 182 | alloc_table = self._allocation_table 183 | alloc_table.stage_delete(guid) 184 | 185 | def _defrag(self): 186 | alloc_table = self._allocation_table 187 | component_dict = self.component_dict 188 | adds_dict = self._cached_adds 189 | if (not adds_dict) and (None not in alloc_table.guids): 190 | return #nothing to do 191 | 192 | #delete_set = self._cached_deletes 193 | def safe_len(item): 194 | if item is None: 195 | return 0 196 | shape = item.shape 197 | if len(shape) > 1: 198 | #TODO only supports arrays like [n] and [1,n] !!! 199 | #TODO Can't think of fix or what one would be expecting but I 200 | #TODO must note this limitation 201 | return shape[0] 202 | else: 203 | return 1 204 | 205 | for add in self._cached_adds: 206 | guid = add['guid'] 207 | add = tuple(safe_len(add.get(name,None)) \ 208 | for name in alloc_table.column_names) 209 | alloc_table.stage_add(guid,add) 210 | 211 | #defrag 212 | #print "defrag" 213 | for name, (new_size, sources, targets) in zip(alloc_table.column_names,alloc_table.compress()): 214 | #if name == 'component_1': print "working on component_1" 215 | component = component_dict[name] 216 | component.assert_capacity(new_size) 217 | for source,target in zip(sources,targets): 218 | #if name == "component_1": 219 | # print "moving",component[source],"to",target 220 | component[target] = component[source] 221 | 222 | #apply adds 223 | for add in self._cached_adds: 224 | guid = add['guid'] 225 | for name, this_slice in zip(alloc_table.column_names,alloc_table.slices_from_guid(guid)): 226 | if name in add: 227 | component_dict[name][this_slice] = add[name] 228 | 229 | #reset 230 | self._cached_adds = list() 231 | self._memoized = dict() 232 | for accessor in self.__accessors: 233 | accessor._dirty = True 234 | 235 | def is_valid_query(self,query,sep=INDEX_SEPERATOR): 236 | known_names = self.names 237 | for x in query: 238 | if (sep not in x) and (x not in known_names): 239 | print "%s in query is not valid"%x 240 | return False 241 | return True 242 | 243 | def selectors_from_component_query(self,query,sep=INDEX_SEPERATOR): 244 | #TODO add indicies to doc string 245 | '''takes: ['comp name 1', 'comp name 3', ...] #list tuple or set 246 | returns {'component name 1': component1[selector] ...} 247 | where selector is for the section where all components 248 | are defined''' 249 | assert isinstance(query,tuple), 'argument must be hashable' 250 | known_names = self.names 251 | assert self.is_valid_query(query), \ 252 | 'col_names must be valid component names and index names' 253 | cache = self._memoized 254 | if query not in cache: 255 | indices = tuple(filter(lambda x: sep in x, query)) 256 | col_names = tuple(filter(lambda x: x not in indices, query)) 257 | table = self._allocation_table 258 | selectors, indices = table.mask_slices(col_names,indices) 259 | cdict = self.component_dict 260 | result = {n:cdict[n][s] for n,s in selectors.items()} 261 | result.update({n:np.array(lst) for n,lst in indices.items()}) 262 | cache[query] = result 263 | else: 264 | result = cache[query] 265 | return dict(result) #return copy of cached result 266 | 267 | 268 | 269 | 270 | ######### 271 | # 272 | # Components 273 | # 274 | ######### 275 | 276 | 277 | 278 | if __name__ == '__main__': 279 | from components import DefraggingArrayComponent as Component 280 | import numpy as np 281 | 282 | #TODO tests should include: when there is nothing in the first class, 283 | # when there is nothing in the last class. When there is nothing 284 | # in one or many in between classes. When the allocation scheme has repeat 285 | # entries. When the allocation scheme is not contiguous. component 286 | # dimensions of (1,),(n,),(n,m) and when incorrect dimensions are added. 287 | # also, when components have complex data types (structs) 288 | 289 | d1 = Component('component_1',(1,),np.int32) 290 | d2 = Component('component_2',(2,),np.int32) 291 | d3 = Component('component_3',(3,),np.int32) 292 | allocation_scheme = ( 293 | (1,1,1), 294 | (1,0,1), 295 | (1,0,0),) 296 | 297 | allocator = GlobalAllocator([d1,d2,d3],allocation_scheme) 298 | 299 | to_add = [] 300 | to_add.append({'component_1':1,'component_3':(1,2,3),'component_2':((1,10),(2,20)),}) 301 | to_add.append({'component_1':2,'component_3':(4,5,6),'component_2':((3,30),(4,40)),}) 302 | to_add.append({'component_1':3,'component_3':(7,8,9),'component_2':((5,50),(6,60)),}) 303 | to_add.append({'component_1':4,'component_3':(10,11,12),'component_2':((7,70),(8,80)),}) 304 | to_add.append({'component_1':7,'component_3':(19,20,21),}) 305 | trash = map(allocator.add,to_add) 306 | #allocator.add(to_add2) 307 | allocator._defrag() 308 | #print "d1:",d1[:] 309 | 310 | to_add = [] 311 | to_add.append({'component_1':8,'component_3':(22,23,24),}) 312 | trash = map(allocator.add,to_add) 313 | #allocator.add(to_add2) 314 | allocator._defrag() 315 | #print "d1:",d1[:] 316 | 317 | to_add = [] 318 | to_add.append({'component_1':5,'component_3':(13,14,15),'component_2':((9,90),(10,100)),}) 319 | to_add.append({'component_1':6,'component_3':(16,17,18),'component_2':((11,110),(12,120)),}) 320 | to_add.append({'component_1':9,'component_3':(25,26,27),}) 321 | trash = map(allocator.add,to_add) 322 | #allocator.add(to_add2) 323 | allocator._defrag() 324 | assert np.all(d1[:9] == np.array([1,2,3,4,5,6,7,8,9])) 325 | 326 | #to_add1 = {'component_1':2,'component_3':8,'component_2':7,} 327 | #to_add2 = {'component_1':5,'component_3':2,} 328 | #allocator.add(to_add1) 329 | #allocator.add(to_add2) 330 | #allocator._defrag() 331 | #to_add1 = {'component_1':3,'component_3':8,'component_2':7,} 332 | #to_add2 = {'component_1':6,'component_3':2,} 333 | #allocator.add(to_add1) 334 | #allocator.add(to_add2) 335 | #allocator._defrag() 336 | 337 | -------------------------------------------------------------------------------- /numpy_ecs/table.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Classes to facilitate allocating guids in groups of class_ids 3 | 4 | Keeps sizes of allocated areas in a table 5 | 6 | class_id guid component_1 component_2 ... 7 | ----- 8 | | 001 7 1 9 | | 003 6 1 10 | 11... 004 4 1 11 | | 005 6 1 12 | | 007 5 1 13 | ----- 14 | | 008 4 0 15 | 10... 010 6 0 16 | | 011 2 0 17 | ----- 18 | 19 | This file is part of Numpy-ECS. 20 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com) 21 | 22 | Numpy-ECS is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU General Public License as published by 24 | the Free Software Foundation, either version 2 of the License, or 25 | (at your option) any later version. 26 | 27 | Data Oriented Python is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU General Public License for more details. 31 | 32 | You should have received a copy of the GNU General Public License 33 | along with this program. If not, see . 34 | ''' 35 | 36 | from collections import MutableMapping, Sequence 37 | from types import GeneratorType 38 | 39 | INDEX_SEPERATOR = '__to__' # 'ie: index from component1__to__component2 40 | 41 | #TODO make row a numpy array and delete TableRow 42 | class TableRow(Sequence): 43 | '''A tuple that can be added to another tuple of the same size''' 44 | def __init__(self,*values): 45 | if isinstance(values[0],GeneratorType): 46 | self.values = tuple(values[0]) 47 | elif isinstance(values[0],tuple): 48 | self.values = values[0] 49 | else: 50 | self.values = tuple(values) 51 | 52 | def __add__(self,other): 53 | other = tuple(other) 54 | assert len(other) == len(self.values), "Must add item of same length" 55 | return TableRow(this+that for this,that in zip(self.values,other)) 56 | 57 | def __sub__(self,other): 58 | other = tuple(other) 59 | assert len(other) == len(self.values), "Must add item of same length" 60 | return TableRow(this-that for this,that in zip(self.values,other)) 61 | 62 | def __eq__(self,other): 63 | return self.values == other 64 | 65 | def __ne__(self,other): 66 | return self.values != other 67 | 68 | def __repr__(self): 69 | return "TableRow: %s" %(self.values,) 70 | 71 | def __len__(self): 72 | return len(self.values) 73 | 74 | def __iter__(self): 75 | return iter(self.values) 76 | 77 | def copy(self): 78 | return TableRow(*self.values) 79 | 80 | def __getitem__(self,index): 81 | return self.values[index] 82 | 83 | def slice_is_not_empty(s): 84 | #print " ",s.start,s.stop-s.start 85 | return s.start != s.stop 86 | 87 | class Table(object): 88 | '''An allocation table that has column names (component names) and major 89 | rows (class ids) and minor rows (guids). The minor rows are divisions of 90 | the major rows. Class ids are a tuple which are the binary of 91 | what columns are present in the guid row. 92 | 93 | keeps track of sizes and starts of allocated guids, and provides 94 | functionality for adding and deleting guids, as well as giving the slices 95 | at which the values of guids can be found in a Component arrays and for 96 | defragging those arrays. 97 | ''' 98 | def __init__(self,column_names,class_ids): 99 | '''column names is a tuple of strings that define, in order, the names 100 | of the columns present in this table. class_ids is a tuple of tuples 101 | that determines the order of the major rows (essential for keeping 102 | related arrays contiguous with respect to each other)''' 103 | self.__col_names = tuple(column_names) 104 | self.__row_length = len(column_names) 105 | self.__row_format = ''.join((" | {:>%s}"%(len(name)) for name in column_names)) 106 | self._staged_adds = dict() 107 | self.known_class_ids = tuple(class_ids) 108 | self.class_ids = list() 109 | self.guids = list() 110 | self.starts = list() 111 | self.sizes = list() 112 | 113 | @property 114 | def column_names(self): 115 | return self.__col_names 116 | 117 | #@property 118 | #def guid_columns(self): 119 | # return zip(*self.sizes) 120 | 121 | #@property 122 | #def class_id_columns(self): 123 | # return zip(*self.as_class_id_table()) 124 | 125 | def entity_class_from_tuple(self,sizes_tuple): 126 | '''takes a component tuple like (5,5,0,2,2,2) 127 | 128 | returns a normalized tuple that is a class id, like (1,1,0,1,1,1)''' 129 | assert len(sizes_tuple) == self.__row_length, "too many values" 130 | # Used to convert to integer, but that's unnecessary 131 | #return sum(map(lambda (i,x): 0 if x ==0 else (x/x)*2**i, 132 | # enumerate(sizes_tuple[::-1]))) 133 | return tuple(0 if x==0 else x/x for x in sizes_tuple) 134 | 135 | #def entity_class_from_dict(self,dictionary): 136 | # '''Creates a class id from a dictionary keyed by column_names''' 137 | # cols = self.column_names 138 | # keys = set(dictionary.keys()) 139 | # #TODO make this one assert, not a loop 140 | # for name in keys: 141 | # assert(name in cols),"unknown class_id %s"%(name,) 142 | # return tuple(1 if name in keys else 0 for name in cols) 143 | 144 | # assert len(sizes_tuple) == self.__row_length, "too many values" 145 | # # Used to convert to integer, but that's unnecessary 146 | # #return sum(map(lambda (i,x): 0 if x ==0 else (x/x)*2**i, 147 | # # enumerate(sizes_tuple[::-1]))) 148 | # return tuple(0 if x==0 else x/x for x in sizes_tuple) 149 | 150 | def stage_add(self,guid,value_tuple): 151 | assert guid not in self.guids, "guid must be unique" 152 | assert guid not in {t[0] for lst in self._staged_adds.values() \ 153 | for t in lst}, "cannot restage a staged guid" 154 | ent_class = self.entity_class_from_tuple(value_tuple) 155 | assert ent_class in self.known_class_ids, \ 156 | "added entity must corispond to a class id in the allocation schema" 157 | #print "guid %s is class %s"%(guid,ent_class) 158 | self._staged_adds.setdefault(ent_class, 159 | list()).append((guid,value_tuple)) 160 | 161 | def stage_delete(self,guid): 162 | '''mark guid None so it can be removed later''' 163 | self.guids[self.guids.index(guid)] = None 164 | 165 | def make_starts_table(self): 166 | '''Create a list of tuples where each value is the start index of that 167 | element. (Table is the sizes)''' 168 | start = TableRow(*(0,)*self.__row_length) 169 | table = [start] 170 | for row in self.sizes: 171 | start = row + start 172 | table.append(start) 173 | return table 174 | 175 | #def class_sizes_table(self): 176 | # '''return a list of rows where each row is the size of the class_id's 177 | # in this table. This can be zipped with class_ids.''' 178 | # result = [] 179 | # empty_row = lambda : TableRow(*(0,)*self.__row_length) 180 | # row = empty_row() 181 | # known_ids = iter(self.known_class_ids) 182 | # this_id = next(known_ids) 183 | # for class_id, size_row in zip(self.class_ids,self.sizes): 184 | # while class_id != this_id: 185 | # result.append(row) 186 | # row = empty_row() 187 | # this_id = next(known_ids) 188 | # row+= size_row 189 | # result.append(row) 190 | # #if class_ids is exausted but not all known ids were reached, 191 | # # add empty rows 192 | # for empty in known_ids: 193 | # result.append(empty_row()) 194 | # return result 195 | 196 | def section_slices(self): 197 | #TODO doc string is wrong 198 | #TODO change to use class_size_table 199 | '''return a list of slices that represent the portions of each column 200 | that corrispond to the known class ids in order. For known ids not 201 | present in class_ids, a zero sized slice is used''' 202 | known_ids = self.known_class_ids 203 | id_column = self.class_ids 204 | expressed_ids = filter(lambda x: x in id_column,known_ids) 205 | starts = map(id_column.index,expressed_ids) 206 | assert all(starts[x]= free_start for start, 346 | free_start in zip(current_start,free_start))),\ 347 | "next start must be larger than current free start" 348 | if guid is None: 349 | current_start += size_tuple 350 | deallocs += size_tuple 351 | else: 352 | #shift data back to cover deletes 353 | #print " counting %s of %s"%(guid,class_id) 354 | new_class_ids.append(class_id) 355 | new_guids.append(guid) 356 | new_sizes.append(size_tuple) 357 | if current_start != free_start: 358 | #print "size_tuple:\n",size_tuple 359 | sources.append(tuple(slice(start,start+size,1) for start, 360 | size in zip(current_start,size_tuple))) 361 | targets.append(tuple(slice(start,start+size,1) for start, 362 | size in zip(free_start,size_tuple))) 363 | #print " small alloc for guid",guid,"to",targets[-1] 364 | free_start += size_tuple 365 | current_start += size_tuple 366 | #deal with adds 367 | for added_guid, size_tuple in self._staged_adds.get(class_id,()): 368 | size = TableRow(size_tuple) 369 | allocs += size 370 | #print" adding guid %s as %s"%(added_guid,class_id) 371 | new_class_ids.append(class_id) 372 | new_guids.append(added_guid) 373 | new_sizes.append(size) 374 | #free_start = current_start + allocs #TODO part of last debugging 375 | free_start += allocs 376 | new_ends += allocs 377 | new_ends -= deallocs 378 | #after inserting adds, write the big change to sources and targets 379 | #Note: empty slices are added here, and are filtered out when we 380 | # columnize the row data 381 | if current_start != free_start: 382 | source = tuple(slice(start,end,1) for start,end in zip( 383 | current_start,ends)) 384 | target = tuple(slice(start,end,1) for start,end in zip( 385 | free_start,new_ends)) 386 | #print "adding:\n",source,target 387 | sources.append(source) 388 | targets.append(target) 389 | #print "big alloc for class",class_id 390 | #for s,t in zip(source,target): print s,"-->",t 391 | #print "starts:\n %s| %s\n %s| %s\n" % (current_start, ends, free_start,new_ends) 392 | #print "starts: %s | %s" % (current_start - free_start, ends - new_ends) 393 | current_start = free_start.copy() #TODO part of last debugging 394 | ends = new_ends.copy() 395 | total_alloc += allocs 396 | this_class_id = class_id 397 | 398 | self.class_ids = new_class_ids 399 | self.guids = new_guids 400 | self.sizes = new_sizes 401 | self.starts = self.make_starts_table() 402 | self._staged_adds = {} 403 | 404 | #columnize the row data 405 | #TODO make this a generator? 406 | ret = [] 407 | for new_capacity, col_sources, col_targets in zip( 408 | new_ends, zip(*sources), zip(*targets)): 409 | #print "sources:" 410 | col_sources = tuple((s for s in col_sources if slice_is_not_empty(s))) 411 | #print "targets:" 412 | col_targets = tuple((t for t in col_targets if slice_is_not_empty(t))) 413 | #Assert that the above did what we expect 414 | if __debug__ == True: 415 | for s,t in zip(col_sources,col_targets): 416 | assert s.stop-s.start == t.stop-t.start, \ 417 | 'source size and target size must match' 418 | assert len(col_sources) == len(col_targets) 419 | ret.append((new_capacity,col_sources,col_targets)) 420 | 421 | return ret 422 | 423 | def slices_from_guid(self,guid): 424 | idx = self.guids.index(guid) 425 | starts = self.starts[idx] 426 | sizes = self.sizes[idx] 427 | return (slice(start,start+size,1) for start,size in zip(starts,sizes)) 428 | 429 | #def as_class_id_table(self): 430 | # ids = self.class_ids 431 | # idx = 0 432 | # cur_id = ids[idx] 433 | # id = ids[idx] 434 | # result = list() 435 | # while idx < len(ids) 436 | # tmp_result = TableRow(*(0,)*self.__row_length) 437 | # while id == cur_id: 438 | 439 | #def __len__(self): return len(self.guids) 440 | #def __iter__(self): return iter(self.guids) 441 | #def __contains__(self,key): return key in self.guids 442 | #def keys(self): return tuple(self.guids) 443 | #def values(self): return tuple(self.rows) 444 | #def items(self): return zip(self.guids,self.rows) 445 | 446 | ##def add_key(self,key,default=None): 447 | ## assert key not in self.ids, "key %s already exists in table" % (key,) 448 | ## self.ids.append(key) 449 | ## if default is None: 450 | ## self.rows.append(TableRow(*(0,)*self.__row_length)) 451 | ## else: 452 | ## assert len(value) == self.__row_length,\ 453 | ## "%s does not have len %s"%(value,self.__row_length) 454 | ## self.rows.append(TableRow(*value)) 455 | 456 | #def get_guid(self,key): 457 | # if key in self.guids: 458 | # return self.rows[self.guids.index(key)] 459 | # else: 460 | # return TableRow(*(0,)*self.__row_length) 461 | # 462 | #def __getitem__(self,key): 463 | # assert key in self.guids, "guid %s not in Table" % (key,) 464 | # return self.rows[self.guids.index(key)] 465 | 466 | #def __setitem__(self,key,value): 467 | # #TODO should this insert into proper class_id? 468 | # assert key in self.guids, "Key %s not in table Table" % (key,) 469 | # assert len(value) == self.__row_length,\ 470 | # "%s does not have len %s"%(value,self.__row_length) 471 | # self.rows[self.guids.index(key)] = TableRow(*value) 472 | 473 | #def __delitem__(self,key): 474 | # raise AttributeError("Table does not support removal of keys") 475 | 476 | def show_sizes(self): 477 | formatter = self.__row_format.format 478 | ret_str = " guid" 479 | ret_str += formatter(*self.__col_names) 480 | ret_str += "\n" 481 | for guid,size_tuple in zip(self.guids, self.sizes): 482 | ret_str += "{:>5}".format(guid) 483 | ret_str += formatter(*size_tuple.values) 484 | ret_str += "\n" 485 | return ret_str 486 | 487 | def show_starts(self): 488 | formatter = self.__row_format.format 489 | ret_str = " guid" 490 | ret_str += formatter(*self.__col_names) 491 | ret_str += "\n" 492 | for guid,size_tuple in zip(self.guids, self.starts): 493 | ret_str += "{:>5}".format(guid) 494 | ret_str += formatter(*size_tuple.values) 495 | ret_str += "\n" 496 | return ret_str 497 | 498 | def __str__(self): 499 | return "" 500 | 501 | #def column_names_from_mask(self,mask): 502 | # '''takes a mask as a tuple or integer (where the binary representation 503 | # of the integer is the mask) and returns only those names. 504 | 505 | # (0,0,1,1) and 3 are identical masks''' 506 | 507 | # names = self.__col_names 508 | # if isinstance(mask,int): 509 | # mask = bin(mask)[2:] 510 | # return tuple(name for name,exists in zip(names[::-1],mask[::-1])) 511 | # elif isinstance(mask,tuple): 512 | # raise NotImplementedError("easy to implement if needed") 513 | 514 | if __name__ == '__main__': 515 | t = Table(('one','two'),((1,0),(1,1),(0,1))) 516 | try: 517 | t.stage_add(1,(3,3,3)) 518 | except AssertionError: 519 | pass 520 | else: 521 | raise AssertionError("added three columns to two column table") 522 | 523 | #test adds and compression work 524 | t.stage_add(1,(0,3)) #added third 525 | t.stage_add(2,(3,0)) #this will be added first, because it is class_id == (1,0) 526 | t.stage_add(3,(3,3)) #added second 527 | 528 | if True: #for indentation 529 | #staging a guid already staged should over-write it 530 | try: 531 | t.stage_add(1,(10,10)) 532 | except AssertionError: 533 | pass 534 | else: 535 | print "Test failed: re-added staged guid" 536 | 537 | for capacity, sources, targets in t.compress(): 538 | assert capacity == 6, "capacity set to %s instead of 6" % alloc 539 | assert not sources, "no data should be moved:" + str(zip(sources,targets)) 540 | 541 | 542 | #check that data ends up where it should 543 | expected = [(slice(0,3,1),slice(0,0,1)), 544 | (slice(3,6,1),slice(0,3,1)), 545 | (slice(6,6,1),slice(3,6,1)),] 546 | #print "starts:\n",t.print_starts() 547 | #print "sizes:\n",t.print_sizes() 548 | 549 | for guid,e in zip((2,3,1),expected): 550 | this_slice = tuple(t.slices_from_guid(guid)) 551 | assert this_slice == e, "got wrong slices for guid: %s"%guid 552 | 553 | #test that adding an already allocated guid is illegal 554 | try: 555 | t.stage_add(1,(10,10)) 556 | except AssertionError: 557 | pass 558 | else: 559 | print "Test failed: re-added allocated guid" 560 | 561 | #test adding an entity to a non-empty table 562 | t.stage_add(4,(10,10)) 563 | print "staged_adds:",t._staged_adds 564 | 565 | for n, (capacity, sources, targets) in enumerate(t.compress()): 566 | assert capacity == 16, "capacity set to %s instead of 16" % alloc 567 | if n == 0: 568 | assert not sources and not targets, "first component does not move" 569 | elif n == 1: 570 | assert sources == (slice(3,6,1),), "guid 1 started at slice(3,6,1)" 571 | assert targets == (slice(13,16,1),), "guid 1 moves to slice(13,16,1)" 572 | 573 | #test getting slices for sections 574 | #TODO need to test that non-continuous columns fail and other edge cases 575 | t = Table(('one','two'),((1,0),(1,1),(0,1))) 576 | t.stage_add(1 ,(0,3)) 577 | t.stage_add(2 ,(0,3)) 578 | t.stage_add(3 ,(0,3)) 579 | t.stage_add(4 ,(0,3)) 580 | t.stage_add(5 ,(0,3)) 581 | t.stage_add(6 ,(3,0)) 582 | t.stage_add(7 ,(3,0)) 583 | t.stage_add(8 ,(3,0)) 584 | t.stage_add(9 ,(3,0)) 585 | t.stage_add(10,(3,0)) 586 | t.stage_add(13,(3,3)) 587 | t.stage_add(14,(3,3)) 588 | t.stage_add(15,(3,3)) 589 | t.stage_add(16,(3,3)) 590 | t.stage_add(17,(3,3)) 591 | t.stage_add(18,(3,3)) 592 | t.compress() 593 | expected = {(1,0):slice(0,5,1),(1,1):slice(5,11,1),(0,1):slice(11,None,1)} 594 | assert t.section_slices()==expected, "section_slices should return expected result" 595 | assert t.mask_slices(('one','two'),())[0] == {'one':slice(15, 33, None), 'two':slice(0, 18, None)} 596 | -------------------------------------------------------------------------------- /planning.txt: -------------------------------------------------------------------------------- 1 | Renderable(): 2 | inputs: 3 | vertices 4 | colors 5 | outputs: 6 | IO 7 | 8 | Transformable(): 9 | inputs: 10 | position 11 | angle 12 | outputs: 13 | vertices 14 | 15 | Colored(): 16 | inputs: 17 | color 18 | outputs: 19 | colors 20 | 21 | (Transformable, Colored) -> Renderable 22 | 23 | Car(Transformable, Colored): 24 | inputs: 25 | models 26 | outputs: 27 | vertices 28 | colors 29 | 30 | ^ That's too complicated. Let's make it more atomic. 31 | 32 | Renderable(vertices, colors): 33 | outputs: 34 | IO 35 | 36 | SolidColored_Rotateable_RegularPolygon(n,r,color,pos,angle): 37 | n,r and color are immutable 38 | pos and angle are mutable 39 | xhelper and yhelper are internal state 40 | 41 | outputs: 42 | vertices 43 | colors 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name = "Numpy-ECS", 5 | version = "0.01", 6 | author = "Elliot Hallmark", 7 | author_email = "permafacture@gmail.com", 8 | description = ("An Entity Component System framework built on numpy arrays"), 9 | license = "GPLv2", 10 | keywords = "data oriented programming, ECS", 11 | url = "https://github.com/Permafacture/data-oriented-pyglet", 12 | install_requires = ['numpy','pyglet'], 13 | packages=['numpy_ecs',], 14 | ) 15 | --------------------------------------------------------------------------------