├── .gitignore ├── LICENSE ├── README.rst ├── _docs ├── README_RUS.rst ├── _NOTES.txt ├── _repo_mirror.txt ├── dev_requirements.txt ├── packaging_notes.txt ├── release_notes.rst ├── sites and guides │ ├── 2018Academy - ECS-DoD.pdf │ ├── How to make a simple entity-component-system in C++.zip │ └── Шаблон проектирования Entity-Component-System — реализация и пример игры _ Хабр.zip └── Временная сложность.txt ├── ecs_pattern ├── __init__.py └── ecs.py ├── examples ├── pong │ ├── README.rst │ ├── __init__.py │ ├── _check.py │ ├── _docs │ │ ├── demo.gif │ │ └── pong_screen.png │ ├── _requirements.txt │ ├── components.py │ ├── consts.py │ ├── entities.py │ ├── main.py │ ├── sprites.py │ └── systems.py ├── snow_day │ ├── README.rst │ ├── _img │ │ ├── demo.gif │ │ ├── fog.png │ │ ├── landscape.jpg │ │ ├── light_shine.png │ │ ├── snowflake orig.jpg │ │ ├── snowflake.png │ │ └── snowflake.psd │ ├── common_tools │ │ ├── __init__.py │ │ ├── components.py │ │ ├── consts.py │ │ ├── resources.py │ │ └── surface.py │ ├── main.py │ ├── requirements.txt │ └── scene1 │ │ ├── __init__.py │ │ ├── entities.py │ │ ├── main.py │ │ ├── surfaces.py │ │ └── systems.py └── trig │ ├── README.rst │ ├── _img │ └── demo.gif │ ├── blacklist.txt │ ├── commands │ ├── build_apk.sh │ ├── build_exe.cmd │ └── p4a_clean.sh │ ├── common_tools │ ├── __init__.py │ ├── compatibility.py │ ├── components.py │ ├── consts.py │ ├── debug.py │ ├── gui.py │ ├── i18n.py │ ├── locale │ │ └── ru │ │ │ └── LC_MESSAGES │ │ │ ├── trig_fall.mo │ │ │ └── trig_fall.po │ ├── math.py │ ├── matrix.py │ ├── resources.py │ ├── settings.py │ └── surface.py │ ├── fall │ ├── __init__.py │ ├── entities.py │ ├── main.py │ ├── surfaces.py │ └── systems.py │ ├── i18n_compile.cmd │ ├── i18n_make.cmd │ ├── main.py │ ├── menu │ ├── __init__.py │ ├── entities.py │ ├── main.py │ ├── surfaces.py │ └── systems.py │ └── requirements.txt ├── setup.py ├── tests ├── __init__.py └── test_ecs.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.orig 2 | __pycache__ 3 | MANIFEST 4 | /.idea/ 5 | /.tox 6 | /*.egg-info/ 7 | /build/ 8 | /dist/ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Kaukin Vladimir 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. http://docutils.sourceforge.net/docs/user/rst/quickref.html 2 | 3 | ======================================================================================================================== 4 | ecs_pattern 🚀 5 | ======================================================================================================================== 6 | 7 | Implementation of the ECS pattern (Entity Component System) for creating games. 8 | 9 | Make a game instead of architecture for a game. 10 | 11 | `Документация на Русском `_. 12 | 13 | .. image:: https://img.shields.io/pypi/dm/ecs_pattern.svg?style=social 14 | 15 | =============== ==================================================================================== 16 | Python version 3.3+ 17 | License Apache-2.0 18 | PyPI https://pypi.python.org/pypi/ecs_pattern/ 19 | Dependencies dataclasses before 3.7, typing before 3.5 20 | Repo mirror https://gitflic.ru/project/ikvk/ecs-pattern 21 | =============== ==================================================================================== 22 | 23 | .. contents:: 24 | 25 | Intro 26 | ======================================================================================================================== 27 | | ECS - Entity-Component-System - it is an architectural pattern created for game development. 28 | 29 | It is great for describing a dynamic virtual world. 30 | 31 | Basic principles of ECS: 32 | 33 | * Composition over inheritance 34 | * Data separated from logic (Data Oriented Design) 35 | 36 | | *Component* - Property with object data 37 | | *Entity* - Container for properties 38 | | *System* - Data processing logic 39 | | *EntityManager* - Entity database 40 | | *SystemManager* - Container for systems 41 | 42 | Installation 43 | ======================================================================================================================== 44 | :: 45 | 46 | $ pip install ecs-pattern 47 | 48 | Guide 49 | ======================================================================================================================== 50 | 51 | The library provides you with 5 tools: 52 | 53 | .. code-block:: python 54 | 55 | from ecs_pattern import component, entity, EntityManager, System, SystemManager 56 | 57 | * Describe components - component 58 | * Describe entities based on components - entity 59 | * Distribute the responsibility of processing entities by systems - System 60 | * Store entities in entity manager - EntityManager 61 | * Manage your systems with SystemManager 62 | 63 | Component 64 | ------------------------------------------------------------------------------------------------------------------------ 65 | | Property with object data. Contains only data, no logic. 66 | 67 | | Use the ecs_pattern.component decorator to create components. 68 | 69 | | Technically this is python dataclass. 70 | 71 | | Use components as mixins for entities. 72 | 73 | .. code-block:: python 74 | 75 | @component 76 | class ComPosition: 77 | x: int = 0 78 | y: int = 0 79 | 80 | @component 81 | class ComPerson: 82 | name: str 83 | health: int 84 | 85 | Entity 86 | ------------------------------------------------------------------------------------------------------------------------ 87 | | Container for properties. Consists of components only. 88 | 89 | | It is forbidden to add attributes to an entity dynamically. 90 | 91 | | Use the ecs_pattern.entity decorator to create entities. 92 | 93 | | Technically this is python dataclass with slots=True. 94 | 95 | | Use EntityManager to store entities. 96 | 97 | .. code-block:: python 98 | 99 | @entity 100 | class Player(ComPosition, ComPerson): 101 | pass 102 | 103 | @entity 104 | class Ball(ComPosition): 105 | pass 106 | 107 | System 108 | ------------------------------------------------------------------------------------------------------------------------ 109 | | Entity processing logic. 110 | 111 | | Does not contain data about entities and components. 112 | 113 | | Use the ecs_pattern.System abstract class to create concrete systems: 114 | 115 | | *system.start* - Initialize the system. It is called once before the main system update cycle. 116 | 117 | | *system.update* - Update the system status. Called in the main loop. 118 | 119 | | *system.stop* - Stops the system. It is called once after the completion of the main loop. 120 | 121 | | Use SystemManager to manage systems. 122 | 123 | .. code-block:: python 124 | 125 | class SysInit(System): 126 | def __init__(self, entities: EntityManager): 127 | self.entities = entities 128 | 129 | def start(self): 130 | self.entities.init( 131 | TeamScoredGoalEvent(Team.LEFT), 132 | Spark(spark_sprite(pygame.display.Info()), 0, 0, 0, 0) 133 | ) 134 | self.entities.add( 135 | GameStateInfo(play=True, pause=False), 136 | WaitForBallMoveEvent(1000), 137 | ) 138 | 139 | class SysGravitation(System): 140 | def __init__(self, entities: EntityManager): 141 | self.entities = entities 142 | 143 | def update(self): 144 | for entity_with_pos in self.entities.get_with_component(ComPosition): 145 | if entity_with_pos.y > 0: 146 | entity_with_pos.y -= 1 147 | 148 | EntityManager 149 | ------------------------------------------------------------------------------------------------------------------------ 150 | | Container for entities. 151 | 152 | | Use class ecs_pattern.EntityManager for creating an entity manager. 153 | 154 | | Time complexity of get_by_class and get_with_component - like a dict 155 | 156 | | *entities.add* - Add entities. 157 | 158 | | *entities.delete* - Delete entities. 159 | 160 | | *entities.delete_buffer_add* - Save entities to the delete buffer to delete later. 161 | 162 | | *entities.delete_buffer_purge* - Delete all entities in the deletion buffer and clear the buffer. 163 | 164 | | *entities.init* - Let manager know about entities. KeyError are raising on access to unknown entities. 165 | 166 | | *entities.get_by_class* - Get all entities of the specified classes. Respects the order of entities. 167 | 168 | | *entities.get_with_component* - Get all entities with the specified components. 169 | 170 | .. code-block:: python 171 | 172 | entities = EntityManager() 173 | entities.add( 174 | Player('Ivan', 20, 1, 2), 175 | Player('Vladimir', 30, 3, 4), 176 | Ball(0, 7) 177 | ) 178 | for entity_with_pos in entities.get_with_component(ComPosition): 179 | print(entity_with_pos.x, entity_with_pos.y) 180 | for player_entity in entities.get_by_class(Player): 181 | print(player_entity.name) 182 | entities.delete_buffer_add(player_entity) 183 | entities.delete_buffer_purge() 184 | entities.delete(*tuple(entities.get_by_class(Ball))) # one line del 185 | 186 | SystemManager 187 | ------------------------------------------------------------------------------------------------------------------------ 188 | | Container for systems. 189 | 190 | | Works with systems in a given order. 191 | 192 | | Use the ecs_pattern.SystemManager class to manage systems. 193 | 194 | | *system_manager.start_systems* - Initialize systems. Call once before the main systems update cycle. 195 | 196 | | *system_manager.update_systems* - Update systems status. Call in the main loop. 197 | 198 | | *system_manager.stop_systems* - Stop systems. Call once after the main loop completes. 199 | 200 | .. code-block:: python 201 | 202 | entities = EntityManager() 203 | entities.add( 204 | Player('Ivan', 20, 1, 2), 205 | Player('Vladimir', 30, 3, 4), 206 | Ball(0, 7) 207 | ) 208 | system_manager = SystemManager([ 209 | SysPersonHealthRegeneration(entities), 210 | SysGravitation(entities) 211 | ]) 212 | system_manager.start_systems() 213 | while play: 214 | system_manager.update_systems() 215 | clock.tick(24) # *pygame clock 216 | system_manager.stop_systems() 217 | 218 | Examples 219 | ======================================================================================================================== 220 | * `Pong `_: game - pygame + ecs_pattern 221 | * `Snow day `_: scene - pygame + ecs_pattern 222 | * `Trig fall `_: commercial game - pygame + ecs_pattern + numpy 223 | 224 | Advantages 225 | ======================================================================================================================== 226 | * Memory efficient - Component and Entity use dataclass 227 | * Convenient search for objects - by entity class and by entity components 228 | * Flexibility - loose coupling in the code allows you to quickly expand the project 229 | * Modularity - the code is easy to test, analyze performance, and reuse 230 | * Execution control - systems work strictly one after another 231 | * Following the principles of the pattern helps to write quality code 232 | * Convenient to parallelize processing 233 | * Compact implementation 234 | 235 | Difficulties 236 | ======================================================================================================================== 237 | * It can take a lot of practice to learn how to cook ECS properly 238 | * Data is available from anywhere - hard to find errors 239 | 240 | Newbie mistakes 241 | ======================================================================================================================== 242 | * Inheritance of components, entities, systems 243 | * Ignoring the principles of ECS, such as storing data in the system 244 | * Raising ECS to the absolute, no one cancels the OOP 245 | * Adaptation of the existing project code under ECS "as is" 246 | * Use of recursive or reactive logic in systems 247 | * Using EntityManager.delete in get_by_class, get_with_component loops 248 | 249 | Good Practices 250 | ======================================================================================================================== 251 | * Use "Singleton" components with data and flags 252 | * Minimize component change locations 253 | * Do not create methods in components and entities 254 | * Divide the project into scenes, a scene can be considered a cycle for the SystemManager with its EntityManager 255 | * Use packages to separate scenes 256 | 257 | Project tree example: 258 | :: 259 | 260 | /common_tools 261 | __init__.py 262 | resources.py 263 | i18n.py 264 | gui.py 265 | consts.py 266 | components.py 267 | math.py 268 | /menu_scene 269 | __init__.py 270 | entities.py 271 | main_loop.py 272 | surfaces.py 273 | systems.py 274 | /game_scene 275 | __init__.py 276 | entities.py 277 | main_loop.py 278 | surfaces.py 279 | systems.py 280 | main.py 281 | 282 | Releases 283 | ======================================================================================================================== 284 | 285 | History of important changes: `release_notes.rst `_ 286 | 287 | Help the project 288 | ======================================================================================================================== 289 | * Found a bug or have a suggestion - issue / merge request 🎯 290 | * There is nothing to help this project with - help another open project that you are using ✋ 291 | * Nowhere to put the money - spend it on family, friends, loved ones or people around you 💰 292 | * Star the project ⭐ 293 | -------------------------------------------------------------------------------- /_docs/README_RUS.rst: -------------------------------------------------------------------------------- 1 | .. http://docutils.sourceforge.net/docs/user/rst/quickref.html 2 | 3 | ======================================================================================================================== 4 | ecs_pattern 🚀 5 | ======================================================================================================================== 6 | 7 | Реализация шаблона ECS (Entity Component System) для создания игр. 8 | 9 | Делайте игру вместо архитектуры для игры. 10 | 11 | `Documentation in English `_. 12 | 13 | .. image:: https://img.shields.io/pypi/dm/ecs_pattern.svg?style=social 14 | 15 | =============== ========================================== 16 | Python version 3.3+ 17 | License Apache-2.0 18 | PyPI https://pypi.python.org/pypi/ecs_pattern/ 19 | Dependencies dataclasses before 3.7, typing before 3.5 20 | =============== ========================================== 21 | 22 | .. contents:: 23 | 24 | Введение 25 | ======================================================================================================================== 26 | | ECS - Entity-Component-System - это архитектурный шаблон, созданный для разработки игр. 27 | 28 | Он отлично подходит для описания динамического виртуального мира. 29 | 30 | Основные принципы ECS: 31 | 32 | * Композиция важнее наследования (Composition over inheritance) 33 | * Данные отделены от логики (Data Oriented Design) 34 | 35 | | *Component* - Свойство с данными объекта 36 | | *Entity* - Контейнер для свойств 37 | | *System* - Логика обработки данных 38 | | *EntityManager* - База данных сущностей 39 | | *SystemManager* - Контейнер для систем 40 | 41 | Установка 42 | ======================================================================================================================== 43 | :: 44 | 45 | $ pip install ecs-pattern 46 | 47 | Руководство 48 | ======================================================================================================================== 49 | 50 | Библиотека предоставляет вам 5 инструментов: 51 | 52 | .. code-block:: python 53 | 54 | from ecs_pattern import component, entity, EntityManager, System, SystemManager 55 | 56 | * Опишите компоненты - component 57 | * Опишите сущности на основе компонентов - entity 58 | * Распределите ответственность обработки сущностей по системам - System 59 | * Храните сущности в менеджере сущностей - EntityManager 60 | * Управляйте системами менеджером систем - SystemManager 61 | 62 | Component 63 | ------------------------------------------------------------------------------------------------------------------------ 64 | | Свойство с данными объекта. Содержат только данные, без логики. 65 | 66 | | Используйте декоратор ecs_pattern.component для создания компонентов. 67 | 68 | | Технически это python dataclass. 69 | 70 | | Используйте компоненты как миксины для сущностей. 71 | 72 | .. code-block:: python 73 | 74 | @component 75 | class ComPosition: 76 | x: int = 0 77 | y: int = 0 78 | 79 | @component 80 | class ComPerson: 81 | name: str 82 | health: int 83 | 84 | Entity 85 | ------------------------------------------------------------------------------------------------------------------------ 86 | | Контейнер для свойств. Состоит только из компонентов. 87 | 88 | | Запрещено добавлять атрибуты к сущности динамически. 89 | 90 | | Используйте декоратор ecs_pattern.entity для создания сущностей. 91 | 92 | | Технически это python dataclass со slots=True. 93 | 94 | | Используйте EntityManager для хранения сущностей. 95 | 96 | .. code-block:: python 97 | 98 | @entity 99 | class Player(ComPosition, ComPerson): 100 | pass 101 | 102 | @entity 103 | class Ball(ComPosition): 104 | pass 105 | 106 | System 107 | ------------------------------------------------------------------------------------------------------------------------ 108 | | Логика обработки сущностей. 109 | 110 | | Не содержит данных о сущностях и компонентах. 111 | 112 | | Используйте абстрактный класс ecs_pattern.System для создания конкретных систем: 113 | 114 | | *system.start* - Инициализировать систему. Вызывается один раз перед основным циклом обновления системы. 115 | 116 | | *system.update* - Обновить состояние системы. Вызывается в основном цикле. 117 | 118 | | *system.stop* - Остановка системы. Вызывается один раз после завершения основного цикла. 119 | 120 | | Используйте SystemManager для управления системами. 121 | 122 | .. code-block:: python 123 | 124 | class SysInit(System): 125 | def __init__(self, entities: EntityManager): 126 | self.entities = entities 127 | 128 | def start(self): 129 | self.entities.init( 130 | TeamScoredGoalEvent(Team.LEFT), 131 | Spark(spark_sprite(pygame.display.Info()), 0, 0, 0, 0) 132 | ) 133 | self.entities.add( 134 | GameStateInfo(play=True, pause=False), 135 | WaitForBallMoveEvent(1000), 136 | ) 137 | 138 | class SysGravitation(System): 139 | def __init__(self, entities: EntityManager): 140 | self.entities = entities 141 | 142 | def update(self): 143 | for entity_with_pos in self.entities.get_with_component(ComPosition): 144 | if entity_with_pos.y > 0: 145 | entity_with_pos.y -= 1 146 | 147 | EntityManager 148 | ------------------------------------------------------------------------------------------------------------------------ 149 | | Контейнер для сущностей. 150 | 151 | | Используйте класс ecs_pattern.EntityManager для создания менеджера сущностей. 152 | 153 | | Временная сложность get_by_class и get_with_component - как у словаря 154 | 155 | | *entities.add* - Добавить сущности. 156 | 157 | | *entities.delete* - Удалить сущности. 158 | 159 | | *entities.delete_buffer_add* - Сохранить сущности в буфер удаления, чтобы удалить позже. 160 | 161 | | *entities.delete_buffer_purge* - Удалить все сущности в буфере удаления и очистить буффер. 162 | 163 | | *entities.init* - Дать менеджеру знать о сущностях. При доступе к неизвестным объектам бросается KeyError. 164 | 165 | | *entities.get_by_class* - Получить все сущности указанных классов. Учитывает порядок сущностей. 166 | 167 | | *entities.get_with_component* - Получить все сущности с указанными компонентами. 168 | 169 | .. code-block:: python 170 | 171 | entities = EntityManager() 172 | entities.add( 173 | Player('Ivan', 20, 1, 2), 174 | Player('Vladimir', 30, 3, 4), 175 | Ball(0, 7) 176 | ) 177 | for entity_with_pos in entities.get_with_component(ComPosition): 178 | print(entity_with_pos.x, entity_with_pos.y) 179 | for player_entity in entities.get_by_class(Player): 180 | print(player_entity.name) 181 | entities.delete_buffer_add(player_entity) 182 | entities.delete_buffer_purge() 183 | entities.delete(*tuple(entities.get_by_class(Ball))) # one line del 184 | 185 | SystemManager 186 | ------------------------------------------------------------------------------------------------------------------------ 187 | | Контейнер для систем. 188 | 189 | | Работает с системами в заданном порядке. 190 | 191 | | Используйте класс ecs_pattern.SystemManager для управления системами. 192 | 193 | | *system_manager.start_systems* - Инициализировать системы. Вызовите один раз перед главным циклом обновления систем. 194 | 195 | | *system_manager.update_systems* - Обновить состояние систем. Вызывайте в главном цикле. 196 | 197 | | *system_manager.stop_systems* - Завершить работу систем. Вызовите один раз после завершения главного цикла. 198 | 199 | .. code-block:: python 200 | 201 | entities = EntityManager() 202 | entities.add( 203 | Player('Ivan', 20, 1, 2), 204 | Player('Vladimir', 30, 3, 4), 205 | Ball(0, 7) 206 | ) 207 | system_manager = SystemManager([ 208 | SysPersonHealthRegeneration(entities), 209 | SysGravitation(entities) 210 | ]) 211 | system_manager.start_systems() 212 | while play: 213 | system_manager.update_systems() 214 | clock.tick(24) # *pygame clock 215 | system_manager.stop_systems() 216 | 217 | Примеры 218 | ======================================================================================================================== 219 | * `Pong `_: игра - pygame + ecs_pattern 220 | * `Snow day `_: сцена - pygame + ecs_pattern 221 | * `Trig fall `_: коммерческая игра - pygame + ecs_pattern + numpy 222 | 223 | Преимущества 224 | ======================================================================================================================== 225 | * Эффективное использования памяти - Component и Entity используют dataclass 226 | * Удобный поиск объектов - по классу сущности и по компонентам сущности 227 | * Гибкость - слабая связность в коде позволяет быстро расширять проект 228 | * Модульность - код легко тестировать, анализировать производительность, переиспользовать 229 | * Контроль за выполнением - системы работают строго друг за другом 230 | * Следование принципам шаблона помогает писать качественный код 231 | * Удобно распараллеливать обработку 232 | * Компактная реализация 233 | 234 | Сложности 235 | ======================================================================================================================== 236 | * Чтобы научиться правильно готовить ECS, может потребоваться много практики 237 | * Данные доступны откуда угодно - сложно искать ошибки 238 | 239 | Ошибки новичка 240 | ======================================================================================================================== 241 | * Наследование компонентов, сущностей, систем 242 | * Игнорирование принципов ECS, например хранение данных в системе 243 | * Возведение ECS в абсолют, ООП никто не отменяет 244 | * Адаптация существующего кода проекта под ECS "как есть" 245 | * Использование рекурсивной или реактивной логики в системах 246 | * Использование EntityManager.delete в циклах get_by_class, get_with_component 247 | 248 | Хорошие практики 249 | ======================================================================================================================== 250 | * Используйте компоненты "одиночки (Singleton)" с данными и флагами 251 | * Минимизируйте места изменения компонента 252 | * Не создавайте методы в компонентах и сущностях 253 | * Делите проект на сцены, сценой можно считать цикл для SystemManager с его EntityManager 254 | * Используйте пакеты для разделения сцен 255 | 256 | Пример дерева проекта: 257 | :: 258 | 259 | /common_tools 260 | __init__.py 261 | resources.py 262 | i18n.py 263 | gui.py 264 | consts.py 265 | components.py 266 | math.py 267 | /menu_scene 268 | __init__.py 269 | entities.py 270 | main_loop.py 271 | surfaces.py 272 | systems.py 273 | /game_scene 274 | __init__.py 275 | entities.py 276 | main_loop.py 277 | surfaces.py 278 | systems.py 279 | main.py 280 | 281 | Релизы 282 | ======================================================================================================================== 283 | 284 | История важных изменений: `release_notes.rst `_ 285 | 286 | Помощь проекту 287 | ======================================================================================================================== 288 | * Нашли ошибку или есть предложение - issue / merge request 🎯 289 | * Нечем помочь этому проекту - помогите другому открытому проекту, который используете ✋ 290 | * Некуда деть деньги - потратьте на семью, друзей, близких или окружающих вас людей 💰 291 | * Поставьте проекту ⭐ 292 | -------------------------------------------------------------------------------- /_docs/_NOTES.txt: -------------------------------------------------------------------------------- 1 | 2 | удаление большой пачки сущностей за раз приведет к задержке, подумать над оптимизацией на стороне либы 3 | ...пробовать удалять раз в интервал, или пачку? 4 | 5 | multiproc phys example 6 | 7 | weakref и weakref_slot в dataclasses не подходят - после удаления объект доступен по слабой ссылке до сборки мусора 8 | 9 | https://github.com/python-attrs/attrs - выгода минимальна 10 | 11 | диаграмма Вороного 12 | 13 | Physarum или Slime Mold Simulation. 14 | 15 | multiproc EntityManager + SystemManager auto get data part by proc uid 16 | https://stackoverflow.com/questions/10190981/get-a-unique-id-for-worker-in-python-multiprocessing-pool 17 | -------------------------------------------------------------------------------- /_docs/_repo_mirror.txt: -------------------------------------------------------------------------------- 1 | 2 | If you (or I am) encounter a problem accessing the repository, you can use the projects mirror: 3 | 4 | * https://github.com/ikvk 5 | * https://gitflic.ru/user/ikvk 6 | -------------------------------------------------------------------------------- /_docs/dev_requirements.txt: -------------------------------------------------------------------------------- 1 | # build 2 | setuptools 3 | twine 4 | wheel 5 | 6 | # test 7 | tox==3.28.0 8 | -------------------------------------------------------------------------------- /_docs/packaging_notes.txt: -------------------------------------------------------------------------------- 1 | Packaging 2 | ========= 3 | 4 | https://packaging.python.org/tutorials/packaging-projects/ 5 | 6 | * Иногда для применения некоторых настроек билда нужно удалить старые папки билдов 7 | 8 | 1. Create .pypirc 9 | C:\Users\v.kaukin\.pypirc 10 | https://docs.python.org/3/distutils/packageindex.html#pypirc 11 | 12 | 2. Install/update build libs: 13 | C:\python\venv\ecs_pattern310\Scripts\python.exe -m pip install --upgrade pip 14 | C:\python\venv\ecs_pattern310\Scripts\pip install --upgrade -r C:\kvk\develop\Python\ecs_pattern\_docs\dev_requirements.txt 15 | 16 | 3. Generating distribution archives 17 | cd C:\kvk\develop\Python\ecs_pattern\ 18 | C:\python\venv\ecs_pattern310\Scripts\python.exe C:\kvk\develop\Python\ecs_pattern\setup.py sdist bdist_wheel 19 | 20 | 4. Check the distribution archives: 21 | C:\python\venv\ecs_pattern310\Scripts\python.exe -m twine check dist/* 22 | 23 | 5. Uploading the distribution archives: 24 | C:\python\venv\ecs_pattern310\Scripts\python.exe -m twine upload dist/* 25 | 26 | 27 | Mirror 28 | ====== 29 | 1. cd to repo 30 | cd C:\kvk\develop\Python\ecs_pattern\ 31 | 2. Cleaning old references to remote branches 32 | git fetch --prune 33 | 3. Push 34 | git push --prune git@gitflic.ru:ikvk/ecs-pattern.git +refs/remotes/origin/*:refs/heads/* +refs/tags/*:refs/tags/* 35 | 36 | https://dev.to/sourcelevel/how-to-properly-mirror-a-git-repository-19d9 -------------------------------------------------------------------------------- /_docs/release_notes.rst: -------------------------------------------------------------------------------- 1 | 1.3.0 2 | ===== 3 | * delete_buffer_purge,delete_buffer_add - allowed to mark the object for deletion more than 1 time 4 | 5 | 1.2.0 6 | ===== 7 | * EntityManager.get_with_component optimization 8 | 9 | 1.1.3 10 | ===== 11 | * Update docs 12 | 13 | 1.1.2 14 | ===== 15 | * Update meta info, docs 16 | * Improve Pong example 17 | 18 | 1.1.1 19 | ===== 20 | * Update meta info 21 | 22 | 1.1.0 23 | ===== 24 | * Added delete buffer: EntityManager.delete_buffer_add, EntityManager.delete_buffer_purge 25 | 26 | 1.0.0 27 | ===== 28 | * first version: 24 Aug 2022 29 | -------------------------------------------------------------------------------- /_docs/sites and guides/2018Academy - ECS-DoD.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/_docs/sites and guides/2018Academy - ECS-DoD.pdf -------------------------------------------------------------------------------- /_docs/sites and guides/How to make a simple entity-component-system in C++.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/_docs/sites and guides/How to make a simple entity-component-system in C++.zip -------------------------------------------------------------------------------- /_docs/sites and guides/Шаблон проектирования Entity-Component-System — реализация и пример игры _ Хабр.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/_docs/sites and guides/Шаблон проектирования Entity-Component-System — реализация и пример игры _ Хабр.zip -------------------------------------------------------------------------------- /_docs/Временная сложность.txt: -------------------------------------------------------------------------------- 1 | https://wiki.python.org/moin/TimeComplexity 2 | -------------------------------------------------------------------------------- /ecs_pattern/__init__.py: -------------------------------------------------------------------------------- 1 | # Lib author: Vladimir Kaukin 2 | # Project home page: https://github.com/ikvk/ecs_pattern 3 | # Mirror: https://gitflic.ru/project/ikvk/ecs-pattern 4 | # License: Apache-2.0 5 | 6 | from .ecs import component, entity, EntityManager, System, SystemManager 7 | 8 | __version__ = '1.3.0' 9 | -------------------------------------------------------------------------------- /ecs_pattern/ecs.py: -------------------------------------------------------------------------------- 1 | """ 2 | ECS - Entity Component system 3 | """ 4 | 5 | from typing import Iterable, Iterator, Any 6 | from dataclasses import dataclass 7 | from functools import partial 8 | from collections import deque 9 | 10 | # all component classes must be decorated with this function 11 | # 3 class with @dataclass(slots=True) - A, B, C, then C(A, B): TypeError: multiple bases have instance lay-out conflict 12 | component = dataclass 13 | 14 | # all entity classes must be decorated with this function 15 | entity = partial(dataclass, slots=True) 16 | 17 | 18 | class EntityManager: 19 | """Entity manager""" 20 | 21 | def __init__(self): 22 | self._entity_map = {} # Person: [ent1, ent2] 23 | self._entity_components_map = {} # Person: {MoveCom, DamageCom, NameCom} 24 | # *needed for set().issubset() 25 | self._set_cache_map = {} # (MoveCom, DamageCom, NameCom): {MoveCom, DamageCom, NameCom} 26 | self._delete_entity_buffer = deque() # deque([Person1, Person2]) 27 | 28 | def add(self, *entity_value_list: Any): 29 | """Add entities to world""" 30 | for entity_value in entity_value_list: 31 | assert getattr(entity_value, '__dict__', None) in (None, {}), 'Data class with inefficient memory usage' 32 | entity_value_class = entity_value.__class__ 33 | self._entity_map.setdefault(entity_value_class, []).append(entity_value) 34 | if entity_value_class not in self._entity_components_map: 35 | self._entity_components_map[entity_value_class] = \ 36 | {i for i in entity_value_class.__mro__ if i is not object} 37 | 38 | def delete(self, *entity_value_list: Any): 39 | """Delete entities from world""" 40 | for entity_value in entity_value_list: 41 | self._entity_map[entity_value.__class__].remove(entity_value) 42 | 43 | def delete_buffer_add(self, *entity_value_list: Any): 44 | """Save entities into delete buffer for delete them from world later""" 45 | for entity_value in entity_value_list: 46 | self._delete_entity_buffer.append(entity_value) 47 | 48 | def delete_buffer_purge(self): 49 | """Delete all entities from delete buffer""" 50 | for delete_entity in self._delete_entity_buffer: 51 | try: 52 | self.delete(delete_entity) 53 | except ValueError: 54 | # the object has been marked for deletion more than 1 time. 55 | pass 56 | self._delete_entity_buffer.clear() 57 | 58 | def init(self, *entity_list: Any): 59 | """ 60 | Let entity manager to "know" about entities before work 61 | If manager do not know about entity, it will raise KeyError on access to it. 62 | event: SomeEvent = next(self.entities.get_by_class(SomeEvent), None) 63 | """ 64 | for ent in entity_list: 65 | self.add(ent) 66 | self.delete(ent) 67 | 68 | def get_by_class(self, *entity_class_val_list: type) -> Iterator[Any]: 69 | """ 70 | Get all entities by specified entity classes in specified order 71 | raise KeyError for uninitialized (never added) entities 72 | """ 73 | for entity_class_val in entity_class_val_list: 74 | yield from self._entity_map[entity_class_val] 75 | 76 | def get_with_component(self, *component_class_val_list: type) -> Iterator[Any]: 77 | """ 78 | Get all entities that contains all specified component classes 79 | Sometimes it will be useful to warm up the cache 80 | raise KeyError for uninitialized (never added) entities 81 | """ 82 | component_class_val_set = \ 83 | self._set_cache_map.setdefault(component_class_val_list, set(component_class_val_list)) 84 | for entity_class, entity_component_set in self._entity_components_map.items(): 85 | if component_class_val_set.issubset(entity_component_set): 86 | yield from self._entity_map[entity_class] 87 | 88 | 89 | class System: 90 | """ 91 | Abstract base class for system 92 | All systems must be derived from this class 93 | System should have data for work: implement __init__ method 94 | """ 95 | 96 | def start(self): 97 | """ 98 | Preparing system to work before starting SystemManager.systems_update loop 99 | Runs by SystemManager.start_systems 100 | """ 101 | 102 | def update(self): 103 | """ 104 | Run main system logic 105 | Runs by SystemManager.update_systems 106 | """ 107 | 108 | def stop(self): 109 | """ 110 | Clean system resources after stop update loop 111 | Runs by SystemManager.stop_systems 112 | """ 113 | 114 | 115 | class SystemManager: 116 | """System manager""" 117 | 118 | def __init__(self, system_list: Iterable[System]): 119 | """ 120 | system_list: Ordered sequence with systems 121 | """ 122 | self._system_list = tuple(system_list) 123 | self._system_with_start_list = tuple(i for i in self._system_list if hasattr(i, 'start')) 124 | self._system_with_update_list = tuple(i for i in self._system_list if hasattr(i, 'update')) 125 | self._system_with_stop_list = tuple(i for i in self._system_list if hasattr(i, 'stop')) 126 | 127 | def start_systems(self): 128 | """Start all systems""" 129 | for system in self._system_with_start_list: 130 | system.start() 131 | 132 | def update_systems(self): 133 | """Update all systems""" 134 | for system in self._system_with_update_list: 135 | system.update() 136 | 137 | def stop_systems(self): 138 | """Stop all systems""" 139 | for system in self._system_with_stop_list: 140 | system.stop() 141 | -------------------------------------------------------------------------------- /examples/pong/README.rst: -------------------------------------------------------------------------------- 1 | .. http://docutils.sourceforge.net/docs/user/rst/quickref.html 2 | 3 | Pong - classic game 4 | ======================================================================================================================== 5 | 6 | Classic game, implemented on pygame + ecs_pattern 7 | 8 | ⚠ This game is not collection of best practice of architecture or optimization - just an example of usage ecs_pattern. 9 | 10 | .. image:: https://github.com/ikvk/ecs_pattern/blob/master/examples/pong/_docs/demo.gif 11 | 12 | .. image:: https://github.com/ikvk/ecs_pattern/blob/master/examples/pong/_docs/pong_screen.png 13 | -------------------------------------------------------------------------------- /examples/pong/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/pong/__init__.py -------------------------------------------------------------------------------- /examples/pong/_check.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pygame 3 | 4 | from ecs_pattern import EntityManager 5 | 6 | from entities import Ball, GameStateInfo, Racket, Score, Table, TeamScoredGoalEvent, WaitForBallMoveEvent 7 | from sprites import ball_sprite, racket_sprite, table_sprite, score_sprite 8 | from consts import Team, BALL_SIZE, RACKET_WIDTH, RACKET_HEIGHT 9 | from components import ComVisible, ComMotion, ComScore, ComTeam, ComWait 10 | 11 | pygame.init() 12 | screen_info = pygame.display.Info() 13 | 14 | score_spr = score_sprite(0) 15 | ball_spr = ball_sprite(screen_info) 16 | racket_spr = racket_sprite(screen_info) 17 | table_spr = table_sprite(screen_info) 18 | 19 | 20 | def show_memory_usage(function): 21 | """ 22 | if __name__ == '__main__': 23 | show_memory_usage(leak) 24 | """ 25 | from memory_profiler import memory_usage 26 | mem_usage = memory_usage(function, interval=.1) 27 | print('Maximum memory usage for {}: {} Mb'.format(function.__name__, max(mem_usage))) 28 | 29 | 30 | def _add_10_entities(entity_manager): 31 | """ 32 | 1_000_000 add 33 | _entity_map - [] 34 | 20.662492752075195 sec 35 | Maximum memory usage for entity_manager: 1949.73828125 Mb 36 | _entity_map - deque() 37 | 21.873197555541992 sec 38 | Maximum memory usage for entity_manager: 1954.2734375 Mb 39 | 40 | 100_000 add 41 | _entity_map - [] 42 | 1.790226697921753 sec 43 | Maximum memory usage for entity_manager: 238.71875 Mb 44 | _entity_map - deque() 45 | 1.8341341018676758 sec 46 | Maximum memory usage for entity_manager: 240.05859375 Mb 47 | """ 48 | entity_manager.add( 49 | GameStateInfo(play=True, pause=False), 50 | GameStateInfo(play=False, pause=False), 51 | TeamScoredGoalEvent(Team.RIGHT), 52 | WaitForBallMoveEvent(1000), 53 | Score( 54 | sprite=score_spr, 55 | x=int(screen_info.current_w * 0.25), 56 | y=int(screen_info.current_h * 0.2), 57 | team=Team.LEFT, 58 | score=0 59 | ), 60 | Score( 61 | sprite=score_spr, 62 | x=int(screen_info.current_w * 0.75), 63 | y=int(screen_info.current_h * 0.2), 64 | team=Team.RIGHT, 65 | score=0 66 | ), 67 | Ball( 68 | sprite=ball_spr, 69 | x=int(screen_info.current_w * 0.5 - BALL_SIZE * screen_info.current_h / 2), 70 | y=int(screen_info.current_h * 0.5 - BALL_SIZE * screen_info.current_h / 2), 71 | speed_x=0, speed_y=0 72 | ), 73 | Racket( 74 | sprite=racket_spr, 75 | x=0, 76 | y=int(screen_info.current_h / 2 - screen_info.current_h * RACKET_HEIGHT / 2), 77 | team=Team.LEFT, 78 | speed_x=0, speed_y=0 79 | ), 80 | Racket( 81 | sprite=racket_spr, 82 | x=int(screen_info.current_w - screen_info.current_h * RACKET_WIDTH), 83 | y=int(screen_info.current_h / 2 - screen_info.current_h * RACKET_HEIGHT / 2), 84 | team=Team.RIGHT, 85 | speed_x=0, speed_y=0 86 | ), 87 | Table( 88 | sprite=table_spr, 89 | x=0, 90 | y=0 91 | ), 92 | ) 93 | 94 | 95 | def entity_manager_access(): 96 | """ 97 | _entity_map 98 | 99 | access 20_000 get_with_component 100 | [] 101 | 13.37865400314331 sec 102 | Maximum memory usage for entity_manager: 52.3203125 Mb 103 | deque() 104 | 14.869358777999878 sec 105 | Maximum memory usage for entity_manager: 51.96875 Mb 106 | 107 | access 20_000 get_by_class 108 | [] 109 | 15.288840770721436 sec 110 | Maximum memory usage for entity_manager: 52.29296875 Mb 111 | deque() 112 | 17.244649410247803 sec 113 | Maximum memory usage for entity_manager: 52.38671875 Mb 114 | """ 115 | 116 | entities = EntityManager() 117 | 118 | t = time.time() 119 | 120 | for k in range(1_000): 121 | _add_10_entities(entities) 122 | 123 | for k in range(20_000): 124 | for _ in entities.get_with_component(ComVisible, ComMotion, ComScore, ComTeam, ComWait): 125 | pass 126 | for _ in entities.get_with_component(ComVisible): 127 | pass 128 | for _ in entities.get_with_component(ComMotion): 129 | pass 130 | for _ in entities.get_with_component(ComScore): 131 | pass 132 | for _ in entities.get_with_component(ComTeam): 133 | pass 134 | for _ in entities.get_with_component(ComWait): 135 | pass 136 | 137 | # * при тесте надо закомментить 1 из частей - get_with_component или get_by_class 138 | 139 | for _ in entities.get_by_class(Ball, GameStateInfo, Racket, Score, Table, TeamScoredGoalEvent, 140 | WaitForBallMoveEvent): 141 | pass 142 | for _ in entities.get_by_class(Ball): 143 | pass 144 | for _ in entities.get_by_class(GameStateInfo): 145 | pass 146 | for _ in entities.get_by_class(Racket): 147 | pass 148 | for _ in entities.get_by_class(Score): 149 | pass 150 | for _ in entities.get_by_class(Table): 151 | pass 152 | for _ in entities.get_by_class(TeamScoredGoalEvent): 153 | pass 154 | for _ in entities.get_by_class(WaitForBallMoveEvent): 155 | pass 156 | 157 | print(time.time() - t, 'sec') 158 | 159 | 160 | def entity_manager_delete_buffer(): 161 | """ 162 | 100_000 163 | 164 | [] 165 | 20.23709201812744 sec 166 | Maximum memory usage for entity_manager_delete_buffer: 249.19140625 Mb 167 | deque() 168 | 19.756350994110107 sec 169 | Maximum memory usage for entity_manager_delete_buffer: 248.75390625 Mb 170 | 171 | deque() pop IndexError 172 | 20.462244749069214 sec 173 | Maximum memory usage for entity_manager_delete_buffer: 249.0703125 Mb 174 | 175 | from queue import Queue, Empty - get_nowait 176 | 24.581512689590454 sec 177 | Maximum memory usage for entity_manager_delete_buffer: 248.80859375 Mb 178 | """ 179 | 180 | entities = EntityManager() 181 | 182 | t = time.time() 183 | 184 | for i in range(100_000): 185 | _add_10_entities(entities) 186 | 187 | for ent in entities.get_by_class(Ball, GameStateInfo, Racket, Score, Table, TeamScoredGoalEvent, 188 | WaitForBallMoveEvent): 189 | entities.delete_buffer_add(ent) 190 | entities.delete_buffer_purge() 191 | 192 | print(time.time() - t, 'sec') 193 | 194 | 195 | def entity_dataclass_slots(): 196 | """ 197 | Checking entity dataclass slots 198 | 199 | Dataclass that inherits another dataclass (1 on N super classes): 200 | on slots=1: contains empty __dict__ 201 | on slots=0: contains full __dict__ 202 | I do not found reason for empty __dict__ in docs 203 | I suppose that it is enough for save memory in python and can not be better 204 | 205 | component = dataclass 206 | entity = partial(dataclass, slots=True) 207 | 1_000_000 - Maximum memory usage for entity_dataclass_slots: 1095.3125 Mb 208 | 100_000 - Maximum memory usage for entity_dataclass_slots: 160.890625 Mb 209 | 210 | component = dataclass 211 | entity = partial(dataclass, slots=False) 212 | 1_000_000 - Maximum memory usage for entity_dataclass_slots: 1940.8984375 Mb 213 | 100_000 - Maximum memory usage for entity_dataclass_slots: 241.96484375 Mb 214 | 215 | component = dataclass 216 | entity = dataclass 217 | 1_000_000 - Maximum memory usage for entity_dataclass_slots: 1940.67578125 Mb 218 | 100_000 - Maximum memory usage for entity_dataclass_slots: 242.8984375 Mb 219 | 220 | """ 221 | entities = EntityManager() 222 | for i in range(1_000_000): 223 | _add_10_entities(entities) 224 | 225 | 226 | def lib_dataclass_mem(): 227 | """ 228 | 8_000_000 229 | Maximum memory usage for lib_dataclass_mem: 3250.58984375 Mb 230 | 2_000_000 231 | Maximum memory usage for lib_dataclass_mem: 830.5625 Mb 232 | 200_000 233 | Maximum memory usage for lib_dataclass_mem: 126.40625 Mb 234 | """ 235 | from dataclasses import dataclass, field 236 | cnt = 8_000_000 237 | 238 | @dataclass 239 | class Class1: 240 | number: int = 42 241 | list_of_numbers: list = field(default_factory=list) 242 | string: str = '' 243 | 244 | data = [] 245 | for i in range(cnt): 246 | data.append(Class1(i, [1, 2, 3], str(i) * 5)) 247 | 248 | 249 | def lib_attrs_mem(): 250 | """ 251 | 8_000_000 252 | Maximum memory usage for lib_attrs_mem: 2552.12890625 Mb 253 | 2_000_000 254 | Maximum memory usage for lib_attrs_mem: 643.125 Mb 255 | 200_000 256 | Maximum memory usage for lib_attrs_mem: 103.33203125 Mb 257 | """ 258 | from attrs import define, Factory 259 | cnt = 8_000_000 260 | 261 | @define 262 | class Class1: 263 | number: int = 42 264 | list_of_numbers: list[int] = Factory(list) 265 | string: str = '' 266 | 267 | data = [] 268 | for i in range(cnt): 269 | data.append(Class1(i, [1, 2, 3], str(i) * 5)) 270 | 271 | 272 | if __name__ == '__main__': 273 | # show_memory_usage(entity_manager_access) 274 | # show_memory_usage(entity_manager_delete_buffer) 275 | # show_memory_usage(entity_dataclass_slots) 276 | # show_memory_usage(lib_dataclass_mem) 277 | # show_memory_usage(lib_attrs_mem) 278 | 279 | pass 280 | -------------------------------------------------------------------------------- /examples/pong/_docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/pong/_docs/demo.gif -------------------------------------------------------------------------------- /examples/pong/_docs/pong_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/pong/_docs/pong_screen.png -------------------------------------------------------------------------------- /examples/pong/_requirements.txt: -------------------------------------------------------------------------------- 1 | pygame==2.1.2 -------------------------------------------------------------------------------- /examples/pong/components.py: -------------------------------------------------------------------------------- 1 | from pygame.sprite import Sprite 2 | from ecs_pattern import component 3 | 4 | from consts import Team 5 | 6 | 7 | @component 8 | class ComVisible: 9 | sprite: Sprite 10 | x: int 11 | y: int 12 | 13 | 14 | @component 15 | class ComMotion: 16 | speed_x: int 17 | speed_y: int 18 | 19 | 20 | @component 21 | class ComScore: 22 | score: int 23 | 24 | 25 | @component 26 | class ComTeam: 27 | team: int 28 | 29 | def __post_init__(self): 30 | assert self.team in Team.values, 'team value not in Team.values' 31 | 32 | 33 | @component 34 | class ComWait: 35 | wait_ms: int 36 | -------------------------------------------------------------------------------- /examples/pong/consts.py: -------------------------------------------------------------------------------- 1 | class Team: 2 | LEFT = 1 3 | RIGHT = 2 4 | names = ( 5 | (LEFT, "Left"), 6 | (RIGHT, "Right"), 7 | ) 8 | values = (LEFT, RIGHT) 9 | 10 | 11 | FPS_MAX = 100 12 | FPS_CORR = 24 / FPS_MAX 13 | 14 | BALL_SIZE = 0.03 # relative to screen height 15 | BALL_SPEED_MIN = 0.02 * FPS_CORR # relative to screen height 16 | 17 | RACKET_WIDTH = BALL_SIZE 18 | RACKET_HEIGHT = 0.22 # relative to screen height 19 | RACKET_SPEED = 0.03 * FPS_CORR # relative to screen height 20 | -------------------------------------------------------------------------------- /examples/pong/entities.py: -------------------------------------------------------------------------------- 1 | from ecs_pattern import entity 2 | 3 | from components import ComMotion, ComScore, ComTeam, ComVisible, ComWait 4 | 5 | 6 | @entity 7 | class Ball(ComMotion, ComVisible): 8 | pass 9 | 10 | 11 | @entity 12 | class Racket(ComMotion, ComTeam, ComVisible): 13 | pass 14 | 15 | 16 | @entity 17 | class Table(ComVisible): 18 | pass 19 | 20 | 21 | @entity 22 | class Score(ComScore, ComTeam, ComVisible): 23 | pass 24 | 25 | 26 | @entity 27 | class Spark(ComMotion, ComVisible): 28 | pass 29 | 30 | 31 | @entity 32 | class TeamScoredGoalEvent(ComTeam): 33 | pass 34 | 35 | 36 | @entity 37 | class WaitForBallMoveEvent(ComWait): 38 | pass 39 | 40 | 41 | @entity 42 | class GameStateInfo: 43 | play: bool 44 | pause: bool 45 | -------------------------------------------------------------------------------- /examples/pong/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pygame 4 | from ecs_pattern import EntityManager, SystemManager 5 | 6 | from consts import FPS_MAX 7 | from entities import GameStateInfo 8 | from systems import SysControl, SysDraw, SysGoal, SysMovement, SysInit, SysRoundStarter 9 | 10 | os.environ['SDL_VIDEO_CENTERED'] = '1' # window at center 11 | 12 | 13 | def pong(): 14 | """Pong game""" 15 | 16 | pygame.init() # init all imported pygame modules 17 | pygame.display.set_caption('Pong') 18 | screen = pygame.display.set_mode((800, 500)) # w h 19 | clock = pygame.time.Clock() 20 | 21 | entities = EntityManager() 22 | 23 | system_manager = SystemManager([ 24 | SysInit(entities), 25 | SysControl(entities, pygame.event.get), 26 | SysMovement(entities), 27 | SysRoundStarter(entities, clock), 28 | SysGoal(entities), 29 | SysDraw(entities, screen), 30 | ]) 31 | 32 | system_manager.start_systems() 33 | 34 | game_state_info: GameStateInfo = next(entities.get_by_class(GameStateInfo)) 35 | while game_state_info.play: 36 | clock.tick_busy_loop(FPS_MAX) # tick_busy_loop точный + ест проц, tick грубый + не ест проц 37 | system_manager.update_systems() 38 | pygame.display.flip() # draw changes on screen 39 | 40 | system_manager.stop_systems() 41 | 42 | 43 | if __name__ == '__main__': 44 | pong() 45 | -------------------------------------------------------------------------------- /examples/pong/sprites.py: -------------------------------------------------------------------------------- 1 | from pygame import Surface, Color 2 | from pygame.sprite import Sprite 3 | from pygame.font import Font 4 | from pygame.display import Info as VideoInfo 5 | 6 | from consts import BALL_SIZE, RACKET_WIDTH, RACKET_HEIGHT 7 | 8 | 9 | class ColoredBlockSprite(Sprite): 10 | 11 | def __init__(self, color: Color, width: int, height: int): 12 | Sprite.__init__(self) 13 | self.image = Surface((width, height)) 14 | self.image.fill(color) 15 | self.rect = self.image.get_rect() 16 | 17 | 18 | class SurfaceSprite(Sprite): 19 | 20 | def __init__(self, surface: Surface): 21 | Sprite.__init__(self) 22 | self.image = surface 23 | self.rect = self.image.get_rect() 24 | 25 | 26 | def ball_sprite(screen_info: VideoInfo) -> Sprite: 27 | size = int(screen_info.current_h * BALL_SIZE) 28 | return ColoredBlockSprite(Color('maroon'), size, size) 29 | 30 | 31 | def racket_sprite(screen_info: VideoInfo) -> Sprite: 32 | return ColoredBlockSprite( 33 | Color(240, 240, 240), int(screen_info.current_h * RACKET_WIDTH), int(screen_info.current_h * RACKET_HEIGHT)) 34 | 35 | 36 | def table_sprite(screen_info: VideoInfo) -> Sprite: 37 | return ColoredBlockSprite(Color('black'), int(screen_info.current_w), int(screen_info.current_h)) 38 | 39 | 40 | def score_sprite(score: int, font_size: int = 80) -> Sprite: 41 | font = Font(None, font_size) # None - default font 42 | font_surface = font.render(str(score), True, Color('steelblue')) 43 | return SurfaceSprite(font_surface) 44 | 45 | 46 | def spark_sprite(screen_info: VideoInfo) -> Sprite: 47 | size = int(screen_info.current_h * BALL_SIZE) 48 | return ColoredBlockSprite(Color('gold'), size, size) 49 | -------------------------------------------------------------------------------- /examples/pong/systems.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | from random import uniform, choice 3 | 4 | import pygame 5 | from pygame import Surface 6 | from pygame.rect import Rect 7 | from pygame.event import Event 8 | from pygame.display import Info as VideoInfo 9 | from pygame.locals import QUIT, KEYDOWN, KEYUP, K_ESCAPE, K_UP, K_DOWN, K_w, K_s, K_SPACE 10 | from ecs_pattern import System, EntityManager 11 | 12 | from components import ComMotion, ComVisible 13 | from entities import Ball, GameStateInfo, Racket, Score, Table, TeamScoredGoalEvent, WaitForBallMoveEvent, Spark 14 | from sprites import ball_sprite, racket_sprite, table_sprite, score_sprite, spark_sprite 15 | from consts import Team, BALL_SIZE, RACKET_WIDTH, RACKET_HEIGHT, RACKET_SPEED, BALL_SPEED_MIN 16 | 17 | 18 | def set_random_ball_speed(ball: Ball, screen_info: VideoInfo, x_direction: int): 19 | assert x_direction in (1, -1), 'Wrong direction' 20 | min_speed = int(BALL_SPEED_MIN * screen_info.current_h) 21 | ball.speed_x = uniform(min_speed, min_speed * 2) * x_direction 22 | ball.speed_y = uniform(min_speed, min_speed * 2) * choice((1, -1)) 23 | 24 | 25 | class SysInit(System): 26 | def __init__(self, entities: EntityManager): 27 | self.entities = entities 28 | 29 | def start(self): 30 | screen_info = pygame.display.Info() 31 | self.entities.init( 32 | TeamScoredGoalEvent(Team.LEFT), 33 | Spark(spark_sprite(pygame.display.Info()), 0, 0, 0, 0) 34 | ) 35 | self.entities.add( 36 | GameStateInfo( 37 | play=True, 38 | pause=False 39 | ), 40 | WaitForBallMoveEvent(1000), 41 | Score( 42 | sprite=score_sprite(0), 43 | x=int(screen_info.current_w * 0.25), 44 | y=int(screen_info.current_h * 0.2), 45 | team=Team.LEFT, 46 | score=0 47 | ), 48 | Score( 49 | sprite=score_sprite(0), 50 | x=int(screen_info.current_w * 0.75), 51 | y=int(screen_info.current_h * 0.2), 52 | team=Team.RIGHT, 53 | score=0 54 | ), 55 | Ball( 56 | sprite=ball_sprite(screen_info), 57 | x=int(screen_info.current_w * 0.5 - BALL_SIZE * screen_info.current_h / 2), 58 | y=int(screen_info.current_h * 0.5 - BALL_SIZE * screen_info.current_h / 2), 59 | speed_x=0, speed_y=0 60 | ), 61 | Racket( 62 | sprite=racket_sprite(screen_info), 63 | x=0, 64 | y=int(screen_info.current_h / 2 - screen_info.current_h * RACKET_HEIGHT / 2), 65 | team=Team.LEFT, 66 | speed_x=0, speed_y=0 67 | ), 68 | Racket( 69 | sprite=racket_sprite(screen_info), 70 | x=int(screen_info.current_w - screen_info.current_h * RACKET_WIDTH), 71 | y=int(screen_info.current_h / 2 - screen_info.current_h * RACKET_HEIGHT / 2), 72 | team=Team.RIGHT, 73 | speed_x=0, speed_y=0 74 | ), 75 | Table( 76 | sprite=table_sprite(screen_info), 77 | x=0, 78 | y=0 79 | ), 80 | ) 81 | print('Ping') 82 | 83 | def stop(self): 84 | print('Pong') 85 | 86 | 87 | class SysMovement(System): 88 | def __init__(self, entities: EntityManager): 89 | self.entities = entities 90 | self.game_state_info = None 91 | 92 | def start(self): 93 | self.game_state_info = next(self.entities.get_by_class(GameStateInfo)) 94 | 95 | def update(self): 96 | if self.game_state_info.pause: 97 | return 98 | # get entities 99 | ball = next(self.entities.get_by_class(Ball)) 100 | table = next(self.entities.get_by_class(Table)) 101 | # move 102 | for movable_entity in self.entities.get_with_component(ComMotion, ComVisible): 103 | movable_entity.x += movable_entity.speed_x 104 | movable_entity.y += movable_entity.speed_y 105 | # ball reflect 106 | if ball.y < table.y: 107 | ball.speed_y = -ball.speed_y 108 | if ball.y > table.sprite.rect.height - ball.sprite.rect.height: 109 | ball.speed_y = -ball.speed_y 110 | # racket 111 | ball_rect = Rect(ball.x, ball.y, ball.sprite.rect.width, ball.sprite.rect.height) 112 | for racket in self.entities.get_by_class(Racket): 113 | # hit ball 114 | if ball_rect.colliderect(Rect(racket.x, racket.y, racket.sprite.rect.width, racket.sprite.rect.height)): 115 | set_random_ball_speed(ball, pygame.display.Info(), -1 if ball.speed_x > 0 else 1) 116 | if racket.team == Team.LEFT: 117 | ball.x = racket.x + racket.sprite.rect.width + 1 118 | else: 119 | ball.x = racket.x - ball.sprite.rect.width - 1 120 | # wall border 121 | if racket.y < table.y: 122 | racket.y = table.y 123 | if racket.y > table.sprite.rect.height - racket.sprite.rect.height: 124 | racket.y = table.sprite.rect.height - racket.sprite.rect.height 125 | # goal 126 | if ball.x < table.x or ball.x > table.sprite.rect.width - ball.sprite.rect.width: 127 | team_scored_goal = Team.RIGHT if ball.x < table.x else Team.LEFT 128 | self.entities.add( 129 | TeamScoredGoalEvent(team_scored_goal), 130 | WaitForBallMoveEvent(1000) 131 | ) 132 | screen_info = pygame.display.Info() 133 | min_speed = int(BALL_SPEED_MIN * screen_info.current_h) 134 | for i in range(40): 135 | self.entities.add( 136 | Spark( 137 | sprite=spark_sprite(screen_info), 138 | x=ball.x, 139 | y=ball.y, 140 | speed_x=uniform(min_speed / 3, min_speed * 4) * choice((1, -1)), 141 | speed_y=uniform(min_speed / 3, min_speed * 4) * choice((1, -1)), 142 | ) 143 | ) 144 | ball.x = int(screen_info.current_w * 0.5 - BALL_SIZE * screen_info.current_h / 2) 145 | ball.y = int(screen_info.current_h * 0.5 - BALL_SIZE * screen_info.current_h / 2) 146 | ball.speed_x = 0 147 | ball.speed_y = 0 148 | # kill sparks 149 | table_rect = Rect(table.x, table.y, table.sprite.rect.width, table.sprite.rect.height) 150 | for spark in self.entities.get_by_class(Spark): 151 | if not table_rect.colliderect( 152 | Rect(spark.x, spark.y, spark.sprite.rect.width, spark.sprite.rect.height)): 153 | self.entities.delete_buffer_add(spark) 154 | self.entities.delete_buffer_purge() 155 | 156 | 157 | class SysGoal(System): 158 | def __init__(self, entities: EntityManager): 159 | self.entities = entities 160 | 161 | def update(self): 162 | team_scored_goal_event: TeamScoredGoalEvent = next(self.entities.get_by_class(TeamScoredGoalEvent), None) 163 | if team_scored_goal_event: 164 | score_entity: Score 165 | for score_entity in self.entities.get_by_class(Score): 166 | if score_entity.team == team_scored_goal_event.team: 167 | score_entity.score += 1 168 | score_entity.sprite = score_sprite(score_entity.score) 169 | self.entities.delete(team_scored_goal_event) 170 | 171 | 172 | class SysRoundStarter(System): 173 | def __init__(self, entities: EntityManager, clock: pygame.time.Clock): 174 | self.entities = entities 175 | self.clock = clock 176 | 177 | def update(self): 178 | wait_for_ball_move_event: WaitForBallMoveEvent = next(self.entities.get_by_class(WaitForBallMoveEvent), None) 179 | if wait_for_ball_move_event: 180 | wait_for_ball_move_event.wait_ms -= self.clock.get_time() 181 | if wait_for_ball_move_event.wait_ms <= 0: 182 | self.entities.delete(wait_for_ball_move_event) 183 | set_random_ball_speed(next(self.entities.get_by_class(Ball)), pygame.display.Info(), choice((1, -1))) 184 | 185 | 186 | class SysControl(System): 187 | def __init__(self, entities: EntityManager, event_getter: Callable[..., list[Event]]): 188 | self.entities = entities 189 | self.event_getter = event_getter 190 | self.move_keys = (K_w, K_s, K_UP, K_DOWN) 191 | self.event_types = (KEYDOWN, KEYUP, QUIT) # white list 192 | self.game_state_info = None 193 | self.pressed_keys = set() 194 | 195 | def start(self): 196 | self.game_state_info = next(self.entities.get_by_class(GameStateInfo)) 197 | 198 | def update(self): 199 | for event in self.event_getter(self.event_types): 200 | event_type = event.type 201 | event_key = getattr(event, 'key', None) 202 | 203 | # quit game 204 | if (event_type == KEYDOWN and event_key == K_ESCAPE) or event_type == QUIT: 205 | self.game_state_info.play = False 206 | 207 | # pause 208 | if event_type == KEYDOWN and event_key == K_SPACE: 209 | self.game_state_info.pause = not self.game_state_info.pause 210 | 211 | # move rackets 212 | if event_key in self.move_keys: 213 | screen_info = pygame.display.Info() 214 | racket: Racket 215 | 216 | if event_type == KEYDOWN: 217 | self.pressed_keys.add(event_key) 218 | elif event_type == KEYUP: 219 | self.pressed_keys.remove(event_key) 220 | 221 | for racket in self.entities.get_by_class(Racket): 222 | # left up 223 | if event_key == K_w and racket.team == Team.LEFT: 224 | if event_type == KEYDOWN: 225 | racket.speed_y = int(screen_info.current_h * -RACKET_SPEED) 226 | elif event_type == KEYUP and K_s not in self.pressed_keys: 227 | racket.speed_y = 0 228 | # left down 229 | if event_key == K_s and racket.team == Team.LEFT: 230 | if event_type == KEYDOWN: 231 | racket.speed_y = int(screen_info.current_h * RACKET_SPEED) 232 | elif event_type == KEYUP and K_w not in self.pressed_keys: 233 | racket.speed_y = 0 234 | # right up 235 | if event_key == K_UP and racket.team == Team.RIGHT: 236 | if event_type == KEYDOWN: 237 | racket.speed_y = int(screen_info.current_h * -RACKET_SPEED) 238 | elif event_type == KEYUP and K_DOWN not in self.pressed_keys: 239 | racket.speed_y = 0 240 | # right down 241 | if event_key == K_DOWN and racket.team == Team.RIGHT: 242 | if event_type == KEYDOWN: 243 | racket.speed_y = int(screen_info.current_h * RACKET_SPEED) 244 | elif event_type == KEYUP and K_UP not in self.pressed_keys: 245 | racket.speed_y = 0 246 | 247 | 248 | class SysDraw(System): 249 | def __init__(self, entities: EntityManager, screen: Surface): 250 | self.entities = entities 251 | self.screen = screen 252 | 253 | def update(self): 254 | for visible_entity in self.entities.get_by_class(Table, Score, Ball, Racket, Spark): 255 | self.screen.blit(visible_entity.sprite.image, (visible_entity.x, visible_entity.y)) 256 | -------------------------------------------------------------------------------- /examples/snow_day/README.rst: -------------------------------------------------------------------------------- 1 | .. http://docutils.sourceforge.net/docs/user/rst/quickref.html 2 | 3 | Snow day - scene 4 | ======================================================================================================================== 5 | 6 | Scene with snowflakes and warm shine, implemented on pygame + ecs_pattern 7 | 8 | At scene: 10 000 snowflakes with different: transparency, rotation speed, movement speed. 9 | 10 | I will be happy to hear about a simple significant optimization. 11 | 12 | Size of example gif is 10MB, wait for load it :D 13 | 14 | gif: 15 | 16 | .. image:: https://github.com/ikvk/ecs_pattern/blob/master/examples/snow_day/_img/demo.gif 17 | -------------------------------------------------------------------------------- /examples/snow_day/_img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/snow_day/_img/demo.gif -------------------------------------------------------------------------------- /examples/snow_day/_img/fog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/snow_day/_img/fog.png -------------------------------------------------------------------------------- /examples/snow_day/_img/landscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/snow_day/_img/landscape.jpg -------------------------------------------------------------------------------- /examples/snow_day/_img/light_shine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/snow_day/_img/light_shine.png -------------------------------------------------------------------------------- /examples/snow_day/_img/snowflake orig.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/snow_day/_img/snowflake orig.jpg -------------------------------------------------------------------------------- /examples/snow_day/_img/snowflake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/snow_day/_img/snowflake.png -------------------------------------------------------------------------------- /examples/snow_day/_img/snowflake.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/snow_day/_img/snowflake.psd -------------------------------------------------------------------------------- /examples/snow_day/common_tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/snow_day/common_tools/__init__.py -------------------------------------------------------------------------------- /examples/snow_day/common_tools/components.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | from dataclasses import field 3 | 4 | from pygame import Surface 5 | from ecs_pattern import component 6 | 7 | 8 | @component 9 | class ComSurface: 10 | """Поверхность (изображение)""" 11 | surface: Surface 12 | 13 | 14 | @component 15 | class Com2dCoord: 16 | """Двухмерные координаты""" 17 | x: float # X координата на дисплее, 0 слева 18 | y: float # Y координата на дисплее, 0 сверху 19 | 20 | 21 | @component 22 | class ComSpeed: 23 | """Скорость перемещения""" 24 | speed_x: float # пикселей в секунду, *используй поправку на FPS 25 | speed_y: float # пикселей в секунду, *используй поправку на FPS 26 | 27 | 28 | @component 29 | class ComAnimationSet: 30 | """Набор поверхностей для анимации""" 31 | frames: Tuple[Surface] 32 | frame_w: int = field(init=False) 33 | frame_h: int = field(init=False) 34 | 35 | def __post_init__(self): 36 | self.frame_w = self.frames[0].get_width() 37 | self.frame_h = self.frames[0].get_height() 38 | 39 | 40 | @component 41 | class ComAnimated: 42 | """Анимированный объект""" 43 | animation_set: ComAnimationSet # набор кадров анимации, 0-последний кадр, len(animation_set)-первый кадр 44 | animation_looped: bool # анимация зациклена либо удаляется после прохода 45 | animation_frame: int # текущий кадр анимации, значение вычитается 46 | animation_frame_float: float # для расчета переключения animation_frame 47 | animation_speed: float # кадров в секунду, *используй поправку на FPS 48 | -------------------------------------------------------------------------------- /examples/snow_day/common_tools/consts.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame import SRCALPHA 3 | 4 | pygame.init() # init all imported pygame modules 5 | 6 | # Рабочий стол 7 | # Длина списка get_desktop_sizes отличается от количества подключенных мониторов, 8 | # поскольку рабочий стол может быть зеркально отображен на нескольких мониторах. 9 | # Размеры рабочего стола указывают не на максимальное разрешение монитора, 10 | # поддерживаемое оборудованием, а на размер рабочего стола, настроенный в операционной системе. 11 | _desktop_size_set = pygame.display.get_desktop_sizes() # рабочие столы 12 | _desktop_max_h = max(height for width, height in _desktop_size_set) # максимальная высота среди рабочих столов 13 | _desktop_w, _desktop_h = next((w, h) for w, h in _desktop_size_set if h == _desktop_max_h) # выбранный рабочий стол 14 | _is_horizontal_desktop = _desktop_h < _desktop_w 15 | 16 | # качество графики 17 | _graphics_quality_div = 1 # {SETTING_GRAPHIC_LOW: 3, SETTING_GRAPHIC_MIDDLE: 2, SETTING_GRAPHIC_HIGH: 1} 18 | _desktop_w = _desktop_w / _graphics_quality_div 19 | _desktop_h = _desktop_h / _graphics_quality_div 20 | 21 | # зависимость размеров от режима экрана 22 | SETTING_SCREEN_IS_FULLSCREEN = False 23 | if SETTING_SCREEN_IS_FULLSCREEN: 24 | SCREEN_WIDTH = _desktop_w # ширина области для рендера в пикселях 25 | SCREEN_HEIGHT = _desktop_h # высота области для рендера в пикселях 26 | else: # WINDOW 27 | SCREEN_HEIGHT = _desktop_h * 0.8 28 | SCREEN_WIDTH = SCREEN_HEIGHT 29 | 30 | # сцена 1 31 | SHINE_SIZE = 0.38 # от высоты 32 | SHINE_WARM_SPEED_MUL = 10 # от высоты 33 | SNOWFLAKE_SIZE_FROM = 0.002 # от высоты 34 | SNOWFLAKE_SIZE_TO = 0.03 # от высоты 35 | SNOWFLAKE_SIZE_CNT = 64 36 | SNOWFLAKE_SIZE_STEP = (SNOWFLAKE_SIZE_TO - SNOWFLAKE_SIZE_FROM) / SNOWFLAKE_SIZE_CNT 37 | SNOWFLAKE_CNT = 10_000 38 | SNOWFLAKE_ANIMATION_FRAMES = 360 # кадров в полном обороте 39 | SNOWFLAKE_ANIMATION_SPEED_MIN = 2.0 # fps 40 | SNOWFLAKE_ANIMATION_SPEED_MAX = 40.0 # fps 41 | SNOWFLAKE_SPEED_X_RANGE = (-10.0, 10.0) 42 | SNOWFLAKE_SPEED_Y_RANGE = (15.0, 40.0) 43 | 44 | # рендер 45 | FPS_MAX = 60 46 | FPS_SHOW = True 47 | SURFACE_ARGS = dict(flags=SRCALPHA, depth=32) 48 | -------------------------------------------------------------------------------- /examples/snow_day/common_tools/resources.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from pygame import Surface 4 | from pygame.font import Font 5 | from pygame.image import load 6 | 7 | from .consts import SCREEN_HEIGHT 8 | from .surface import colored_block_surface 9 | 10 | 11 | def _load_img(path: str) -> Surface: 12 | """Загрузить изображение из файла""" 13 | try: 14 | return load(path) 15 | except FileNotFoundError: 16 | warnings.warn(f'Image not found: {path}') 17 | return colored_block_surface('#FF00FFff', 100, 100) 18 | 19 | 20 | # объекты изображений 21 | IMG_SNOWFLAKE = _load_img('_img/snowflake.png') 22 | IMG_SHINE = _load_img('_img/light_shine.png') 23 | IMG_BACKGROUND = _load_img('_img/landscape.jpg') 24 | 25 | # объекты шрифтов - Font 26 | FONT_DEFAULT = Font(None, int(SCREEN_HEIGHT / 35)) 27 | -------------------------------------------------------------------------------- /examples/snow_day/common_tools/surface.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Union 2 | 3 | from pygame.transform import rotate 4 | from pygame.draw import rect 5 | from pygame.math import Vector2 6 | from pygame.font import Font 7 | from pygame import Color, Surface 8 | 9 | from .consts import SURFACE_ARGS 10 | 11 | 12 | def blit_rotated(surf: Surface, image: Surface, pos, origin_pos, angle: int, fill_color=(0, 0, 0, 0)): 13 | """ 14 | Вывести на surf изображение image, повернутое вокруг pos на angle градусов 15 | surf: target Surface 16 | image: Surface which has to be rotated and blit 17 | pos: position of the pivot on the target Surface surf (relative to the top left of surf) 18 | origin_pos: position of the pivot on the image Surface (relative to the top left of image) 19 | angle: angle of rotation in degrees 20 | """ 21 | # offset from pivot to center 22 | image_rect = image.get_rect(topleft=(pos[0] - origin_pos[0], pos[1] - origin_pos[1])) 23 | offset_center_to_pivot = Vector2(pos) - image_rect.center 24 | # rotated offset from pivot to center 25 | rotated_offset = offset_center_to_pivot.rotate(-angle) 26 | # rotated image center 27 | rotated_image_center = (pos[0] - rotated_offset.x, pos[1] - rotated_offset.y) 28 | # get a rotated image 29 | rotated_image = rotate(image, angle) 30 | rotated_image_rect = rotated_image.get_rect(center=rotated_image_center) 31 | # rotate and blit the image 32 | surf.blit(rotated_image, rotated_image_rect) 33 | # draw rectangle around the image 34 | rect(surf, fill_color, (*rotated_image_rect.topleft, *rotated_image.get_size()), 2) 35 | 36 | 37 | def text_surface(font_obj: Font, value: Any, color_main: str, color_shadow: Optional[str] = None, 38 | shadow_shift: float = 0.03) -> Surface: 39 | """Создать поверхность с текстом""" 40 | surface_main = font_obj.render(str(value), True, Color(color_main)) 41 | if not color_shadow: 42 | return surface_main 43 | surface_shadow = font_obj.render(str(value), True, Color(color_shadow)) 44 | w, h = surface_main.get_size() 45 | shadow_dx = shadow_dy = int(h * shadow_shift) 46 | surface_res = Surface((w + shadow_dx, h + shadow_dy), **SURFACE_ARGS) 47 | surface_res.blit(surface_shadow, (shadow_dx, shadow_dy)) 48 | surface_res.blit(surface_main, (0, 0)) 49 | return surface_res 50 | 51 | 52 | def colored_block_surface(color: Union[Color, int, str], width: int, height: int): 53 | """Квадратная поверхность с заданным цветом и размером""" 54 | surface = Surface((width, height), **SURFACE_ARGS) 55 | surface.fill(color) 56 | return surface 57 | -------------------------------------------------------------------------------- /examples/snow_day/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pygame 4 | from pygame import FULLSCREEN, DOUBLEBUF, SCALED 5 | from pygame.time import Clock 6 | 7 | from common_tools.consts import SCREEN_WIDTH, SCREEN_HEIGHT, SURFACE_ARGS, SETTING_SCREEN_IS_FULLSCREEN 8 | from scene1.main import scene1_loop 9 | 10 | os.environ['SDL_VIDEO_CENTERED'] = '1' # window at center 11 | 12 | 13 | def main(): 14 | """Точка входа в приложение""" 15 | pygame.display.set_caption('Snow day') 16 | display = pygame.display.set_mode( 17 | size=(SCREEN_WIDTH, SCREEN_HEIGHT), 18 | flags=(FULLSCREEN | SCALED if SETTING_SCREEN_IS_FULLSCREEN else 0) | DOUBLEBUF, 19 | depth=SURFACE_ARGS['depth'] 20 | ) 21 | clock = Clock() 22 | 23 | while True: 24 | scene1_loop(display, clock) 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /examples/snow_day/requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | pygame==2.6.1 # making multimedia apps like games built on top of the SDL library. C, Python, Native, OpenGL 3 | ecs-pattern==1.1.2 # Implementation of the ECS pattern for creating games. 4 | 5 | setuptools==68.0.0 6 | wheel==0.40.0 7 | -------------------------------------------------------------------------------- /examples/snow_day/scene1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/snow_day/scene1/__init__.py -------------------------------------------------------------------------------- /examples/snow_day/scene1/entities.py: -------------------------------------------------------------------------------- 1 | from ecs_pattern import entity 2 | 3 | from common_tools.components import ComSurface, ComSpeed, Com2dCoord, ComAnimationSet, ComAnimated 4 | 5 | 6 | @entity 7 | class Scene1Info: 8 | do_play: bool # Флаг продолжения основного цикла игры 9 | 10 | 11 | @entity 12 | class Background(Com2dCoord, ComSurface): 13 | pass 14 | 15 | 16 | @entity 17 | class Snowflake(Com2dCoord, ComSpeed, ComAnimated): 18 | pass 19 | 20 | 21 | @entity 22 | class Shine(Com2dCoord, ComSurface): 23 | pass 24 | 25 | 26 | @entity 27 | class SnowflakeAnimationSet(ComAnimationSet): 28 | pass 29 | -------------------------------------------------------------------------------- /examples/snow_day/scene1/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame import Surface 3 | from pygame.time import Clock 4 | from ecs_pattern import EntityManager, SystemManager 5 | 6 | from common_tools.consts import FPS_MAX 7 | from .entities import Scene1Info 8 | from .systems import SysControl, SysDraw, SysInit, SysLive 9 | 10 | 11 | def scene1_loop(display: Surface, clock: Clock): 12 | """Основной цикл игры""" 13 | entities = EntityManager() 14 | system_manager = SystemManager([ 15 | SysInit(entities), 16 | SysControl(entities), 17 | SysLive(entities, clock), 18 | SysDraw(entities, display, clock), 19 | ]) 20 | system_manager.start_systems() 21 | 22 | info: Scene1Info = next(entities.get_by_class(Scene1Info)) 23 | 24 | while info.do_play: 25 | clock.tick_busy_loop(FPS_MAX) # tick_busy_loop точный + ест проц, tick грубый + не ест проц 26 | system_manager.update_systems() 27 | pygame.display.flip() # draw changes on screen 28 | 29 | system_manager.stop_systems() 30 | -------------------------------------------------------------------------------- /examples/snow_day/scene1/surfaces.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from pygame import Surface 4 | from pygame.transform import scale 5 | 6 | from common_tools.consts import SCREEN_WIDTH, SCREEN_HEIGHT, SURFACE_ARGS, SHINE_SIZE, SNOWFLAKE_ANIMATION_FRAMES 7 | from common_tools.resources import IMG_SHINE, IMG_SNOWFLAKE, IMG_BACKGROUND 8 | from common_tools.surface import blit_rotated 9 | 10 | 11 | def surface_background() -> Surface: 12 | return scale(IMG_BACKGROUND.convert_alpha(), (SCREEN_WIDTH, SCREEN_HEIGHT)) 13 | 14 | 15 | def surface_shine() -> Surface: 16 | return scale(IMG_SHINE.convert_alpha(), (SCREEN_HEIGHT * SHINE_SIZE, SCREEN_HEIGHT * SHINE_SIZE)) 17 | 18 | 19 | def surface_snowflake_animation_set(snowflake_scale: str, snowflake_alpha: int, reverse: bool) -> Tuple[Surface, ...]: 20 | snowflake_size = SCREEN_HEIGHT * snowflake_scale 21 | snowflake_sf = scale(IMG_SNOWFLAKE.convert_alpha(), (snowflake_size, snowflake_size)) 22 | 23 | snowflake_frames = [] 24 | snowflake_frame_cnt = SNOWFLAKE_ANIMATION_FRAMES # кадров в полном повороте от 0 до 360 градусов 25 | for i in range(snowflake_frame_cnt): 26 | _angle = int(360 / snowflake_frame_cnt) * i 27 | new_sf = Surface((snowflake_size, snowflake_size), **SURFACE_ARGS).convert_alpha() 28 | req_center_point = (snowflake_size / 2, snowflake_size / 2) 29 | blit_rotated(new_sf, snowflake_sf, req_center_point, req_center_point, _angle) 30 | new_sf.set_alpha(snowflake_alpha) # 255 непрозрачный 31 | snowflake_frames.append(new_sf) 32 | return tuple((reversed if reverse else lambda x: x)(snowflake_frames)) 33 | -------------------------------------------------------------------------------- /examples/snow_day/scene1/systems.py: -------------------------------------------------------------------------------- 1 | import math 2 | from sys import exit # *for windows 3 | from typing import Callable 4 | from random import choice, uniform 5 | 6 | import pygame 7 | from pygame import Surface, Color 8 | from pygame.math import Vector2 9 | from pygame.event import Event 10 | from pygame.locals import QUIT, MOUSEBUTTONDOWN, MOUSEMOTION, KEYDOWN, K_ESCAPE 11 | from ecs_pattern import System, EntityManager 12 | 13 | from common_tools.consts import SCREEN_WIDTH, SCREEN_HEIGHT, SHINE_SIZE, SNOWFLAKE_SIZE_FROM, \ 14 | SNOWFLAKE_SIZE_TO, SNOWFLAKE_SIZE_CNT, FPS_MAX, SNOWFLAKE_SIZE_STEP, SNOWFLAKE_CNT, \ 15 | SNOWFLAKE_ANIMATION_SPEED_MAX, SNOWFLAKE_ANIMATION_SPEED_MIN, \ 16 | SNOWFLAKE_SPEED_X_RANGE, SNOWFLAKE_SPEED_Y_RANGE, FPS_SHOW, SHINE_WARM_SPEED_MUL 17 | from common_tools.components import ComAnimated, ComSpeed, Com2dCoord, ComSurface 18 | from common_tools.resources import FONT_DEFAULT 19 | from .entities import Scene1Info, Background, Snowflake, Shine, SnowflakeAnimationSet 20 | from .surfaces import surface_background, surface_snowflake_animation_set, surface_shine 21 | 22 | 23 | def on_click_lmb(entities: EntityManager, pointer_pos: Vector2): # noqa 24 | print('L') 25 | 26 | 27 | def on_click_rmb(entities: EntityManager, pointer_pos: Vector2): # noqa 28 | print('R') 29 | 30 | 31 | class SysInit(System): 32 | 33 | def __init__(self, entities: EntityManager): 34 | self.entities = entities 35 | 36 | def start(self): 37 | snowflake_animation_set_collection = [] 38 | snowflake_alpha_step = 255 / SNOWFLAKE_SIZE_CNT * 0.99 39 | for i, scale_rate in enumerate(range(SNOWFLAKE_SIZE_CNT)): 40 | snowflake_animation_set_collection.append(surface_snowflake_animation_set( 41 | snowflake_scale=SNOWFLAKE_SIZE_FROM + scale_rate * SNOWFLAKE_SIZE_STEP, 42 | snowflake_alpha=255 - int(i * snowflake_alpha_step), 43 | reverse=choice((True, False)) 44 | )) 45 | 46 | for i in range(SNOWFLAKE_CNT): 47 | self.entities.add( 48 | Snowflake( 49 | x=uniform(0, SCREEN_WIDTH), 50 | y=uniform(0, SCREEN_HEIGHT) - SCREEN_HEIGHT * SNOWFLAKE_SIZE_TO, 51 | speed_x=uniform(*SNOWFLAKE_SPEED_X_RANGE), 52 | speed_y=uniform(*SNOWFLAKE_SPEED_Y_RANGE), 53 | animation_set=SnowflakeAnimationSet(choice(snowflake_animation_set_collection)), 54 | animation_looped=True, 55 | animation_frame=0, 56 | animation_frame_float=0., 57 | animation_speed=uniform(SNOWFLAKE_ANIMATION_SPEED_MIN, SNOWFLAKE_ANIMATION_SPEED_MAX), 58 | ), 59 | ) 60 | 61 | self.entities.add( 62 | Scene1Info( 63 | do_play=True, 64 | ), 65 | Background( 66 | surface_background(), x=0.0, y=0.0 67 | ), 68 | Shine( 69 | surface_shine(), x=SCREEN_HEIGHT / 2, y=SCREEN_HEIGHT / 2 70 | ), 71 | ) 72 | 73 | 74 | class SysLive(System): 75 | 76 | def __init__(self, entities: EntityManager, clock: pygame.time.Clock): 77 | self.entities = entities 78 | self.clock = clock 79 | self.half_shine_size = SCREEN_HEIGHT * SHINE_SIZE / 2 80 | self.shine = None 81 | 82 | def start(self): 83 | self.shine = next(self.entities.get_by_class(Shine)) 84 | 85 | def update(self): 86 | # движение 87 | now_fps = self.clock.get_fps() or FPS_MAX 88 | for speed_obj in self.entities.get_with_component(ComSpeed): 89 | speed_obj.x += speed_obj.speed_x / now_fps 90 | speed_obj.y += speed_obj.speed_y / now_fps 91 | if speed_obj.y > SCREEN_HEIGHT: 92 | speed_obj.x = uniform(0, SCREEN_WIDTH) 93 | speed_obj.y = 0 - speed_obj.animation_set.frame_h 94 | dist_to_shine = math.dist( 95 | (speed_obj.x + speed_obj.animation_set.frame_w, speed_obj.y + speed_obj.animation_set.frame_h), 96 | (self.shine.x + self.half_shine_size, self.shine.y + self.half_shine_size) 97 | ) 98 | if dist_to_shine <= self.half_shine_size: 99 | speed_obj.x += speed_obj.speed_x / now_fps * SHINE_WARM_SPEED_MUL * ( 100 | 1 if abs( 101 | speed_obj.x + speed_obj.animation_set.frame_w - self.shine.x + self.half_shine_size 102 | ) < self.half_shine_size else -1 103 | ) / (dist_to_shine * 0.01) 104 | 105 | # анимация 106 | for ani_obj in self.entities.get_with_component(ComAnimated): 107 | ani_obj.animation_frame_float -= ani_obj.animation_speed / now_fps 108 | ani_obj.animation_frame = ani_obj.animation_frame_float.__trunc__() # быстрее int() 109 | if ani_obj.animation_frame_float < 0: 110 | if ani_obj.animation_looped: 111 | ani_obj.animation_frame = len(ani_obj.animation_set.frames) - 1 112 | ani_obj.animation_frame_float = float(ani_obj.animation_frame) 113 | else: 114 | self.entities.delete_buffer_add(ani_obj) 115 | 116 | self.entities.delete_buffer_purge() 117 | 118 | 119 | class SysControl(System): 120 | 121 | def __init__(self, entities: EntityManager): 122 | self.entities = entities 123 | self.event_getter: Callable[..., list[Event]] = pygame.event.get 124 | 125 | def update(self): 126 | for event in self.event_getter(): 127 | event_type = event.type 128 | event_key = getattr(event, 'key', None) 129 | 130 | if event_type == MOUSEMOTION: 131 | shine_obj = next(self.entities.get_by_class(Shine)) 132 | shine_obj.x = event.pos[0] - SCREEN_HEIGHT * SHINE_SIZE / 2 133 | shine_obj.y = event.pos[1] - SCREEN_HEIGHT * SHINE_SIZE / 2 134 | if event_type == MOUSEBUTTONDOWN: 135 | if event.button == 1: 136 | on_click_lmb(self.entities, event.pos) 137 | if event.button == 3: 138 | on_click_rmb(self.entities, event.pos) 139 | 140 | # выйти из игры 141 | if event_type == QUIT or event_type == KEYDOWN and event_key == K_ESCAPE: 142 | exit() 143 | 144 | 145 | class SysDraw(System): 146 | 147 | def __init__(self, entities: EntityManager, display: Surface, clock: pygame.time.Clock): 148 | self.entities = entities 149 | self.display = display 150 | self.clock = clock 151 | self._fps_pos = (0, SCREEN_HEIGHT * 0.98) 152 | self._fps_color = Color('#1339AC') 153 | 154 | def update(self): 155 | # static 156 | for sf_w_pos in self.entities.get_with_component(Com2dCoord, ComSurface): 157 | self.display.blit(sf_w_pos.surface, (sf_w_pos.x, sf_w_pos.y)) 158 | # animated 159 | for ani_w_pos in self.entities.get_with_component(Com2dCoord, ComAnimated): 160 | self.display.blit(ani_w_pos.animation_set.frames[ani_w_pos.animation_frame], (ani_w_pos.x, ani_w_pos.y)) 161 | # fps 162 | if FPS_SHOW: 163 | self.display.blit( 164 | FONT_DEFAULT.render(f'FPS: {int(self.clock.get_fps())}', True, self._fps_color), 165 | self._fps_pos 166 | ) 167 | -------------------------------------------------------------------------------- /examples/trig/README.rst: -------------------------------------------------------------------------------- 1 | .. http://docutils.sourceforge.net/docs/user/rst/quickref.html 2 | 3 | Trig fall - game 4 | ======================================================================================================================== 5 | 6 | This is commercial game, implemented on pygame + ecs_pattern + numpy 7 | 8 | | Key parts of game logic are cutted (*pass # todo CUT*). 9 | | But it code still *useful to learn* how to use ECS with ecs_pattern lib. 10 | | I hope the reasons for deleting resources are obvious. 11 | | 12 | Also there is examples for: 13 | 14 | * Build .apk 15 | * Build .exe 16 | * Make and compile i18n 17 | * And much more ... 18 | 19 | More info about game: https://gitflic.ru/project/ikvk/trig-fall-game 20 | 21 | .. image:: https://github.com/ikvk/ecs_pattern/blob/master/examples/trig/_img/demo.gif 22 | -------------------------------------------------------------------------------- /examples/trig/_img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/trig/_img/demo.gif -------------------------------------------------------------------------------- /examples/trig/blacklist.txt: -------------------------------------------------------------------------------- 1 | # file containing blacklisted patterns that will be excluded from the final APK. 2 | ^*.git/* 3 | ^*.idea/* 4 | ^*_docs/* 5 | ^*_test_app/* 6 | ^*bash/* 7 | ^*old_bricks/* 8 | -------------------------------------------------------------------------------- /examples/trig/commands/build_apk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Bash has a handy SECONDS builtin var that tracks the number of seconds that have passed since the shell was started 4 | SECONDS=0 5 | GAME_VERSION="1.0.3" # дублируй в common_tools ! 6 | 7 | echo 8 | echo "🚀 Build $GAME_NAME started at: $(date +'%Y-%m-%d %H:%M:%S')" 9 | echo 10 | 11 | # Вариант сборки - prod или test 12 | BUILD_EDITION=$1 13 | if ! [[ "$BUILD_EDITION" =~ ^(prod|test)$ ]]; then 14 | echo "⛔ Firts argument must be: prod or test" 15 | exit 1 16 | fi 17 | if [[ ${BUILD_EDITION} == "prod" ]]; then 18 | P4A_EXTRA_ARGS="--release" 19 | echo "🚂 Building with PROD args" 20 | echo 21 | else 22 | P4A_EXTRA_ARGS="--with-debug-symbols" 23 | echo "👷 Building with DEBUG args" 24 | echo 25 | fi 26 | 27 | # Обновление репозитория 28 | GIT_REPO_PATH="$HOME/trig" 29 | cd "$GIT_REPO_PATH" 30 | git reset --hard HEAD 31 | git checkout master 32 | git pull 33 | COMMIT_INFO=$(git show -s --format='%ai %s') 34 | echo 35 | echo "📜 Commit: $COMMIT_INFO" 36 | echo 37 | cd $HOME 38 | 39 | # Вариант пакета 40 | PACKAGE_EDITION=$2 41 | if ! [[ "$PACKAGE_EDITION" =~ ^(pay|pay.hw|free)$ ]]; then 42 | echo "⛔ Second argument must be in: pay, pay.hw, free" 43 | exit 2 44 | fi 45 | if [[ ${PACKAGE_EDITION} == "free" ]]; then 46 | GAME_NAME="Trig fall (free)" # дублируй в common_tools ! 47 | else 48 | GAME_NAME="Trig fall" # дублируй в common_tools ! 49 | fi 50 | printf "PACKAGE_EDITION = '$PACKAGE_EDITION'\n" >> $GIT_REPO_PATH/common_tools/build_flags.py 51 | 52 | # *для сборки это не обязательно 53 | pip install -r "$GIT_REPO_PATH/requirements.txt" 54 | 55 | # Сборка 56 | p4a apk \ 57 | --dist-name=trig_fall \ 58 | --orientation=portrait \ 59 | --private=$GIT_REPO_PATH \ 60 | --package=game.ikvk.trig_fall_$PACKAGE_EDITION \ 61 | --name "$GAME_NAME" \ 62 | --version=$GAME_VERSION \ 63 | --bootstrap=sdl2 \ 64 | --requirements=python3,pysdl2,pygame,numpy,ecs-pattern,pyjnius \ 65 | --arch=arm64-v8a \ 66 | --arch=armeabi-v7a \ 67 | --blacklist-requirements=sqlite3 \ 68 | --blacklist=$GIT_REPO_PATH/blacklist.txt \ 69 | --presplash=$GIT_REPO_PATH/res/img/loading.jpg \ 70 | --presplash-color=wheat \ 71 | --icon=$GIT_REPO_PATH/res/img/game_icon.png \ 72 | --permission=android.permission.WRITE_EXTERNAL_STORAGE \ 73 | $P4A_EXTRA_ARGS 74 | if ! [[ "$?" == 0 ]]; then 75 | echo "⛔ Failed: p4a apk" 76 | exit 3 77 | fi 78 | 79 | # Подпись 80 | if [[ ${BUILD_EDITION} == "prod" ]]; then 81 | # _docs/подпись приложений - keytool и jarsigner.txt 82 | KEYSTORE_PWD=$(<$HOME/trig_fall_$PACKAGE_EDITION.pwd) 83 | APK_NAME_RES="trig_fall-$BUILD_EDITION-$PACKAGE_EDITION-$GAME_VERSION.apk" 84 | echo 85 | echo "Zipalign optimization:" 86 | zipalign -p -f -v 4 trig_fall-release-unsigned-$GAME_VERSION.apk _zipaligned.apk 87 | echo 88 | echo "Signing app:" 89 | apksigner sign \ 90 | --verbose \ 91 | --ks $HOME/trig_fall_$PACKAGE_EDITION.keystore \ 92 | --ks-key-alias trig_fall_$PACKAGE_EDITION \ 93 | --ks-pass pass:$KEYSTORE_PWD \ 94 | --key-pass pass:$KEYSTORE_PWD \ 95 | --v1-signing-enabled true \ 96 | --v2-signing-enabled true \ 97 | --in _zipaligned.apk \ 98 | --out $APK_NAME_RES 99 | if ! [[ "$?" == 0 ]]; then 100 | echo "⛔ Failed: apksigner sign" 101 | exit 4 102 | fi 103 | apksigner verify $APK_NAME_RES 104 | rm _zipaligned.apk 105 | fi 106 | 107 | # Результаты 108 | BUILD_TIME="$(($SECONDS / 60)):$(($SECONDS % 60))" 109 | echo 110 | echo "✅ Build $GAME_NAME is complete, $BUILD_EDITION, $PACKAGE_EDITION, $GAME_VERSION" 111 | echo "📜 Commit: $COMMIT_INFO" 112 | echo "⌚ Finished at: $(date +'%Y-%m-%d %H:%M:%S')" 113 | echo "⌚ Build time: $BUILD_TIME" 114 | echo 115 | -------------------------------------------------------------------------------- /examples/trig/commands/build_exe.cmd: -------------------------------------------------------------------------------- 1 | ECHO off 2 | CLS 3 | ECHO Building Trig fall .exe ... 4 | 5 | REM https://pyinstaller.org/en/stable/usage.html 6 | REM pyinstaller 6.0.0 7 | 8 | cd %~dp0 9 | 10 | set BUILD_EDITION=%1 11 | set GAME_VERSION="1.0.3" 12 | 13 | if "%BUILD_EDITION%" == "pay" ( 14 | echo PACKAGE_EDITION = 'pay'> "../common_tools/build_flags.py" 15 | goto :build 16 | ) else if "%BUILD_EDITION%" == "free" ( 17 | echo PACKAGE_EDITION = 'free'> "../common_tools/build_flags.py" 18 | goto :build 19 | ) else ( 20 | echo ERROR: Firts argument BUILD_EDITION must be: free or pay, specified: "%BUILD_EDITION%" 21 | goto :end 22 | ) 23 | 24 | :build 25 | echo BUILD_EDITION = %BUILD_EDITION% 26 | echo GAME_VERSION = %GAME_VERSION% 27 | ECHO --- 28 | C:\python\venv\trig310\Scripts\pyinstaller ^ 29 | --clean ^ 30 | --onefile ^ 31 | --noconfirm ^ 32 | --noconsole ^ 33 | --name "Trig fall %GAME_VERSION% %BUILD_EDITION%.exe" ^ 34 | --add-data "../res:res" ^ 35 | --splash "../_docs/img win/win_splash.png" ^ 36 | --icon "../_docs/img win/game_icon.ico" ^ 37 | ../main.py 38 | ECHO --- 39 | ECHO Build finished. 40 | 41 | :end 42 | -------------------------------------------------------------------------------- /examples/trig/commands/p4a_clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 4 | echo "🚀 Clean started at: $(date +'%Y-%m-%d %H:%M:%S')" 5 | echo 6 | 7 | p4a clean_dists 8 | p4a clean_builds 9 | p4a delete_dist 10 | 11 | echo 12 | echo "✅ Clean finished at: $(date +'%Y-%m-%d %H:%M:%S')" 13 | echo 14 | 15 | # p4a help: 16 | # clean_all (clean-all) 17 | # Delete all builds, dists and caches 18 | # clean_dists (clean-dists) 19 | # Delete all dists 20 | # clean_bootstrap_builds (clean-bootstrap-builds) 21 | # Delete all bootstrap builds 22 | # clean_builds (clean-builds) 23 | # Delete all builds 24 | # clean Delete build components. 25 | # . 26 | # clean_recipe_build (clean-recipe-build) 27 | # Delete the build components of the given recipe. By default this will also delete built dists 28 | # clean_download_cache (clean-download-cache) 29 | # Delete cached downloads for requirement builds 30 | # delete_dist (delete-dist) 31 | # Delete a compiled dist 32 | 33 | # p4a clean --help 34 | # positional arguments: 35 | # component The build component(s) to delete. You can pass any number of arguments from 36 | # "all", "builds", "dists", "distributions", "bootstrap_builds", "downloads" 37 | -------------------------------------------------------------------------------- /examples/trig/common_tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/trig/common_tools/__init__.py -------------------------------------------------------------------------------- /examples/trig/common_tools/compatibility.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from io import BytesIO 4 | from functools import cache 5 | 6 | 7 | def bytes_buffer_instead_path(path: str) -> BytesIO: 8 | """ 9 | some problems with pygame on kivy, you can't load a file directly in pygame 10 | https://stackoverflow.com/questions/75843421/ 11 | """ 12 | # PyInstaller creates a temp folder and stores path in _MEIPASS 13 | base_path = getattr(sys, '_MEIPASS', os.path.abspath(".")) 14 | path = os.path.join(base_path, path) 15 | 16 | with open(path, 'rb') as f: 17 | return BytesIO(f.read()) 18 | 19 | 20 | def is_android(): 21 | return 'P4A_BOOTSTRAP' in os.environ or 'ANDROID_ARGUMENT' in os.environ 22 | 23 | 24 | @cache 25 | def get_user_data_dir(package_name: str) -> str: 26 | """ 27 | Путь к каталогу в файловой системе пользователей, 28 | который приложение может использовать для хранения дополнительных данных. 29 | """ 30 | if is_android(): 31 | from jnius import autoclass, cast # noqa *renamed 32 | PythonActivity = autoclass('org.kivy.android.PythonActivity') # noqa 33 | context = cast('android.content.Context', PythonActivity.mActivity) 34 | file_p = cast('java.io.File', context.getFilesDir()) 35 | data_dir = file_p.getAbsolutePath() 36 | elif sys.platform == 'win32': 37 | data_dir = os.path.join(os.environ['APPDATA'], package_name) 38 | else: # 'linux' and other 39 | data_dir = os.environ.get('XDG_CONFIG_HOME', '~/.config') 40 | data_dir = os.path.expanduser(os.path.join(data_dir, package_name)) 41 | if not os.path.exists(data_dir): 42 | os.mkdir(data_dir) 43 | return data_dir 44 | -------------------------------------------------------------------------------- /examples/trig/common_tools/components.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Callable, Hashable 2 | 3 | from pygame import Rect, Surface 4 | from pygame.font import Font 5 | from ecs_pattern import component 6 | 7 | from common_tools.consts import BS_STATIC, IS_STATIC 8 | 9 | 10 | @component 11 | class ComSurface: 12 | """Поверхность (изображение)""" 13 | surface: Surface 14 | 15 | 16 | @component 17 | class Com2dCoord: 18 | """Двухмерные координаты""" 19 | x: float # X координата на дисплее, 0 слева 20 | y: float # Y координата на дисплее, 0 сверху 21 | 22 | 23 | @component 24 | class ComSpeed: 25 | """Скорость перемещения""" 26 | speed_x: float # пикселей в секунду, *используй поправку на FPS 27 | speed_y: float # пикселей в секунду, *используй поправку на FPS 28 | 29 | 30 | @component 31 | class ComAnimationSet: 32 | """Набор поверхностей для анимации""" 33 | frames: Tuple[Surface] 34 | 35 | 36 | @component 37 | class ComAnimated: 38 | """Анимированный объект""" 39 | animation_set: ComAnimationSet # набор кадров анимации, 0-последний кадр, len(animation_set)-первый кадр 40 | animation_looped: bool # анимация зациклена либо удаляется после прохода 41 | animation_frame: int # текущий кадр анимации, значение вычитается 42 | animation_frame_float: float # для расчета переключения animation_frame 43 | animation_speed: float # кадров в секунду, *используй поправку на FPS 44 | 45 | 46 | @component 47 | class _ComUiElement: 48 | """Общие свойства элементов графического интерфейса""" 49 | rect: Rect # Rect(left, top, width, height), у контролов разные поверхности - для движения мыши 50 | scenes: [Hashable, ...] # на каких сценах отображать 51 | 52 | 53 | @component 54 | class ComUiButton(_ComUiElement): 55 | """Элемент графического интерфейса - кнопка""" 56 | sf_static: Surface 57 | sf_hover: Surface = None 58 | sf_pressed: Surface = None 59 | mask: Surface = None # активная область 60 | on_click: Callable = lambda: None 61 | state: int = BS_STATIC # см. BS_NAME 62 | 63 | 64 | @component 65 | class ComUiInput(_ComUiElement): 66 | """Элемент графического интерфейса - поле ввода текста""" 67 | font: Font 68 | max_length: int 69 | sf_static: Surface 70 | sf_active: Surface = None 71 | mask: Surface = None # активная область 72 | text: str = '' # текст в поле ввода 73 | cursor: int = 0 74 | on_confirm: Callable = lambda: None # подтверждение ввода - enter, графический ввод android 75 | on_edit: Callable = lambda: None # изменение текста 76 | state: int = IS_STATIC # см. IS_NAMES 77 | 78 | 79 | @component 80 | class ComUiText(_ComUiElement): 81 | """Элемент графического интерфейса - многострочный текст с прокруткой""" 82 | sf_text: Surface 83 | sf_bg: Surface 84 | scroll_pos: float = 0.0 # от 0 до 1 85 | -------------------------------------------------------------------------------- /examples/trig/common_tools/consts.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import pygame 4 | from pygame import SRCALPHA 5 | 6 | try: 7 | from common_tools.build_flags import PACKAGE_EDITION # устанавливается скриптом при сборке 8 | except (ModuleNotFoundError, ImportError): 9 | warnings.warn('Create common_tools/build_flags.py with PACKAGE_EDITION str var') 10 | PACKAGE_EDITION = 'free' 11 | from common_tools.compatibility import is_android, get_user_data_dir 12 | from common_tools.settings import SettingsStorage, SETTING_GRAPHIC_LOW, SETTING_GRAPHIC_MIDDLE, SETTING_GRAPHIC_HIGH, \ 13 | SETTING_SCREEN_MODE_FULL, SETTING_SOUND_NORMAL, SETTING_SOUND_QUIET, SETTING_SOUND_DISABLED 14 | 15 | pygame.mixer.pre_init(44100, -16, 2, 512) # best place - before calling the top level pygame.init() 16 | pygame.init() # init all imported pygame modules 17 | 18 | # общие 19 | PLUS_MINUS_ONE = (-1, 1) 20 | 21 | # ДУБЛИРУЮЩИЕСЯ в build_apk.sh ! 22 | PACKAGE_NAME = f'game.ikvk.trig_fall_{PACKAGE_EDITION}' 23 | GAME_NAME = 'Trig fall' 24 | GAME_VERSION = f'1.0.3 {PACKAGE_EDITION}' 25 | 26 | # варианты игры, лучше проверять на PACKAGE_EDITION_FREE 27 | PACKAGE_EDITION_PAY = 'pay' 28 | PACKAGE_EDITION_PAY_HUAWEI = 'pay.hw' 29 | PACKAGE_EDITION_FREE = 'free' 30 | 31 | # хранилище настроек 32 | SETTINGS_STORAGE = SettingsStorage(get_user_data_dir(PACKAGE_NAME), is_android()) 33 | 34 | # Рабочий стол 35 | # Длина списка get_desktop_sizes отличается от количества подключенных мониторов, 36 | # поскольку рабочий стол может быть зеркально отображен на нескольких мониторах. 37 | # Размеры рабочего стола указывают не на максимальное разрешение монитора, 38 | # поддерживаемое оборудованием, а на размер рабочего стола, настроенный в операционной системе. 39 | _desktop_size_set = pygame.display.get_desktop_sizes() # рабочие столы 40 | _desktop_max_h = max(height for width, height in _desktop_size_set) # максимальная высота среди рабочих столов 41 | _desktop_w, _desktop_h = next((w, h) for w, h in _desktop_size_set if h == _desktop_max_h) # выбранный рабочий стол 42 | _is_horizontal_desktop = _desktop_h < _desktop_w 43 | 44 | # зависимость размеров от ориентации экрана 45 | if _is_horizontal_desktop: 46 | _desktop_w = _desktop_h * 0.62 47 | _desktop_h = _desktop_h 48 | 49 | # качество графики 50 | _quality_div = {SETTING_GRAPHIC_LOW: 3, SETTING_GRAPHIC_MIDDLE: 2, SETTING_GRAPHIC_HIGH: 1}[SETTINGS_STORAGE.graphic] 51 | _desktop_w = _desktop_w / _quality_div 52 | _desktop_h = _desktop_h / _quality_div 53 | 54 | # зависимость размеров от режима экрана 55 | if SETTINGS_STORAGE.screen_mode == SETTING_SCREEN_MODE_FULL: 56 | SCREEN_WIDTH = _desktop_w # ширина области для рендера в пикселях 57 | SCREEN_HEIGHT = _desktop_h # высота области для рендера в пикселях 58 | else: # SETTING_SCREEN_MODE_WINDOW 59 | SCREEN_HEIGHT = int(_desktop_h / 100) * 100 - 100 60 | SCREEN_WIDTH = SCREEN_HEIGHT * 0.62 61 | 62 | # рендер 63 | FPS_MAX = 30 64 | FPS_SHOW = True if SETTINGS_STORAGE.player_name.endswith('@') else False # отображать FPS 65 | SPARK_SIZE_PX = SCREEN_HEIGHT // 132 66 | SURFACE_ARGS = dict(flags=SRCALPHA, depth=32) 67 | 68 | # шрифт 69 | FONT_COLOR_SPEED1 = '#4682B4' 70 | FONT_COLOR_SPEED2 = '#FFE4B5' 71 | FONT_COLOR_SCORE1 = '#FFD700' 72 | FONT_COLOR_SCORE2 = '#8B4513' 73 | FONT_COLOR_PAUSE1 = '#4682B4' 74 | FONT_COLOR_PAUSE2 = '#8B4513' 75 | FONT_COLOR_GAME_OVER1 = '#B22222' 76 | FONT_COLOR_GAME_OVER2 = '#ffcc7a' 77 | 78 | # Размер видимого игрового поля 79 | GRID_ROWS = 18 80 | GRID_COLS = 17 81 | GRID_HIDDEN_TOP_ROWS = 2 82 | assert GRID_ROWS % 2 == 0 # *очистка заполненных строк делается на пару строк 83 | 84 | # размеры видимых игровых сущностей (коэффициенты от ширины и высоты экрана) 85 | INFO_AREA_WIDTH = 1.0 86 | INFO_AREA_HEIGHT = 0.07 87 | PLAY_AREA_WIDTH = 1.0 88 | PLAY_AREA_HEIGHT = 1.0 - INFO_AREA_HEIGHT 89 | _pah = SCREEN_HEIGHT * PLAY_AREA_HEIGHT 90 | _paw = SCREEN_WIDTH * PLAY_AREA_WIDTH 91 | PLAY_AREA_PADDING = 0.033 # поля игрового поля 92 | PLAY_AREA_H_W_RATIO = _pah / _paw # соотношение игровой области: высота / ширина 93 | GRID_H_W_RATIO = 1.55 # требуемое соотношение: высота / ширина 94 | GRID_MARGIN_VER = 0.0 # отступ сверху и снизу 95 | GRID_MARGIN_HOR = 0.0 # отступ справа и слева 96 | _grid_margin = (_pah - _paw * GRID_H_W_RATIO) / 2 97 | if PLAY_AREA_H_W_RATIO > GRID_H_W_RATIO: 98 | GRID_MARGIN_VER = abs(_grid_margin) / SCREEN_HEIGHT 99 | else: 100 | GRID_MARGIN_HOR = abs(_grid_margin) / SCREEN_WIDTH 101 | GRID_WIDTH = PLAY_AREA_WIDTH - PLAY_AREA_PADDING * 2 - GRID_MARGIN_HOR * 2 102 | GRID_HEIGHT = PLAY_AREA_HEIGHT - PLAY_AREA_PADDING * 2 - GRID_MARGIN_VER * 2 103 | GRID_TRI_WIDTH = GRID_WIDTH / GRID_COLS * 0.81 * 2 104 | GRID_TRI_HEIGHT = GRID_HEIGHT / GRID_ROWS * 0.855 105 | GRID_TRI_GAP_COL = GRID_WIDTH / GRID_COLS * 0.18 106 | GRID_TRI_GAP_ROW = GRID_HEIGHT / GRID_ROWS * 0.17 107 | GRID_TRI_Y_CORR = 0.0015 # коррекция в зависимости от направления вершины вверх или вниз 108 | 109 | # игра 110 | SPEED_LEVEL_COUNT = 31 # количество уровней скорости 111 | SPEED_MAP = tuple(0.95 - 0.61 / SPEED_LEVEL_COUNT * i for i in range(SPEED_LEVEL_COUNT)) # скорость уровней игры, сек 112 | SCORE_MAP = tuple(int(100 * i) for i in range(SPEED_LEVEL_COUNT)) # уровни очков для переключения скорости 113 | SPEED_FAST_FALL = 0.1 # скорость падения фигуры с включенным ускорением, сек 114 | SPEED_FAST_FALL_CNT = 3 if PACKAGE_EDITION == PACKAGE_EDITION_FREE else 4 # N строк - для тачей 115 | EVENT_SCORE_CHANCE = 15 if PACKAGE_EDITION == PACKAGE_EDITION_FREE else 10 # выпадать 1 раз за N фигур 116 | EVENT_NO_INTERSECT_CHANCE = 18 # выпадать 1 раз за N фигур 117 | 118 | # размеры сущностей меню (коэффициенты от ширины и высоты экрана) 119 | MENU_ROOT_AREA_GAME_NAME_HEIGHT = 0.38 # часть главного экрана для имени игры 120 | MENU_ROOT_AREA_BUTTONS_HEIGHT = 1 - MENU_ROOT_AREA_GAME_NAME_HEIGHT # часть главного экрана для кнопок 121 | MENU_ROOT_BUTTON_GROUP_WIDTH = 0.95 # ширина группы кнопок главного меню 122 | MENU_ROOT_BUTTON_GROUP_GAP_WIDTH = 0.03 # отступ между кнопками группы кнопок главного меню 123 | MENU_SHINE_WIDTH = 0.38 # *сияние квадратное 124 | 125 | # базовые уровни громкости 126 | SOUND_LEVEL = { 127 | SETTING_SOUND_NORMAL: 0.61, 128 | SETTING_SOUND_QUIET: 0.23, 129 | SETTING_SOUND_DISABLED: 0, 130 | } 131 | 132 | # gui 133 | TEXT_ML_WIDTH = 0.95 # размер блока для многострочных текстов 134 | TEXT_ML_HEIGHT = 0.95 # размер блока для многострочных текстов 135 | BUTTON_WIDTH = 0.28 # базовая ширина прямоугольных кнопок 136 | 137 | # сцены меню 138 | MENU_SCENE_ROOT = 1 139 | MENU_SCENE_ABOUT = 2 140 | MENU_SCENE_GUIDE = 3 141 | MENU_SCENE_RECORDS = 4 142 | MENU_SCENE_SETTINGS = 5 143 | 144 | # сцены меню 145 | FALL_SCENE_PLAY = 1 146 | FALL_SCENE_PAUSE = 2 147 | FALL_SCENE_GAME_OVER = 3 148 | 149 | # размеры матриц с данными 150 | MATRIX_ROWS = GRID_ROWS + GRID_HIDDEN_TOP_ROWS 151 | MATRIX_COLS = GRID_COLS 152 | MATRIX_ROW_COL = tuple((row, col) for row in range(MATRIX_ROWS) for col in range(MATRIX_COLS)) 153 | 154 | # События указателя мыши или пальца, Pointer Action 155 | PA_MOVE_LEFT = 1 156 | PA_MOVE_RIGHT = 2 157 | PA_ROTATE = 3 158 | PA_SWITCH_DIR = 4 159 | PA_MOVE_FAST = 5 160 | PA_NAMES = { 161 | PA_MOVE_LEFT: 'MOVE_LEFT', 162 | PA_MOVE_RIGHT: 'MOVE_RIGHT', 163 | PA_ROTATE: 'ROTATE', 164 | PA_SWITCH_DIR: 'SWITCH_DIR', 165 | PA_MOVE_FAST: 'MOVE_FAST', 166 | } 167 | 168 | # Cостояния кнопок, Button States 169 | BS_STATIC = 1 170 | BS_HOVER = 2 171 | BS_PRESSED = 3 172 | BS_NAMES = { 173 | BS_STATIC: 'STATIC', 174 | BS_HOVER: 'HOVER', 175 | BS_PRESSED: 'PRESSED', 176 | } 177 | 178 | # Cостояния полей ввода, Input States 179 | IS_STATIC = 1 180 | IS_ACTIVE = 2 181 | IS_NAMES = { 182 | IS_STATIC: 'STATIC', 183 | IS_ACTIVE: 'ACTIVE', 184 | } 185 | 186 | # Фигуры 187 | FIGURE_COLS = 5 188 | FIGURE_ROWS = 3 189 | FIGURE_CENTER_PAD = GRID_COLS // 2 - FIGURE_COLS // 2 + 2 # постоянный отступ колонок при создании 190 | assert FIGURE_CENTER_PAD % 2 == 0 191 | FIGURE_ROW_COL = tuple((row, col) for row in range(FIGURE_ROWS) for col in range(FIGURE_COLS)) 192 | FIGURES = ( # Все варианты фигур, верхний левый угол фигуры - треугольник с вершиной вверху 193 | # single 1 194 | ( 195 | ( 196 | (1, 0, 0, 0, 0), 197 | (0, 0, 0, 0, 0), 198 | (0, 0, 0, 0, 0), 199 | ), 200 | ( 201 | (0, 1, 0, 0, 0), 202 | (0, 0, 0, 0, 0), 203 | (0, 0, 0, 0, 0), 204 | ), 205 | ), 206 | # double 1 207 | ( 208 | ( 209 | (1, 0, 0, 0, 0), 210 | (1, 0, 0, 0, 0), 211 | (0, 0, 0, 0, 0), 212 | ), 213 | ( 214 | (0, 1, 1, 0, 0), 215 | (0, 0, 0, 0, 0), 216 | (0, 0, 0, 0, 0), 217 | ), 218 | ( 219 | (0, 0, 0, 0, 0), 220 | (0, 1, 1, 0, 0), 221 | (0, 0, 0, 0, 0), 222 | ), 223 | ), 224 | # triple 1 225 | ( 226 | ( 227 | (1, 1, 1, 0, 0), 228 | (0, 0, 0, 0, 0), 229 | (0, 0, 0, 0, 0), 230 | ), 231 | ( 232 | (0, 0, 1, 0, 0), 233 | (0, 1, 1, 0, 0), 234 | (0, 0, 0, 0, 0), 235 | ), 236 | ( 237 | (1, 0, 0, 0, 0), 238 | (1, 1, 0, 0, 0), 239 | (0, 0, 0, 0, 0), 240 | ), 241 | ), 242 | # triple 2 243 | ( 244 | ( 245 | (1, 1, 0, 0, 0), 246 | (1, 0, 0, 0, 0), 247 | (0, 0, 0, 0, 0), 248 | ), 249 | ( 250 | (0, 1, 1, 0, 0), 251 | (0, 0, 1, 0, 0), 252 | (0, 0, 0, 0, 0), 253 | ), 254 | ( 255 | (0, 0, 0, 0, 0), 256 | (1, 1, 1, 0, 0), 257 | (0, 0, 0, 0, 0), 258 | ), 259 | ), 260 | # triple 3 261 | ( 262 | ( 263 | (0, 1, 1, 1, 0), 264 | (0, 0, 1, 0, 0), 265 | (0, 0, 0, 0, 0), 266 | ), 267 | ( 268 | (0, 1, 1, 0, 0), 269 | (0, 1, 1, 1, 0), 270 | (0, 0, 0, 0, 0), 271 | ), 272 | ), 273 | # quadruple 274 | () # todo CUT 275 | ) 276 | 277 | for figure in FIGURES: 278 | for variant in figure: 279 | if len(variant) != FIGURE_ROWS: 280 | raise ValueError(f'Wrong figure ROWS config: {variant}') 281 | for row in variant: 282 | if len(row) != FIGURE_COLS: 283 | raise ValueError(f'Wrong figure COLS config: {row}') 284 | -------------------------------------------------------------------------------- /examples/trig/common_tools/debug.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def timer(func): 5 | def wrapper(*arg, **kw): 6 | t1 = time.time() 7 | res = func(*arg, **kw) 8 | print(str(func).split(' ')[1], time.time() - t1) 9 | return res 10 | 11 | return wrapper 12 | -------------------------------------------------------------------------------- /examples/trig/common_tools/gui.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pygame 4 | from ecs_pattern import EntityManager 5 | from pygame import Rect, Surface, Mask, Vector2 6 | from pygame.event import Event 7 | from pygame.key import start_text_input, stop_text_input, set_text_input_rect 8 | from pygame.locals import KEYDOWN, MOUSEBUTTONUP, MOUSEBUTTONDOWN, MOUSEMOTION, TEXTINPUT, K_BACKSPACE, K_KP_ENTER, \ 9 | K_RETURN 10 | from pygame.transform import smoothscale 11 | 12 | from common_tools.components import ComUiButton, ComUiInput, ComUiText 13 | from common_tools.consts import SCREEN_WIDTH, BS_STATIC, BUTTON_WIDTH, BS_HOVER, BS_PRESSED, IS_STATIC, IS_ACTIVE 14 | from common_tools.resources import IMG_BUTTON_RECT, FONT_BUTTON, FONT_TEXT_ML 15 | from common_tools.surface import colorize_surface, text_surface 16 | 17 | _MOUSE_EVENT_SET = (MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION) 18 | 19 | 20 | def is_point_in_mask(point: Vector2, mask: Mask, obj_rect: Rect) -> bool: 21 | try: 22 | return bool(mask.get_at((point[0] - obj_rect.x, point[1] - obj_rect.y))) 23 | except IndexError: 24 | return False 25 | 26 | 27 | def gui_button_attrs(x: int, y: int, text: str, w_scale: float = 1) -> dict: 28 | """Графическая часть атрибутов для кнопки""" 29 | bg = IMG_BUTTON_RECT 30 | button_width = SCREEN_WIDTH * BUTTON_WIDTH * w_scale 31 | scale_rate = button_width / bg.get_width() / w_scale * 0.75 32 | button_sf = smoothscale(bg.convert_alpha(), (button_width, bg.get_height() * scale_rate)) 33 | 34 | font_sf = text_surface(FONT_BUTTON, text, '#F0F8FF', '#696969', 0.05) 35 | fw, fh = font_sf.get_size() 36 | w, h = button_sf.get_size() 37 | 38 | button_sf.blit(font_sf, (w / 2 - fw / 2, h / 2 - fh / 2)) 39 | 40 | sf_static = button_sf 41 | sf_hover = colorize_surface(button_sf, '#FFD700') 42 | sf_pressed = colorize_surface(button_sf, '#7FFFD4') 43 | 44 | return dict( 45 | rect=Rect(x, y, w, h), 46 | sf_static=sf_static, 47 | sf_hover=sf_hover, 48 | sf_pressed=sf_pressed, 49 | mask=pygame.mask.from_surface(sf_static), 50 | state=BS_STATIC, 51 | ) 52 | 53 | 54 | def draw_button(surface: Surface, scene_active: int, entities: EntityManager): 55 | """Вывод кнопки на поверхность""" 56 | for button in entities.get_with_component(ComUiButton): 57 | if scene_active not in button.scenes: 58 | continue 59 | if button.state == BS_STATIC: 60 | surface.blit(button.sf_static, (button.rect[0], button.rect[1])) 61 | elif button.state == BS_HOVER: 62 | surface.blit(button.sf_hover, (button.rect[0], button.rect[1])) 63 | elif button.state == BS_PRESSED: 64 | surface.blit(button.sf_pressed, (button.rect[0], button.rect[1])) 65 | 66 | 67 | def draw_input(surface: Surface, scene_active: int, entities: EntityManager): 68 | """Вывод поля ввода на поверхность""" 69 | for input_ in entities.get_with_component(ComUiInput): 70 | if scene_active not in input_.scenes: 71 | continue 72 | left, top, width, height = input_.rect 73 | if input_.state == IS_STATIC: 74 | surface.blit(input_.sf_static, (left, top)) 75 | elif input_.state == IS_ACTIVE: 76 | surface.blit(input_.sf_active, (left, top)) 77 | if input_.state == IS_ACTIVE: 78 | cursor = '' if divmod(time.time(), 1)[1] > 0.5 else '_' 79 | else: 80 | cursor = '' 81 | text_sf = text_surface(FONT_TEXT_ML, input_.text + cursor, '#2F4F4F', '#FFFF00') 82 | surface.blit(text_sf, (left + height * 0.38, top + height / 2 - text_sf.get_height() / 2)) 83 | 84 | 85 | def draw_text_ml(surface: Surface, scene_active: int, entities: EntityManager): 86 | """Вывод текстовой панели на поверхность""" 87 | for text_ml in entities.get_with_component(ComUiText): 88 | if scene_active not in text_ml.scenes: 89 | continue 90 | surface.blit(text_ml.sf_bg, (text_ml.rect[0], text_ml.rect[1])) 91 | surface.blit(text_ml.sf_text, (text_ml.rect[0], text_ml.rect[1])) 92 | 93 | 94 | def control_button(event: Event, event_type: int, scene_active: int, entities: EntityManager) -> bool: 95 | """ 96 | Управление кнопками 97 | Возвращает - было ли нажатие (any_button_clicked) 98 | """ 99 | # мышь 100 | if event_type in _MOUSE_EVENT_SET: 101 | for button in entities.get_with_component(ComUiButton): 102 | if button.state != BS_PRESSED or event_type == MOUSEBUTTONUP: 103 | button.state = BS_STATIC 104 | for button in entities.get_with_component(ComUiButton): 105 | if scene_active in button.scenes and is_point_in_mask(event.pos, button.mask, button.rect): 106 | # кнопка мыши нажата 107 | if event_type == MOUSEBUTTONDOWN: 108 | button.state = BS_PRESSED 109 | # кнопка мыши отпущена 110 | elif event_type == MOUSEBUTTONUP: 111 | button.state = BS_HOVER 112 | button.on_click(entities, event.pos) 113 | stop_text_input() 114 | return True 115 | # движение мыши 116 | elif event_type == MOUSEMOTION: 117 | if button.state != BS_PRESSED: 118 | button.state = BS_HOVER 119 | return False 120 | 121 | 122 | def control_input_activate(event: Event, event_type: int, scene_active: int, entities: EntityManager) -> bool: 123 | """ 124 | Управление полями ввода 125 | Возвращает - была ли активация (any_input_clicked) 126 | """ 127 | if event_type in _MOUSE_EVENT_SET and event_type == MOUSEBUTTONUP: 128 | # активация поля 129 | for input_ in entities.get_with_component(ComUiInput): 130 | if scene_active in input_.scenes and is_point_in_mask(event.pos, input_.mask, input_.rect): 131 | set_text_input_rect(input_.rect) 132 | start_text_input() 133 | input_.state = IS_ACTIVE 134 | return True 135 | # никакое поле на сцене не кликнули - потеря фокуса у всех 136 | stop_text_input() 137 | for input_ in entities.get_with_component(ComUiInput): 138 | input_.state = IS_STATIC 139 | return False 140 | 141 | 142 | def control_input_edit(event: Event, event_type: int, event_key: int, scene_active: int, entities: EntityManager): 143 | """ 144 | Ввод текста в поля ввода 145 | """ 146 | for input_ in entities.get_with_component(ComUiInput): 147 | if scene_active in input_.scenes and input_.state == IS_ACTIVE: 148 | if event_type == TEXTINPUT: 149 | input_.text = input_.text + event.text 150 | input_.on_edit(entities, Vector2(0, 0)) 151 | elif event_type == KEYDOWN and event_key == K_BACKSPACE: 152 | input_.text = input_.text[:-1] 153 | input_.on_edit(entities, Vector2(0, 0)) 154 | elif event_type == KEYDOWN and event_key in [K_RETURN, K_KP_ENTER]: 155 | input_.on_confirm(entities, Vector2(0, 0)) 156 | -------------------------------------------------------------------------------- /examples/trig/common_tools/i18n.py: -------------------------------------------------------------------------------- 1 | """ 2 | Строки с переводом 3 | """ 4 | 5 | import gettext 6 | 7 | from common_tools.consts import SETTINGS_STORAGE 8 | from common_tools.settings import SETTING_GRAPHIC_HIGH, SETTING_SCREEN_MODE_FULL, SETTING_SCREEN_MODE_WINDOW, \ 9 | SETTING_SOUND_DISABLED, SETTING_SOUND_NORMAL, SETTING_SOUND_QUIET, SETTING_GRAPHIC_LOW, SETTING_GRAPHIC_MIDDLE, \ 10 | SETTING_LANGUAGE_RU, SETTING_LANGUAGE_EN 11 | 12 | i18n_domain_name = 'trig_fall' # для локализации 13 | localedir = 'common_tools/locale' 14 | gettext.install(i18n_domain_name, localedir=localedir) # install _() function globally into the built-in namespace 15 | lang = gettext.translation(i18n_domain_name, localedir=localedir, languages=[SETTINGS_STORAGE.language], fallback=True) 16 | lang.install() 17 | _ = lang.gettext 18 | 19 | I18N_SF_TEXT_RECORDS = _(': Records\n\n') 20 | 21 | I18N_SF_TEXT_FREE_VERSION = _( 22 | 'Limitations of the free version of the game:\n' 23 | '1. Records are not displayed at this page, example entry:\n' 24 | '§ 1208 • Name • 2023-JAN-28 16:45\n' 25 | '2. Yellow triangles appear less frequently.\n' 26 | '3. The figure accelerates for less time.\n' 27 | '\n' 28 | 'The paid version is available in the RuStore app market.\n' 29 | '\n' 30 | 'If the game is successfully sold, I will add an online rating.\n' 31 | ) 32 | 33 | I18N_SF_TEXT_ABOUT = _( 34 | ': About\n' 35 | '\n' 36 | 'Version:\n' 37 | '• GAME_VERSION\n' 38 | '\n' 39 | 'Author and developer:\n' 40 | '• Vladimir Kaukin, KaukinVK@ya.ru\n' 41 | '\n' 42 | 'Special thanks:\n' 43 | '• Python language devs\n' 44 | '• Site: freesound.org\n' 45 | '• Lib devs: pygame, numpy, python-for-android\n' 46 | '\n' 47 | 'Fonts:\n' 48 | '• Devinne Swash: Dieter Steffmann\n' 49 | '• Faster One: Eduardo Tunni\n' 50 | '• Alice: Cyreal, Alexei Vanyashin, Gayaneh Bagdasaryan, Ksenia Erulevich\n' 51 | ) 52 | 53 | I18N_SF_TEXT_GUIDE = _( 54 | ': Guide\n\n' 55 | 'Figures from the triangles alternately fall down, reflecting off the side walls.\n' 56 | 'A filled line of triangles clears a pair of lines - even and odd, points are awarded for each triangle.\n' 57 | 'Game speed gradually increases when you score points.\n' 58 | 'The goal of the game is to score as many points as possible.\n' 59 | 'Yellow triangle - add random scores.\n' 60 | 'Violet triangle - current figure passes through walls and stops on rotate.\n' 61 | '\n' 62 | 'MOVE LEFT - Left key, swipe left\n' 63 | 'MOVE RIGHT - Right key, swipe right\n' 64 | 'ROTATE - Up key, swipe up\n' 65 | 'SPEED UP - Down key, swipe down\n' 66 | 'CHANGE DIR - Shift, tap\n' 67 | 'PAUSE - Spacebar, android back\n' 68 | ) 69 | 70 | I18N_SF_TEXT_SETTINGS = _( 71 | ': Settings\n\n' 72 | 'Graphic quality: GRAPHIC_CAPTION\n\n\n\n\n' 73 | 'Screen mode: SCREEN_MODE_CAPTION\n\n\n\n\n' 74 | 'Sound: SOUND_CAPTION\n\n\n\n\n' 75 | 'Language: LANGUAGE_CAPTION\n\n\n\n\n' 76 | '*Restart the game to apply graphic settings\n' 77 | ) 78 | 79 | I18N_SETTING_GRAPHIC_LOW = _('Low') 80 | I18N_SETTING_GRAPHIC_MIDDLE = _('Middle') 81 | I18N_SETTING_GRAPHIC_HIGH = _('High') 82 | I18N_SETTING_SOUND_DISABLED = _('Disabled') 83 | I18N_SETTING_SOUND_QUIET = _('Quiet') 84 | I18N_SETTING_SOUND_NORMAL = _('Normal') 85 | I18N_SETTING_SCREEN_MODE_FULL = _('Full screen') 86 | I18N_SETTING_SCREEN_MODE_WINDOW = _('Window') 87 | I18N_SETTING_LANGUAGE_RU = _('Русский') 88 | I18N_SETTING_LANGUAGE_EN = _('English') 89 | 90 | I18N_BUTTON_TO_MENU_ROOT = _('Main menu') 91 | 92 | # словари _CAPTION чтобы избежать кросс референс settings-consts 93 | SETTING_GRAPHIC_CAPTION = { 94 | SETTING_GRAPHIC_LOW: I18N_SETTING_GRAPHIC_LOW, 95 | SETTING_GRAPHIC_MIDDLE: I18N_SETTING_GRAPHIC_MIDDLE, 96 | SETTING_GRAPHIC_HIGH: I18N_SETTING_GRAPHIC_HIGH, 97 | } 98 | SETTING_SOUND_CAPTION = { 99 | SETTING_SOUND_DISABLED: I18N_SETTING_SOUND_DISABLED, 100 | SETTING_SOUND_QUIET: I18N_SETTING_SOUND_QUIET, 101 | SETTING_SOUND_NORMAL: I18N_SETTING_SOUND_NORMAL, 102 | } 103 | SETTING_SCREEN_MODE_CAPTION = { 104 | SETTING_SCREEN_MODE_FULL: I18N_SETTING_SCREEN_MODE_FULL, 105 | SETTING_SCREEN_MODE_WINDOW: I18N_SETTING_SCREEN_MODE_WINDOW, 106 | } 107 | SETTING_LANGUAGE_CAPTION = { 108 | SETTING_LANGUAGE_RU: I18N_SETTING_LANGUAGE_RU, 109 | SETTING_LANGUAGE_EN: I18N_SETTING_LANGUAGE_EN, 110 | } 111 | 112 | I18N_FALL_SAVE_RESULT = _('Save and exit') 113 | I18N_FALL_RESUME_GAME = _('Resume game') 114 | I18N_FALL_TO_MAIN_MENU = _('To main menu') 115 | I18N_FALL_EXIT_GAME = _('Exit game') 116 | 117 | I18N_SF_TEXT_GAME_RESULTS = _( 118 | ' Game over, your result:\n' 119 | ' SCORE points\n\n' 120 | ' Player name:\n\n\n\n\n\n\n\n' 121 | ) 122 | -------------------------------------------------------------------------------- /examples/trig/common_tools/locale/ru/LC_MESSAGES/trig_fall.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/trig/common_tools/locale/ru/LC_MESSAGES/trig_fall.mo -------------------------------------------------------------------------------- /examples/trig/common_tools/locale/ru/LC_MESSAGES/trig_fall.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Report-Msgid-Bugs-To: \n" 4 | "POT-Creation-Date: 2023-08-30 16:02+0500\n" 5 | "Language: ru\n" 6 | "MIME-Version: 1.0\n" 7 | "Content-Type: text/plain; charset=UTF-8\n" 8 | "Content-Transfer-Encoding: 8bit\n" 9 | 10 | #: common_tools/i18n.py:118 11 | msgid "" 12 | " Game over, your result:\n" 13 | " SCORE points\n" 14 | "\n" 15 | " Player name:\n" 16 | "\n" 17 | "\n" 18 | "\n" 19 | "\n" 20 | "\n" 21 | "\n" 22 | "\n" 23 | msgstr "" 24 | " Игра окончена, ваш результат:\n" 25 | " SCORE очков\n" 26 | "\n" 27 | " Имя игрока:\n" 28 | "\n" 29 | "\n" 30 | "\n" 31 | "\n" 32 | "\n" 33 | "\n" 34 | "\n" 35 | 36 | #: common_tools/i18n.py:34 37 | msgid "" 38 | ": About\n" 39 | "\n" 40 | "Version:\n" 41 | "• GAME_VERSION\n" 42 | "\n" 43 | "Author and developer:\n" 44 | "• Vladimir Kaukin, KaukinVK@ya.ru\n" 45 | "\n" 46 | "Special thanks:\n" 47 | "• Python language devs\n" 48 | "• Site: freesound.org\n" 49 | "• Lib devs: pygame, numpy, python-for-android\n" 50 | "\n" 51 | "Fonts:\n" 52 | "• Devinne Swash: Dieter Steffmann\n" 53 | "• Faster One: Eduardo Tunni\n" 54 | "• Alice: Cyreal, Alexei Vanyashin, Gayaneh Bagdasaryan, Ksenia Erulevich\n" 55 | msgstr "" 56 | ": Об игре\n" 57 | "\n" 58 | "Версия:\n" 59 | "• GAME_VERSION\n" 60 | "\n" 61 | "Автор и разработчик:\n" 62 | "• Владимир Каукин, KaukinVK@ya.ru\n" 63 | "\n" 64 | "Отдельное спасибо:\n" 65 | "• Разработчикам языка Python\n" 66 | "• Сайту: freesound.org\n" 67 | "• Разработчикам библиотек: pygame, numpy, python-for-android\n" 68 | "\n" 69 | "Шрифты:\n" 70 | "• Devinne Swash: Dieter Steffmann\n" 71 | "• Faster One: Eduardo Tunni\n" 72 | "• Alice: Cyreal, Alexei Vanyashin, Gayaneh Bagdasaryan, Ksenia Erulevich\n" 73 | 74 | #: common_tools/i18n.py:54 75 | msgid "" 76 | ": Guide\n" 77 | "\n" 78 | "Figures from the triangles alternately fall down, reflecting off the side " 79 | "walls.\n" 80 | "A filled line of triangles clears a pair of lines - even and odd, points are " 81 | "awarded for each triangle.\n" 82 | "Game speed gradually increases when you score points.\n" 83 | "The goal of the game is to score as many points as possible.\n" 84 | "Yellow triangle - add random scores.\n" 85 | "Violet triangle - current figure passes through walls and stops on rotate.\n" 86 | "\n" 87 | "MOVE LEFT - Left key, swipe left\n" 88 | "MOVE RIGHT - Right key, swipe right\n" 89 | "ROTATE - Up key, swipe up\n" 90 | "SPEED UP - Down key, swipe down\n" 91 | "CHANGE DIR - Shift, tap\n" 92 | "PAUSE - Spacebar, android back\n" 93 | msgstr "" 94 | ": Руководство\n" 95 | "\n" 96 | "Фигуры из треугольников поочередно падают вниз, отражаясь от боковых " 97 | "стенок.\n" 98 | "Заполненная линия треугольников очищает пару линий - четную и нечетную, за " 99 | "каждый треугольник начисляются очки.\n" 100 | "Скорость игры постепенно увеличивается по мере того, как вы набираете очки.\n" 101 | "Цель игры - набрать как можно больше очков.\n" 102 | "Желтый треугольник - случайное количество очков.\n" 103 | "Фиолетовый треугольник - текущая фигура проходит сквозь стены и " 104 | "останавливается при повороте.\n" 105 | "\n" 106 | "СДВИГ ВЛЕВО - Влево, взмах влево\n" 107 | "СДВИГ ВПРАВО - Вправо, взмах вправо\n" 108 | "ПОВОРОТ - Вверх, взмах вверх\n" 109 | "УСКОРЕНИЕ - Вниз, взмах вниз\n" 110 | "СМЕНА НАПРАВЛ. - Шифт, касание\n" 111 | "ПАУЗА - Пробел, назад на android\n" 112 | 113 | #: common_tools/i18n.py:19 114 | msgid "" 115 | ": Records\n" 116 | "\n" 117 | msgstr "" 118 | ": Достижения\n" 119 | "\n" 120 | 121 | #: common_tools/i18n.py:71 122 | msgid "" 123 | ": Settings\n" 124 | "\n" 125 | "Graphic quality: GRAPHIC_CAPTION\n" 126 | "\n" 127 | "\n" 128 | "\n" 129 | "\n" 130 | "Screen mode: SCREEN_MODE_CAPTION\n" 131 | "\n" 132 | "\n" 133 | "\n" 134 | "\n" 135 | "Sound: SOUND_CAPTION\n" 136 | "\n" 137 | "\n" 138 | "\n" 139 | "\n" 140 | "Language: LANGUAGE_CAPTION\n" 141 | "\n" 142 | "\n" 143 | "\n" 144 | "\n" 145 | "*Restart the game to apply graphic settings\n" 146 | msgstr "" 147 | ": Настройки\n" 148 | "\n" 149 | "Качество графики: GRAPHIC_CAPTION\n" 150 | "\n" 151 | "\n" 152 | "\n" 153 | "\n" 154 | "Режим экрана: SCREEN_MODE_CAPTION\n" 155 | "\n" 156 | "\n" 157 | "\n" 158 | "\n" 159 | "Звук: SOUND_CAPTION\n" 160 | "\n" 161 | "\n" 162 | "\n" 163 | "\n" 164 | "Язык: LANGUAGE_CAPTION\n" 165 | "\n" 166 | "\n" 167 | "\n" 168 | "\n" 169 | "*Перезапустите игру, чтобы применить графические настройки\n" 170 | 171 | #: common_tools/i18n.py:82 172 | msgid "Disabled" 173 | msgstr "Выключен" 174 | 175 | #: common_tools/i18n.py:88 176 | msgid "English" 177 | msgstr "English" 178 | 179 | #: common_tools/i18n.py:115 180 | msgid "Exit game" 181 | msgstr "Выйти из игры" 182 | 183 | #: common_tools/i18n.py:85 184 | msgid "Full screen" 185 | msgstr "Полн. экр." 186 | 187 | #: common_tools/i18n.py:81 188 | msgid "High" 189 | msgstr "Высокое" 190 | 191 | #: common_tools/i18n.py:22 192 | msgid "" 193 | "Limitations of the free version of the game:\n" 194 | "1. Records are not displayed at this page, example entry:\n" 195 | "§ 1208 • Name • 2023-JAN-28 16:45\n" 196 | "2. Yellow triangles appear less frequently.\n" 197 | "3. The figure accelerates for less time.\n" 198 | "\n" 199 | "The paid version is available in the RuStore app market.\n" 200 | "\n" 201 | "If the game is successfully sold, I will add an online rating.\n" 202 | msgstr "" 203 | "Ограничения бесплатной версии игры:\n" 204 | "1. Достижения не отображаются в этом разделе, пример записи:\n" 205 | "§ 1208 • Имя игрока • 2023-JAN-28 16:45\n" 206 | "2. Жёлтые треугольники появляются реже.\n" 207 | "3. Фигура ускоряется на меньшее время.\n" 208 | "\n" 209 | "Платная версия доступна в магазине приложений RuStore.\n" 210 | "\n" 211 | "Если игра будет успешно продаваться, добавлю онлайн рейтинг.\n" 212 | 213 | #: common_tools/i18n.py:79 214 | msgid "Low" 215 | msgstr "Низкое" 216 | 217 | #: common_tools/i18n.py:90 218 | msgid "Main menu" 219 | msgstr "Главное меню" 220 | 221 | #: common_tools/i18n.py:80 222 | msgid "Middle" 223 | msgstr "Среднее" 224 | 225 | #: common_tools/i18n.py:84 226 | msgid "Normal" 227 | msgstr "Полный" 228 | 229 | #: common_tools/i18n.py:83 230 | msgid "Quiet" 231 | msgstr "Тихий" 232 | 233 | #: common_tools/i18n.py:113 234 | msgid "Resume game" 235 | msgstr "Продолжить игру" 236 | 237 | #: common_tools/i18n.py:112 238 | msgid "Save and exit" 239 | msgstr "Сохранить и выйти" 240 | 241 | #: common_tools/i18n.py:114 242 | msgid "To main menu" 243 | msgstr "В главное меню" 244 | 245 | #: common_tools/i18n.py:86 246 | msgid "Window" 247 | msgstr "Окно" 248 | 249 | #: common_tools/i18n.py:87 250 | msgid "Русский" 251 | msgstr "Русский" 252 | 253 | #~ msgid "Player name" 254 | #~ msgstr "Имя игрока" 255 | -------------------------------------------------------------------------------- /examples/trig/common_tools/math.py: -------------------------------------------------------------------------------- 1 | from math import cos, sin 2 | 3 | 4 | def polar2cart(r: float, phi: float) -> (float, float): 5 | """ 6 | Преобразовать полярные координаты в декартовы 7 | r - Радиальная координата 8 | phi - Угловая координата 9 | вернет (x, y) 10 | размер итоговой Декартовой плоскости = r*2 11 | """ 12 | return r * cos(phi), r * sin(phi) 13 | -------------------------------------------------------------------------------- /examples/trig/common_tools/matrix.py: -------------------------------------------------------------------------------- 1 | from numpy import ndarray, array, vstack, insert, delete, sum as np_sum, ndindex 2 | 3 | 4 | def m_create(row_cnt: int = None, col_cnt: int = None, *, data=None) -> ndarray: 5 | """ 6 | Создать матрицу 7 | (row_cnt, col_cnt) | data 8 | """ 9 | assert bool(row_cnt and col_cnt) ^ bool(data), 'set matrix Dimension or Data' 10 | if data: 11 | return array(data) 12 | else: 13 | assert row_cnt and col_cnt, 'row_cnt and col_cnt params expected' 14 | matrix = ndarray(shape=(row_cnt, col_cnt), dtype=int) 15 | matrix.fill(0) 16 | return matrix 17 | 18 | 19 | def m_intersects(matrix1: ndarray, matrix2: ndarray) -> bool: 20 | """Пересекаются ли матрицы""" 21 | return any((i > 1 for i in (matrix1 + matrix2).flatten())) 22 | 23 | 24 | def m_expand(matrix: ndarray, top=0, bottom=0, left=0, right=0, _filler=0) -> ndarray: 25 | """Добавить строки и колонки по краям матрицы""" 26 | assert top >= 0 and bottom >= 0 and left >= 0 and right >= 0 27 | new_matrix = matrix.copy() 28 | # rows 29 | col_cnt = new_matrix.shape[1] 30 | for i in range(top): 31 | new_matrix = vstack((tuple(_filler for _ in range(col_cnt)), new_matrix)) 32 | for i in range(bottom): 33 | new_matrix = vstack((new_matrix, tuple(_filler for _ in range(col_cnt)))) 34 | # cols 35 | for i in range(left): 36 | new_matrix = insert(new_matrix, 0, _filler, axis=1) 37 | col_cnt = new_matrix.shape[1] 38 | for i in range(right): 39 | new_matrix = insert(new_matrix, col_cnt, _filler, axis=1) 40 | return new_matrix 41 | 42 | 43 | def m_trim(matrix: ndarray, top=0, bottom=0, left=0, right=0) -> ndarray: 44 | """Удалить строки и колонки по краям матрицы""" 45 | assert top >= 0 and bottom >= 0 and left >= 0 and right >= 0 46 | row_cnt, col_cnt = matrix.shape 47 | return matrix[top:row_cnt - bottom, left:col_cnt - right] 48 | 49 | 50 | def m_is_sum_equals(matrix1: ndarray, matrix2: ndarray) -> bool: 51 | """Равны ли суммы элементов матриц""" 52 | assert matrix1.shape == matrix2.shape, 'Different matrix shapes' 53 | return np_sum(matrix1) == np_sum(matrix2) 54 | 55 | 56 | def m_del_rows(matrix: ndarray, rows: tuple or list): 57 | """Удалить из матрицы строки с указанными индексами""" 58 | return delete(matrix, rows, axis=0) 59 | 60 | 61 | def m_indexes(matrix: ndarray) -> iter: 62 | """ 63 | Итератор элементов матрицы 64 | Matrix index iterator (An N-dimensional iterator object to index arrays - internal shortcut) 65 | example: 66 | for row, col in m_indexes(matrix): 67 | """ 68 | return ndindex(matrix.shape) # noqa 69 | 70 | 71 | def m_2d_move_in_place(matrix: ndarray, row_num: int, col_num: int, x: int = 0, y: int = 0, filler: int = 0) -> None: 72 | """ 73 | *МЕДЛЕННЫЙ вариант 74 | Сдвинуть данные заданной матрицы в двухмерной плоскости 75 | """ 76 | # вправо 77 | if x > 0: 78 | for row in range(row_num): 79 | for col in range(col_num - 1, -1, -1): 80 | col_src = col - x 81 | matrix[row, col] = matrix[row, col_src] if col_src >= 0 else filler 82 | # влево 83 | if x < 0: 84 | for row in range(row_num): 85 | for col in range(col_num): 86 | col_src = col + abs(x) 87 | matrix[row, col] = matrix[row, col_src] if col_src < col_num else filler 88 | # вверх 89 | if y > 0: 90 | for col in range(col_num): 91 | for row in range(row_num): 92 | row_src = row + y 93 | matrix[row, col] = matrix[row_src, col] if row_src < row_num else filler 94 | # вниз 95 | if y < 0: 96 | for col in range(col_num): 97 | for row in range(row_num - 1, -1, -1): 98 | row_src = row - abs(y) 99 | matrix[row, col] = matrix[row_src, col] if row_src >= 0 else filler 100 | 101 | 102 | def m_2d_move(matrix: ndarray, x: int = 0, y: int = 0) -> ndarray: 103 | """Создать новую матрицу, сдвинутую в двухмерной плоскости""" 104 | assert x or y 105 | if x > 0: 106 | # вправо 107 | matrix = m_expand(m_trim(matrix, right=x), left=x) 108 | else: 109 | # влево 110 | matrix = m_expand(m_trim(matrix, left=abs(x)), right=abs(x)) 111 | if y > 0: 112 | # вверх 113 | matrix = m_expand(m_trim(matrix, top=y), bottom=y) 114 | else: 115 | # вниз 116 | matrix = m_expand(m_trim(matrix, bottom=abs(y)), top=abs(y)) 117 | return matrix 118 | -------------------------------------------------------------------------------- /examples/trig/common_tools/resources.py: -------------------------------------------------------------------------------- 1 | """ 2 | Путь к файлам ресурсов должен быть без пробелов и состоять из латинских символов 3 | На android нужно использовать bytes_buffer_instead_path 4 | """ 5 | import warnings 6 | 7 | from pygame import Surface 8 | from pygame.font import Font 9 | from pygame.image import load 10 | from pygame.mixer import Sound 11 | 12 | from common_tools.compatibility import bytes_buffer_instead_path 13 | from common_tools.consts import SCREEN_HEIGHT, SCREEN_WIDTH, SOUND_LEVEL 14 | from common_tools.surface import colored_block_surface 15 | 16 | 17 | def _load_font(path: str, size: int) -> Font: 18 | """Загрузить шрифт из файла""" 19 | try: 20 | return Font(bytes_buffer_instead_path(path), size) 21 | except FileNotFoundError: 22 | warnings.warn(f'Font not found: {path}') 23 | return Font(None, int(SCREEN_HEIGHT / 13)) # None - default font 24 | 25 | 26 | def _load_img(path: str) -> Surface: 27 | """Загрузить изображение из файла""" 28 | try: 29 | return load(bytes_buffer_instead_path(path)) 30 | except FileNotFoundError: 31 | warnings.warn(f'Image not found: {path}') 32 | return colored_block_surface('#FF00FFff', 100, 100) 33 | 34 | 35 | def _load_sound(path: str) -> Sound: 36 | """Загрузить изображение из файла""" 37 | try: 38 | return Sound(bytes_buffer_instead_path(path)) 39 | except FileNotFoundError: 40 | warnings.warn(f'Sound not found: {path}') 41 | return Sound(bytes_buffer_instead_path('res/sound/_silence.ogg')) 42 | 43 | 44 | # объекты изображений 45 | IMG_TRI_GRID = _load_img('res/img/stone_sand_light.png') # stone_sand_light.png 46 | IMG_AREA_PLAY = _load_img('res/img/sand1.jpg') # sand1.jpg 47 | IMG_AREA_INFO = _load_img('res/img/stone_gray.jpg') # stone_gray.jpg 48 | IMG_BORDER = _load_img('res/img/border.png') 49 | # 50 | IMG_MENU_BG = _load_img('res/img/menu_bg.jpg') 51 | IMG_LOADING = _load_img('res/img/loading.jpg') 52 | IMG_GAME_NAME_BG = _load_img('res/img/game_name_bg.jpg') 53 | IMG_LIGHT_SHINE = _load_img('res/img/light_shine.png') # stone_sand_dark.jpg 54 | IMG_BUTTON_ROOT_1 = _load_img('res/img/button_root_1.png') 55 | IMG_BUTTON_ROOT_2 = _load_img('res/img/button_root_2.png') 56 | IMG_BUTTON_ROOT_3 = _load_img('res/img/button_root_3.png') 57 | IMG_BUTTON_ROOT_4 = _load_img('res/img/button_root_4.png') 58 | IMG_BUTTON_ROOT_5 = _load_img('res/img/button_root_5.png') 59 | IMG_BUTTON_ROOT_6 = _load_img('res/img/button_root_6.png') 60 | IMG_BUTTON_RECT = _load_img('res/img/button_rect.png') 61 | IMG_INPUT = _load_img('res/img/input_bg.png') 62 | IMG_ICON_ABOUT = _load_img('res/img/icon_about.png') 63 | IMG_ICON_EXIT = _load_img('res/img/icon_exit.png') 64 | IMG_ICON_GUIDE = _load_img('res/img/icon_guide.png') 65 | IMG_ICON_RECORDS = _load_img('res/img/icon_records.png') 66 | IMG_ICON_SETTINGS = _load_img('res/img/icon_settings.png') 67 | IMG_ICON_PLAY = _load_img('res/img/icon_play.png') 68 | 69 | # объекты шрифтов - Font 70 | _faster_one_path = 'res/font/FasterOne/FasterOne-Regular.ttf' 71 | _devinne_swash_path = 'res/font/DevinneSwash/DevinneSwash.ttf' 72 | _alice_path = 'res/font/Alice/Alice-Regular.ttf' 73 | FONT_DEFAULT = Font(None, int(SCREEN_HEIGHT / 35)) 74 | FONT_SPEED = _load_font(_faster_one_path, int(SCREEN_HEIGHT / 20)) 75 | FONT_SCORE = _load_font(_devinne_swash_path, int(SCREEN_HEIGHT / 17)) 76 | FONT_BIGTEXT = _load_font(_devinne_swash_path, int(SCREEN_HEIGHT / 10)) 77 | FONT_MENU_GAME_NAME = _load_font(_devinne_swash_path, int(SCREEN_WIDTH / 5.5)) 78 | FONT_BUTTON = _load_font(_alice_path, int(SCREEN_WIDTH / 20)) 79 | FONT_TEXT_ML = _load_font(_alice_path, int(SCREEN_HEIGHT / 40)) 80 | 81 | # *после шрифта есть пустое место 82 | FONT_SPEED_HEIGHT_PX = FONT_SPEED.get_linesize() 83 | FONT_SCORE_HEIGHT_PX = FONT_SCORE.get_linesize() 84 | FONT_BIGTEXT_HEIGHT_PX = FONT_BIGTEXT.get_linesize() 85 | 86 | # звуки 87 | SOUND_MENU = _load_sound('res/sound/earth_qual0.ogg') 88 | SOUND_TEXT_INPUT = _load_sound('res/sound/stone_on_stone_high.ogg') 89 | SOUND_BUTTON_CLICK = _load_sound('res/sound/stone_on_stone_low.ogg') 90 | SOUND_GAME_OVER = _load_sound('res/sound/game_over.ogg') 91 | SOUND_PAUSE = _load_sound('res/sound/pause.ogg') 92 | SOUND_SCORE = _load_sound('res/sound/score_ring.ogg') 93 | SOUND_ROTATE = _load_sound('res/sound/rotate.ogg') 94 | SOUND_CLEAN_LINE = _load_sound('res/sound/stone_break.ogg') 95 | SOUND_SHIFT = _load_sound('res/sound/shift.ogg') 96 | SOUND_START = _load_sound('res/sound/start.ogg') 97 | SOUND_GAME_SPEED_UP = _load_sound('res/sound/game_speed_up.ogg') 98 | SOUND_FAST_FALL = _load_sound('res/sound/fast_fall.ogg') 99 | SOUND_CHANGE_DIR = _load_sound('res/sound/stone_on_stone_high.ogg') # sand_hit.ogg 100 | SOUND_EVENT_NO_INTERSECT = _load_sound('res/sound/event_no_intersect.ogg') 101 | SOUND_FIGURE_DROP = _load_sound('res/sound/stone_on_stone_low.ogg') 102 | SOUND_DENY = _load_sound('res/sound/deny.ogg') 103 | 104 | 105 | def set_sound_volume(value: str): 106 | """Установить громкость всех звуков""" 107 | sound_all = ( 108 | SOUND_MENU, SOUND_TEXT_INPUT, SOUND_BUTTON_CLICK, SOUND_GAME_OVER, SOUND_PAUSE, SOUND_SCORE, SOUND_ROTATE, 109 | SOUND_CLEAN_LINE, SOUND_SHIFT, SOUND_START, SOUND_GAME_SPEED_UP, SOUND_FAST_FALL, 110 | SOUND_EVENT_NO_INTERSECT, SOUND_CHANGE_DIR, SOUND_FIGURE_DROP, SOUND_DENY 111 | ) 112 | sound_correction_map = { 113 | SOUND_MENU: 0.7, 114 | SOUND_TEXT_INPUT: 1, 115 | SOUND_CHANGE_DIR: 0.61, 116 | SOUND_BUTTON_CLICK: 1, 117 | SOUND_FIGURE_DROP: 0.9, 118 | SOUND_GAME_OVER: 1, 119 | SOUND_PAUSE: 0.7, 120 | SOUND_SCORE: 1, 121 | SOUND_ROTATE: 0.25, 122 | SOUND_CLEAN_LINE: 1, 123 | SOUND_SHIFT: 0.38, 124 | SOUND_START: 1, 125 | SOUND_GAME_SPEED_UP: 1, 126 | SOUND_FAST_FALL: 0.05, 127 | SOUND_EVENT_NO_INTERSECT: 1, 128 | SOUND_DENY: 0.61, 129 | } 130 | assert len(sound_all) == len(sound_correction_map) 131 | # общий базовый уровень 132 | for i in sound_all: 133 | i.set_volume(SOUND_LEVEL[value]) 134 | 135 | # настройка отдельных звуков 136 | for sound, volume_rate in sound_correction_map.items(): 137 | sound.set_volume(sound.get_volume() * volume_rate) 138 | -------------------------------------------------------------------------------- /examples/trig/common_tools/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | import configparser 4 | import warnings 5 | import locale 6 | 7 | SETTING_GRAPHIC_LOW = 'l' 8 | SETTING_GRAPHIC_MIDDLE = 'm' 9 | SETTING_GRAPHIC_HIGH = 'h' 10 | 11 | SETTING_SOUND_NORMAL = 'n' 12 | SETTING_SOUND_QUIET = 'q' 13 | SETTING_SOUND_DISABLED = 'd' 14 | 15 | SETTING_SCREEN_MODE_FULL = 'f' 16 | SETTING_SCREEN_MODE_WINDOW = 'w' 17 | 18 | SETTING_LANGUAGE_RU = 'ru' 19 | SETTING_LANGUAGE_EN = 'en' 20 | 21 | 22 | def _clean_player_name(player_name: str) -> str: 23 | return player_name.replace(',', '_').replace('|', '_') 24 | 25 | 26 | class SettingsStorage: 27 | """Интерфейс для хранения настроек игры""" 28 | 29 | _config_section = 'GAME' 30 | _config_encoding = 'utf8' 31 | _dt_format = '%Y-%b-%d %H:%M' 32 | 33 | _key_graphic = 'graphic' 34 | _key_sound = 'sound' 35 | _key_screen_mode = 'screen_mode' 36 | _key_language = 'language' 37 | _key_player_name = 'player_name' 38 | _key_records = 'records' 39 | 40 | _valid_keys = { 41 | _key_graphic: (SETTING_GRAPHIC_LOW, SETTING_GRAPHIC_MIDDLE, SETTING_GRAPHIC_HIGH), 42 | _key_sound: (SETTING_SOUND_NORMAL, SETTING_SOUND_QUIET, SETTING_SOUND_DISABLED), 43 | _key_screen_mode: (SETTING_SCREEN_MODE_FULL, SETTING_SCREEN_MODE_WINDOW), 44 | _key_language: (SETTING_LANGUAGE_RU, SETTING_LANGUAGE_EN), 45 | _key_player_name: None, # any 46 | _key_records: None, # any 47 | } 48 | 49 | def __init__(self, user_data_dir: str, is_android_: bool): 50 | # пример: C:\Users\v.kaukin\AppData\Roaming\game.ikvk.trig_fall_pay\trig_fall.ini 51 | self.config_file_path = os.path.join(user_data_dir, 'trig_fall.ini') 52 | self.is_android = is_android_ 53 | 54 | self.config = configparser.ConfigParser() 55 | self.config.read(self.config_file_path, encoding=self._config_encoding) 56 | if self._config_section not in self.config: 57 | self.config[self._config_section] = {} 58 | 59 | locale_lang_code, locale_encoding = locale.getlocale() 60 | locale_lang_code = (locale_lang_code or '').lower() 61 | ru_by_default = 'russia' in locale_lang_code or locale_lang_code in ('ru', 'ru_ru', 'ru-ru') 62 | self.defaults = { 63 | self._key_graphic: SETTING_GRAPHIC_MIDDLE if self.is_android else SETTING_GRAPHIC_HIGH, 64 | self._key_sound: SETTING_SOUND_NORMAL, 65 | self._key_screen_mode: SETTING_SCREEN_MODE_FULL if self.is_android else SETTING_SCREEN_MODE_WINDOW, 66 | self._key_language: SETTING_LANGUAGE_RU if ru_by_default else SETTING_LANGUAGE_EN, 67 | self._key_player_name: 'Player', 68 | self._key_records: '', 69 | } 70 | for key in self._valid_keys.keys(): 71 | if not self._read(key): 72 | self._write(key, self.defaults[key]) 73 | 74 | def _read(self, key: str) -> str: 75 | res = self.config[self._config_section].get(key, '') 76 | has_valid_keys = self._valid_keys[key] 77 | if has_valid_keys and res and res not in self._valid_keys[key]: 78 | warnings.warn( 79 | f'SettingsStorage invalid value: "{res}" for key: "{key}" at "{self.config_file_path}" (fixed)') 80 | self._write(key, self.defaults[key]) 81 | return self.defaults[key] 82 | return res 83 | 84 | def _write(self, key: str, value: str): 85 | has_valid_keys = self._valid_keys[key] 86 | if has_valid_keys: 87 | assert value in self._valid_keys[key] 88 | self.config[self._config_section][key] = value 89 | with open(self.config_file_path, 'w', encoding=self._config_encoding) as f: 90 | self.config.write(f) 91 | 92 | @property 93 | def graphic(self) -> str: 94 | return self._read(self._key_graphic) 95 | 96 | @graphic.setter 97 | def graphic(self, value: str): 98 | self._write(self._key_graphic, value) 99 | 100 | @property 101 | def sound(self) -> str: 102 | return self._read(self._key_sound) 103 | 104 | @sound.setter 105 | def sound(self, value: str): 106 | self._write(self._key_sound, value) 107 | 108 | @property 109 | def screen_mode(self) -> str: 110 | return self._read(self._key_screen_mode) 111 | 112 | @screen_mode.setter 113 | def screen_mode(self, value: str): 114 | self._write(self._key_screen_mode, value) 115 | 116 | @property 117 | def language(self) -> str: 118 | return self._read(self._key_language) 119 | 120 | @language.setter 121 | def language(self, value: str): 122 | self._write(self._key_language, value) 123 | 124 | @property 125 | def player_name(self) -> str: 126 | return self._read(self._key_player_name) 127 | 128 | @player_name.setter 129 | def player_name(self, value: str): 130 | self._write(self._key_player_name, _clean_player_name(value)[:100]) 131 | 132 | @property 133 | def records(self) -> [[str, str, str], ...]: 134 | data_str = self._read(self._key_records) 135 | res = [] 136 | for i in data_str.split('|'): 137 | line_set = i.split(',') 138 | if len(line_set) == 3: 139 | res.append(line_set) 140 | return res 141 | 142 | def records_add(self, score: int, player_name: str): 143 | """ 144 | При добавлении сортирует и ограничивает количество 145 | Формат: "Очки,игрок,время|Очки,игрок,время" 146 | """ 147 | new_rec = f'{score},{_clean_player_name(player_name)},{datetime.datetime.now().strftime(self._dt_format)}' 148 | curr_data_str = self._read(self._key_records) 149 | new_data_str = '|'.join([i for i in [new_rec, curr_data_str] if i]) 150 | max_records = 25 151 | try: 152 | record_str_list = sorted(new_data_str.split('|'), key=lambda x: int(x.split(',')[0]), reverse=True) 153 | self._write(self._key_records, '|'.join(record_str_list[:max_records])) 154 | except Exception as e: 155 | warnings.warn(f'error on records_add (fixed): {e}') 156 | self._write(self._key_records, new_rec) 157 | 158 | def records_clean(self): 159 | self._write(self._key_records, '') 160 | -------------------------------------------------------------------------------- /examples/trig/common_tools/surface.py: -------------------------------------------------------------------------------- 1 | from functools import cache 2 | from typing import Any, Optional, Union 3 | 4 | from pygame.transform import rotate, scale 5 | from pygame.draw import rect 6 | from pygame.math import Vector2 7 | from pygame.font import Font 8 | from pygame import Color, Surface, BLEND_RGBA_MULT, mask 9 | 10 | from common_tools.consts import SURFACE_ARGS 11 | 12 | 13 | @cache 14 | def _circle_points(r: int) -> [(int, int), ...]: 15 | r = int(round(r)) 16 | x, y, e = r, 0, 1 - r 17 | points = [] 18 | while x >= y: 19 | points.append((x, y)) 20 | y += 1 21 | if e < 0: 22 | e += 2 * y - 1 23 | else: 24 | x -= 1 25 | e += 2 * (y - x) - 1 26 | points += [(y, x) for x, y in points if x > y] 27 | points += [(-x, y) for x, y in points if x] 28 | points += [(x, -y) for x, y in points if y] 29 | points.sort() 30 | return points 31 | 32 | 33 | def blit_rotated(surf: Surface, image: Surface, pos, origin_pos, angle: int, fill_color=(0, 0, 0, 0)): 34 | """ 35 | Вывести на surf изображение image, повернутое вокруг pos на angle градусов 36 | surf: target Surface 37 | image: Surface which has to be rotated and blit 38 | pos: position of the pivot on the target Surface surf (relative to the top left of surf) 39 | origin_pos: position of the pivot on the image Surface (relative to the top left of image) 40 | angle: angle of rotation in degrees 41 | """ 42 | # offset from pivot to center 43 | image_rect = image.get_rect(topleft=(pos[0] - origin_pos[0], pos[1] - origin_pos[1])) 44 | offset_center_to_pivot = Vector2(pos) - image_rect.center 45 | # rotated offset from pivot to center 46 | rotated_offset = offset_center_to_pivot.rotate(-angle) 47 | # rotated image center 48 | rotated_image_center = (pos[0] - rotated_offset.x, pos[1] - rotated_offset.y) 49 | # get a rotated image 50 | rotated_image = rotate(image, angle) 51 | rotated_image_rect = rotated_image.get_rect(center=rotated_image_center) 52 | # rotate and blit the image 53 | surf.blit(rotated_image, rotated_image_rect) 54 | # draw rectangle around the image 55 | rect(surf, fill_color, (*rotated_image_rect.topleft, *rotated_image.get_size()), 2) 56 | 57 | 58 | def colorize_surface(surface: Surface, color: str) -> Surface: 59 | colorized_surface = Surface(surface.get_size(), **SURFACE_ARGS) 60 | colorized_surface.fill(Color(color)) 61 | res_surface = surface.copy() 62 | res_surface.blit(colorized_surface, (0, 0), special_flags=BLEND_RGBA_MULT) 63 | return res_surface 64 | 65 | 66 | def texture_onto_sf(texture: Surface, surface: Surface, special_flags: int) -> Surface: 67 | """Масштабировать и наложить текстуру на поверхность""" 68 | scaled_texture = scale(texture.convert_alpha(), surface.get_size()) 69 | surface.blit(scaled_texture, (0, 0), special_flags=special_flags) 70 | return surface 71 | 72 | 73 | def text_surface(font_obj: Font, value: Any, color_main: str, color_shadow: Optional[str] = None, 74 | shadow_shift: float = 0.03) -> Surface: 75 | """Создать поверхность с текстом""" 76 | surface_main = font_obj.render(str(value), True, Color(color_main)) 77 | if not color_shadow: 78 | return surface_main 79 | surface_shadow = font_obj.render(str(value), True, Color(color_shadow)) 80 | w, h = surface_main.get_size() 81 | shadow_dx = shadow_dy = int(h * shadow_shift) 82 | surface_res = Surface((w + shadow_dx, h + shadow_dy), **SURFACE_ARGS) 83 | surface_res.blit(surface_shadow, (shadow_dx, shadow_dy)) 84 | surface_res.blit(surface_main, (0, 0)) 85 | return surface_res 86 | 87 | 88 | def text_ml_surface(font_obj: Font, ml_text: str, color: str, width: float, linesize_rate: float = 1.0) -> Surface: 89 | """Создать поверхность с многострочным текстом""" 90 | words_by_lines = [line.split(' ') for line in ml_text.splitlines()] 91 | space_width = font_obj.size(' ')[0] 92 | color_obj = Color(color) 93 | font_line_height = font_obj.get_linesize() * linesize_rate 94 | 95 | # get height 96 | x, y = 0, 0 97 | for line in words_by_lines: 98 | for word in line: 99 | word_width = font_obj.size(word)[0] 100 | if x + word_width >= width: 101 | x = 0 102 | y += font_line_height 103 | x += word_width + space_width 104 | x = 0 105 | y += font_line_height 106 | 107 | # render 108 | surface = Surface((width, y), **SURFACE_ARGS) 109 | x, y = 0, 0 110 | for line in words_by_lines: 111 | for word in line: 112 | word_surface = font_obj.render(word, True, color_obj) 113 | word_width = word_surface.get_size()[0] 114 | if x + word_width >= width: 115 | x = 0 116 | y += font_line_height 117 | surface.blit(word_surface, (x, y)) 118 | x += word_width + space_width 119 | x = 0 120 | y += font_line_height 121 | 122 | return surface 123 | 124 | 125 | def colored_block_surface(color: Union[Color, int, str], width: int, height: int): 126 | """Квадратная поверхность с заданным цветом и размером""" 127 | surface = Surface((width, height), **SURFACE_ARGS) 128 | surface.fill(color) 129 | return surface 130 | 131 | 132 | def outline(surface: Surface, color_str: str, size_px: int, alpha: int = 255) -> Surface: 133 | """Создать обводку поверхности указанной толщины и прозрачности""" 134 | mask_sf = mask.from_surface(surface).to_surface() 135 | mask_sf.set_colorkey(0) 136 | mask_sf.convert_alpha() 137 | mask_sf = colorize_surface(mask_sf, color_str) 138 | w, h = surface.get_size() 139 | res_sf = Surface((w + size_px * 2, h + size_px * 2), **SURFACE_ARGS) 140 | for dx, dy in _circle_points(size_px): 141 | res_sf.blit(mask_sf, (dx + size_px, dy + size_px)) 142 | res_sf.set_alpha(alpha) 143 | return res_sf 144 | 145 | 146 | def shine_surface(surface: Surface, color_str: str, size_px: int, shine_sf_alpha: int, alt_size: int = None) -> Surface: 147 | """ 148 | Добавить внешнее сияние для заданной поверхности 149 | alt_size для оптимизации скорости 150 | """ 151 | sw, sh = surface.get_size() 152 | res_sf = Surface((sw + size_px * 2, sh + size_px * 2), **SURFACE_ARGS) 153 | for i in range(1, (alt_size or size_px) + 1): 154 | outline_sf = outline(surface, color_str, i, shine_sf_alpha) 155 | res_sf.blit(outline_sf, (size_px - i, size_px - i)) 156 | res_sf.blit(surface, (size_px, size_px)) 157 | return res_sf 158 | -------------------------------------------------------------------------------- /examples/trig/fall/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/trig/fall/__init__.py -------------------------------------------------------------------------------- /examples/trig/fall/entities.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Optional 2 | 3 | from ecs_pattern import entity 4 | from numpy import ndarray 5 | 6 | from common_tools.components import ComSurface, ComSpeed, Com2dCoord, ComAnimationSet, ComAnimated, ComUiInput, \ 7 | ComUiButton, ComUiText 8 | 9 | 10 | @entity 11 | class GameData: 12 | do_play: bool # Флаг продолжения основного цикла игры 13 | scene_active: int # текущая сцена 14 | 15 | do_figure_fast_fall: bool # Флаг быстрого падения фигуры - для кнопок 16 | do_figure_fast_fall_until: float # Быстрое падение фигуры до указанной секунды - для тача и мыши 17 | do_figure_spawn: bool # Флаг указывает на необходимость создать фигуру 18 | do_figure_live_pause_until: float # пауза жизни фигур до указанной секунды (monotonic) 19 | 20 | score: int # Количество заработанных очков 21 | speed: int # Текущая скорость игры 22 | pause_cnt: int # Количество пауз 23 | figure_current: Tuple[int, int] # (Индекс фигуры, Индекс варианта фигуры), текущая фигура, для переворота 24 | figure_next: Tuple[int, int] # (Индекс фигуры, Индекс варианта фигуры), следующая фигура 25 | figure_dir: int # Направление падения фигуры: PLUS_MINUS_ONE 26 | figure_row: int # На сколько сдвинута строка фигуры с момента создания, для возможности переворота 27 | figure_col: int # На сколько сдвинут столбец фигуры с момента создания, для возможности переворота 28 | last_move_time: int # Время последнего передвижения фигуры вниз 29 | 30 | event_score_coord: Optional[Tuple[int, int]] # x, y активатора начисления случайного кол-ва очков, флаг 31 | event_score: int # Количество очков за взятие активатора 32 | 33 | event_no_intersect_coord: Optional[Tuple[int, int]] # x, y активатора режима падения без препятствий, флаг 34 | event_no_intersect_now: bool # Признак включенного режима фигуры - падение без препятствий 35 | 36 | grid_figure_active: ndarray # Матрица активной фигуры 37 | grid_static: ndarray # Матрица со статичными треугольниками 38 | grid_temp1: ndarray # Временная матрица 1 39 | grid_temp2: ndarray # Временная матрица 2 40 | 41 | grid_rows_for_del: [Tuple[int, int]] # строки матрицы для предстоящей очистки 42 | grid_cells_for_del: [Tuple[int, int]] # ячейки матрицы для предстоящей очистки 43 | grid_cells_for_del_blocked: [Tuple[int, int]] # ячейки матрицы для предстоящей очистки, заблокированные 44 | 45 | 46 | @entity 47 | class TriangleActiveUp(ComSurface): 48 | """Треугольник для текущей фигуры, вершина вверх""" 49 | 50 | 51 | @entity 52 | class TriangleActiveDown(ComSurface): 53 | """Треугольник для текущей фигуры, вершина вниз""" 54 | 55 | 56 | @entity 57 | class TriangleStaticUp(ComSurface): 58 | """Треугольник для статичной фигуры, вершина вверх""" 59 | 60 | 61 | @entity 62 | class TriangleStaticDown(ComSurface): 63 | """Треугольник для статичной фигуры, вершина вниз""" 64 | 65 | 66 | @entity 67 | class TriangleGridUp(ComSurface): 68 | """Треугольник для игровой таблицы, вершина вверх""" 69 | 70 | 71 | @entity 72 | class TriangleGridDown(ComSurface): 73 | """Треугольник для игровой таблицы, вершина вниз""" 74 | 75 | 76 | @entity 77 | class TriangleScoreUp(ComSurface): 78 | """Треугольник активатора получения очков, вершина вверх""" 79 | 80 | 81 | @entity 82 | class TriangleScoreDown(ComSurface): 83 | """Треугольник активатора получения очков, вершина вниз""" 84 | 85 | 86 | @entity 87 | class TriangleNoIntersectUp(ComSurface): 88 | """Треугольник активатора режима падения без препятствий, вершина вверх""" 89 | 90 | 91 | @entity 92 | class TriangleNoIntersectDown(ComSurface): 93 | """Треугольник активатора режима падения без препятствий, вершина вниз""" 94 | 95 | 96 | @entity 97 | class InfoArea(Com2dCoord, ComSurface): 98 | """Поле информации об игре""" 99 | 100 | 101 | @entity 102 | class PlayArea(Com2dCoord, ComSurface): 103 | """Игровое поле""" 104 | 105 | 106 | @entity 107 | class Border(Com2dCoord, ComSurface): 108 | """Граница инфо поля""" 109 | 110 | 111 | @entity 112 | class IconNextFigure(ComSurface): 113 | """Картинка следующей фигуры""" 114 | pass 115 | 116 | 117 | @entity 118 | class LabelPause(ComSurface): 119 | pass 120 | 121 | 122 | @entity 123 | class LabelGameOver(ComSurface): 124 | pass 125 | 126 | 127 | @entity 128 | class TextSpeed(Com2dCoord, ComSurface): 129 | pass 130 | 131 | 132 | @entity 133 | class TextScore(Com2dCoord, ComSurface): 134 | pass 135 | 136 | 137 | @entity 138 | class SparkDel1AnimationSet(ComAnimationSet): 139 | """ 140 | Набор искр разной прозрачности для взрыва треугольника при очистке 141 | 0 - прозрачная, 255-не прозрачная 142 | """ 143 | 144 | def __post_init__(self): 145 | assert len(self.frames) == 256 146 | 147 | 148 | @entity 149 | class SparkDel2AnimationSet(ComAnimationSet): 150 | """ 151 | Набор искр разной прозрачности для взрыва треугольника при очистке - вариант для заблокированных 152 | 0 - прозрачная, 255-не прозрачная 153 | """ 154 | 155 | def __post_init__(self): 156 | assert len(self.frames) == 256 157 | 158 | 159 | @entity 160 | class Spark(ComSpeed, Com2dCoord, ComAnimated): 161 | """Затухающая движущаяся искра""" 162 | 163 | 164 | @entity 165 | class InputPlayerName(ComUiInput): 166 | """Поле для ввода имени, после окончания игры""" 167 | 168 | 169 | @entity 170 | class ButtonSaveResult(ComUiButton): 171 | """Кнопка - Сохранить результат на указанное имя и выйти, после окончания игры""" 172 | 173 | 174 | @entity 175 | class ButtonResumeGame(ComUiButton): 176 | """Кнопка - Продолжить игру, при паузе""" 177 | 178 | 179 | @entity 180 | class ButtonToMainMenu(ComUiButton): 181 | """Кнопка - Продолжить игру, при паузе""" 182 | 183 | 184 | @entity 185 | class ButtonExitGame(ComUiButton): 186 | """Кнопка - Выйти из игры, при паузе""" 187 | 188 | 189 | @entity 190 | class TextGameResults(ComUiText): 191 | """Текст - Форма с результатом игры и контролами""" 192 | 193 | 194 | @entity 195 | class Loading(Com2dCoord, ComSurface): 196 | """Картинка загрузки""" 197 | -------------------------------------------------------------------------------- /examples/trig/fall/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame import Color, Surface 3 | from pygame.time import Clock 4 | from ecs_pattern import EntityManager, SystemManager 5 | 6 | from common_tools.consts import FPS_MAX, SCREEN_HEIGHT, FPS_SHOW 7 | from common_tools.resources import FONT_DEFAULT 8 | from fall.entities import GameData 9 | from fall.systems import SysControl, SysDraw, SysLiveFigure, SysInit, SysLive 10 | 11 | 12 | def game_loop(display: Surface, clock: Clock): 13 | """Основной цикл игры""" 14 | entities = EntityManager() 15 | system_manager = SystemManager([ 16 | SysInit(entities), 17 | SysControl(entities), 18 | SysLiveFigure(entities), 19 | SysLive(entities, clock), 20 | SysDraw(entities, display), 21 | ]) 22 | system_manager.start_systems() 23 | 24 | game_data: GameData = next(entities.get_by_class(GameData)) 25 | 26 | while game_data.do_play: 27 | clock.tick_busy_loop(FPS_MAX) # tick_busy_loop точный + ест проц, tick грубый + не ест проц 28 | system_manager.update_systems() 29 | if FPS_SHOW: 30 | display.blit( 31 | FONT_DEFAULT.render(f'FPS: {int(clock.get_fps())}', True, Color('#1339AC')), (0, SCREEN_HEIGHT * 0.98)) 32 | pygame.display.flip() # draw changes on screen 33 | 34 | system_manager.stop_systems() 35 | -------------------------------------------------------------------------------- /examples/trig/fall/surfaces.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | from pygame import Color, Surface, gfxdraw, transform, BLEND_RGBA_MULT 4 | from pygame.transform import scale 5 | 6 | from common_tools.i18n import I18N_SF_TEXT_GAME_RESULTS 7 | from common_tools.consts import INFO_AREA_WIDTH, PLAY_AREA_WIDTH, GRID_TRI_WIDTH, GRID_TRI_HEIGHT, \ 8 | SCREEN_WIDTH, SCREEN_HEIGHT, INFO_AREA_HEIGHT, PLAY_AREA_HEIGHT, \ 9 | FONT_COLOR_PAUSE1, FONT_COLOR_GAME_OVER1, FONT_COLOR_PAUSE2, FONT_COLOR_GAME_OVER2, \ 10 | SPARK_SIZE_PX, SURFACE_ARGS, IS_ACTIVE, IS_STATIC 11 | from common_tools.resources import FONT_BIGTEXT, IMG_AREA_INFO, IMG_AREA_PLAY, IMG_BORDER, IMG_TRI_GRID, FONT_TEXT_ML, \ 12 | IMG_INPUT, IMG_LOADING 13 | from common_tools.surface import texture_onto_sf, text_surface, colored_block_surface, text_ml_surface, shine_surface 14 | 15 | 16 | def _surface_triangle(color: str, reflect: bool) -> Surface: 17 | """Поверхность в форме треугольника""" 18 | width = int(SCREEN_WIDTH * GRID_TRI_WIDTH) 19 | height = int(SCREEN_HEIGHT * GRID_TRI_HEIGHT) 20 | surface = Surface((width, height), **SURFACE_ARGS) 21 | points = ((width / 2, 0), (0, height), (width, height), (width / 2, 0)) 22 | # сглаживание 23 | gfxdraw.aapolygon(surface, points, Color(color)) 24 | # основа 25 | gfxdraw.filled_polygon(surface, points, Color(color)) 26 | # текстура 27 | tri = scale(IMG_TRI_GRID.convert_alpha(), surface.get_size()) 28 | surface.blit(tri, (0, 0), special_flags=BLEND_RGBA_MULT) 29 | # отражение по вертикали 30 | surface = transform.flip(surface, False, reflect) 31 | return surface 32 | 33 | 34 | def surface_spark(alpha: int, center_color_str: str, main_color_str: str, noise_color_str: str = None) -> Surface: 35 | """ 36 | Поверхность в виде вертикального крестика с точкой в центре (Искра) 37 | alpha=0 - прозрачный 38 | """ 39 | assert 0 <= alpha <= 255 40 | surface = Surface((3, 3), **SURFACE_ARGS) 41 | alpha_str = f'{alpha:x}'.zfill(2) 42 | surface.set_at((1, 1), Color(f'{center_color_str}{alpha_str}')) 43 | for main_point in ((1, 0), (0, 1), (1, 2), (2, 1)): 44 | surface.set_at(main_point, Color(f'{main_color_str}{alpha_str}')) 45 | if noise_color_str: 46 | surface.set_at((randint(0, 2), randint(0, 2)), Color(f'{noise_color_str}FF')) 47 | surface = scale(surface, (SPARK_SIZE_PX, SPARK_SIZE_PX)) 48 | return surface 49 | 50 | 51 | def surface_info_area() -> Surface: 52 | # D7D7D7-single 53 | surface = colored_block_surface( 54 | Color('#ffffff'), width=int(SCREEN_WIDTH * INFO_AREA_WIDTH), height=SCREEN_HEIGHT * INFO_AREA_HEIGHT) 55 | return texture_onto_sf(IMG_AREA_INFO, surface, BLEND_RGBA_MULT) 56 | 57 | 58 | def surface_play_area() -> Surface: 59 | # DCB78B-single 60 | surface = colored_block_surface( # #ebd6be 61 | Color('#efdfcb'), width=int(SCREEN_WIDTH * PLAY_AREA_WIDTH), height=SCREEN_HEIGHT * PLAY_AREA_HEIGHT) 62 | return texture_onto_sf(IMG_AREA_PLAY, surface, BLEND_RGBA_MULT) 63 | 64 | 65 | def surface_border() -> Surface: 66 | scaled_texture = scale(IMG_BORDER.convert_alpha(), 67 | (int(SCREEN_WIDTH * INFO_AREA_WIDTH), int(SCREEN_WIDTH * INFO_AREA_HEIGHT * 0.15))) 68 | return scaled_texture 69 | 70 | 71 | def surface_triangle_active_up() -> Surface: 72 | return _surface_triangle('#AE2E0B', False) 73 | 74 | 75 | def surface_triangle_active_down() -> Surface: 76 | return _surface_triangle('#AE2E0B', True) 77 | 78 | 79 | def surface_triangle_grid_up() -> Surface: 80 | return _surface_triangle('#f5ebd6', False) # EEDDBB single 81 | 82 | 83 | def surface_triangle_grid_down() -> Surface: 84 | return _surface_triangle('#f5ebd6', True) # EEDDBB single 85 | 86 | 87 | def surface_triangle_static_up() -> Surface: 88 | return _surface_triangle('#5C8072', False) 89 | 90 | 91 | def surface_triangle_static_down() -> Surface: 92 | return _surface_triangle('#5C8072', True) 93 | 94 | 95 | def surface_triangle_score_up() -> Surface: 96 | return _surface_triangle('#FFD700', False) 97 | 98 | 99 | def surface_triangle_score_down() -> Surface: 100 | return _surface_triangle('#FFD700', True) 101 | 102 | 103 | def surface_triangle_no_intersect_up() -> Surface: 104 | return _surface_triangle('#9932CC', False) 105 | 106 | 107 | def surface_triangle_no_intersect_down() -> Surface: 108 | return _surface_triangle('#9932CC', True) 109 | 110 | 111 | def surface_label_pause() -> Surface: 112 | font_surface = text_surface(FONT_BIGTEXT, 'PAUSE', FONT_COLOR_PAUSE1, FONT_COLOR_PAUSE2) 113 | font_surface.set_alpha(int(255 * 0.8)) 114 | return font_surface 115 | 116 | 117 | def surface_label_game_over() -> Surface: 118 | text1, text2, *more = 'GAME OVER'.split(' ') # *шрифт без кириллицы 119 | font_surface1 = text_surface(FONT_BIGTEXT, text1, FONT_COLOR_GAME_OVER1, FONT_COLOR_GAME_OVER2) 120 | font_surface2 = text_surface(FONT_BIGTEXT, text2, FONT_COLOR_GAME_OVER1, FONT_COLOR_GAME_OVER2) 121 | fs1_w, fs1_h = font_surface1.get_size() 122 | fs2_w, fs2_h = font_surface2.get_size() 123 | line_dist = fs1_h * 0.1 124 | font_surface_res = Surface((max(fs1_w, fs2_w), max(fs1_h, fs2_h) * 2 + line_dist), **SURFACE_ARGS) 125 | font_surface_res.blit(font_surface1, (0, 0)) 126 | font_surface_res.blit(font_surface2, (0, fs2_h + line_dist)) 127 | font_surface_res.set_alpha(int(255 * 0.8)) 128 | return font_surface_res 129 | 130 | 131 | def surface_text_game_results(score: int) -> Surface: 132 | text = I18N_SF_TEXT_GAME_RESULTS.replace('SCORE', str(score)) 133 | surface = text_ml_surface(FONT_TEXT_ML, text, '#696969', SCREEN_WIDTH * 0.8) 134 | surface.set_alpha(int(255 * 0.95)) 135 | return surface 136 | 137 | 138 | def surface_input_player_name(input_state: int) -> Surface: 139 | if input_state == IS_STATIC: 140 | shine_color = '#FFD700' 141 | elif input_state == IS_ACTIVE: 142 | shine_color = '#7FFFD4' 143 | else: 144 | raise ValueError(f'wrong input_state: {input_state}') 145 | surface = colored_block_surface('#ffffff', SCREEN_WIDTH * 0.7, FONT_TEXT_ML.get_linesize() * 2) 146 | surface = texture_onto_sf(IMG_INPUT, surface, BLEND_RGBA_MULT) 147 | surface = shine_surface(surface, '#333333', 2, 200) 148 | surface = shine_surface(surface, shine_color, int(FONT_TEXT_ML.get_linesize() * 0.3), 10) 149 | return surface 150 | 151 | 152 | def surface_loading() -> Surface: 153 | return scale(IMG_LOADING.convert_alpha(), (SCREEN_WIDTH, SCREEN_HEIGHT)) 154 | -------------------------------------------------------------------------------- /examples/trig/i18n_compile.cmd: -------------------------------------------------------------------------------- 1 | REM Скомпилировать .mo из .po 2 | chcp 65001 3 | @echo off 4 | 5 | msgfmt common_tools/locale/ru/LC_MESSAGES/trig_fall.po -o common_tools/locale/ru/LC_MESSAGES/trig_fall.mo 6 | 7 | echo i18n_compile done at %date% %time% 8 | echo ~ 9 | -------------------------------------------------------------------------------- /examples/trig/i18n_make.cmd: -------------------------------------------------------------------------------- 1 | REM Пересобрать .po проекта 2 | chcp 65001 3 | @echo off 4 | 5 | REM создать шаблон без переводов 6 | xgettext -d trig_fall -s -L Python --no-wrap -o common_tools/locale/ru/LC_MESSAGES/trig_fall.pot common_tools/i18n.py 7 | 8 | REM объединить старый перевод и новый шаблон - fuzzy 9 | msgmerge -U common_tools/locale/ru/LC_MESSAGES/trig_fall.po common_tools/locale/ru/LC_MESSAGES/trig_fall.pot 10 | 11 | REM удалить шаблон 12 | del "common_tools\locale\ru\LC_MESSAGES\trig_fall.pot" 13 | 14 | echo i18n_make done at %date% %time% 15 | echo ~ 16 | -------------------------------------------------------------------------------- /examples/trig/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pygame 4 | from pygame import FULLSCREEN, DOUBLEBUF, SCALED 5 | from pygame.time import Clock 6 | 7 | from common_tools.consts import SCREEN_WIDTH, SCREEN_HEIGHT, SURFACE_ARGS, SETTING_SCREEN_MODE_FULL, SETTINGS_STORAGE 8 | from fall.main import game_loop 9 | from menu.main import menu_loop 10 | 11 | os.environ['SDL_VIDEO_CENTERED'] = '1' # window at center 12 | 13 | 14 | def main(): 15 | """Точка входа в приложение""" 16 | # pygame.init() в common_tools.consts 17 | pygame.display.set_caption('Trig FALL') 18 | is_fullscreen = SETTINGS_STORAGE.screen_mode == SETTING_SCREEN_MODE_FULL 19 | display = pygame.display.set_mode( 20 | size=(SCREEN_WIDTH, SCREEN_HEIGHT), 21 | flags=(FULLSCREEN | SCALED if is_fullscreen else 0) | DOUBLEBUF, 22 | depth=SURFACE_ARGS['depth'] 23 | ) 24 | clock = Clock() 25 | 26 | while True: 27 | menu_loop(display, clock) 28 | game_loop(display, clock) 29 | 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /examples/trig/menu/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/examples/trig/menu/__init__.py -------------------------------------------------------------------------------- /examples/trig/menu/entities.py: -------------------------------------------------------------------------------- 1 | from ecs_pattern import entity 2 | from pygame.mixer import Channel 3 | 4 | from common_tools.components import ComUiButton, ComUiText, Com2dCoord, ComSurface, ComAnimationSet, \ 5 | ComAnimated 6 | 7 | 8 | @entity 9 | class MenuData: 10 | do_menu: bool # Флаг продолжения основного цикла меню 11 | scene_active: int # текущая сцена 12 | music_channel: Channel # фоновая музыка меню 13 | 14 | 15 | @entity 16 | class Background(Com2dCoord, ComSurface): 17 | """Общий фон меню""" 18 | 19 | 20 | @entity 21 | class LabelGameName(Com2dCoord, ComSurface): 22 | """Название игры""" 23 | 24 | 25 | @entity 26 | class ButtonToMenuRoot(ComUiButton): 27 | """Кнопка - Переход к главному меню""" 28 | 29 | 30 | @entity 31 | class ButtonPlay(ComUiButton): 32 | """Кнопка - старт игры""" 33 | 34 | 35 | @entity 36 | class ButtonGuide(ComUiButton): 37 | """Кнопка - Как играть""" 38 | 39 | 40 | @entity 41 | class ButtonSettings(ComUiButton): 42 | """Кнопка - Настройки""" 43 | 44 | 45 | @entity 46 | class ButtonRecords(ComUiButton): 47 | """Кнопка - Достижения""" 48 | 49 | 50 | @entity 51 | class ButtonAbout(ComUiButton): 52 | """Кнопка - Об игре""" 53 | 54 | 55 | @entity 56 | class ButtonExit(ComUiButton): 57 | """Кнопка - Выход""" 58 | 59 | 60 | @entity 61 | class TextGuide(ComUiText): 62 | """Текст - Как играть""" 63 | 64 | 65 | @entity 66 | class TextRecords(ComUiText): 67 | """Текст - Достижения""" 68 | 69 | 70 | @entity 71 | class TextAbout(ComUiText): 72 | """Текст - Об игре""" 73 | 74 | 75 | @entity 76 | class TextSettings(ComUiText): 77 | """Текст - Настройки""" 78 | 79 | 80 | @entity 81 | class ButtonGraphicHigh(ComUiButton): 82 | """Кнопка - Качество графики - высокое""" 83 | 84 | 85 | @entity 86 | class ButtonGraphicMiddle(ComUiButton): 87 | """Кнопка - Качество графики - среднее""" 88 | 89 | 90 | @entity 91 | class ButtonGraphicLow(ComUiButton): 92 | """Кнопка - Качество графики - низкое""" 93 | 94 | 95 | @entity 96 | class ButtonSoundNormal(ComUiButton): 97 | """Кнопка - Включить звук 100%""" 98 | 99 | 100 | @entity 101 | class ButtonSoundQuiet(ComUiButton): 102 | """Кнопка - Включить звук 40%""" 103 | 104 | 105 | @entity 106 | class ButtonSoundDisable(ComUiButton): 107 | """Кнопка - Выключить звук""" 108 | 109 | 110 | @entity 111 | class ButtonScreenModeFull(ComUiButton): 112 | """Кнопка - Включить режим окна - полный экран""" 113 | 114 | 115 | @entity 116 | class ButtonScreenModeWindow(ComUiButton): 117 | """Кнопка - Включить режим окна - окно""" 118 | 119 | 120 | @entity 121 | class ShineLightAnimationSet(ComAnimationSet): 122 | """Набор кадров анимации сияния света - светлый вариант""" 123 | 124 | 125 | @entity 126 | class ShineDarkAnimationSet(ComAnimationSet): 127 | """Набор кадров анимации сияния света - тёмный вариант""" 128 | 129 | 130 | @entity 131 | class Shine(Com2dCoord, ComAnimated): 132 | """Анимация - сияние света""" 133 | 134 | 135 | @entity 136 | class ButtonLanguageRu(ComUiButton): 137 | """Кнопка - Включить русский язык""" 138 | 139 | 140 | @entity 141 | class ButtonLanguageEn(ComUiButton): 142 | """Кнопка - Включить английский язык""" 143 | -------------------------------------------------------------------------------- /examples/trig/menu/main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame import Color, Surface 3 | from pygame.time import Clock 4 | from ecs_pattern import EntityManager, SystemManager 5 | 6 | from common_tools.resources import FONT_DEFAULT 7 | from common_tools.consts import FPS_MAX, SCREEN_HEIGHT, FPS_SHOW 8 | from menu.entities import MenuData 9 | from menu.systems import SysControl, SysDraw, SysInit, SysLive 10 | 11 | 12 | def menu_loop(display: Surface, clock: Clock): 13 | """Основной цикл меню игры""" 14 | try: 15 | import pyi_splash # noqa (PyInstaller splash screen, поставляется вместе с ним) 16 | pyi_splash.close() 17 | except Exception: # noqa 18 | pass 19 | 20 | entities = EntityManager() 21 | system_manager = SystemManager([ 22 | SysInit(entities), 23 | SysControl(entities), 24 | SysLive(entities, clock), 25 | SysDraw(entities, display), 26 | ]) 27 | system_manager.start_systems() 28 | 29 | menu_data: MenuData = next(entities.get_by_class(MenuData)) 30 | 31 | while menu_data.do_menu: 32 | clock.tick_busy_loop(FPS_MAX) # tick_busy_loop точный + ест проц, tick грубый + не ест проц 33 | system_manager.update_systems() 34 | if FPS_SHOW: 35 | display.blit( 36 | FONT_DEFAULT.render(f'FPS: {int(clock.get_fps())}', True, Color('#1339AC')), (0, SCREEN_HEIGHT * 0.98)) 37 | pygame.display.flip() # draw changes on screen 38 | 39 | system_manager.stop_systems() 40 | -------------------------------------------------------------------------------- /examples/trig/menu/surfaces.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from pygame import Surface, BLEND_RGBA_MULT 4 | from pygame.transform import scale 5 | 6 | from common_tools.consts import SCREEN_WIDTH, SURFACE_ARGS, MENU_SHINE_WIDTH, SCREEN_HEIGHT, TEXT_ML_WIDTH, \ 7 | SETTINGS_STORAGE, GAME_NAME, GAME_VERSION, PACKAGE_EDITION, PACKAGE_EDITION_FREE 8 | from common_tools.resources import IMG_LIGHT_SHINE, FONT_MENU_GAME_NAME, FONT_TEXT_ML, IMG_GAME_NAME_BG, IMG_MENU_BG 9 | from common_tools.i18n import I18N_SF_TEXT_RECORDS, I18N_SF_TEXT_ABOUT, I18N_SF_TEXT_GUIDE, I18N_SF_TEXT_SETTINGS, \ 10 | SETTING_GRAPHIC_CAPTION, SETTING_SOUND_CAPTION, SETTING_LANGUAGE_CAPTION, SETTING_SCREEN_MODE_CAPTION, \ 11 | I18N_SF_TEXT_FREE_VERSION 12 | from common_tools.surface import blit_rotated, colorize_surface, texture_onto_sf, text_surface, text_ml_surface, \ 13 | shine_surface 14 | 15 | 16 | def _text_ml_surface(text: str) -> Surface: 17 | return text_ml_surface(FONT_TEXT_ML, text, '#696969', SCREEN_WIDTH * TEXT_ML_WIDTH) 18 | 19 | 20 | def surface_shine_animation_set(color: str) -> Tuple[Surface, ...]: 21 | """Набор кадров анимации сияния с указанным цветом""" 22 | shine_w = shine_h = SCREEN_WIDTH * MENU_SHINE_WIDTH 23 | img_light_shine = scale(IMG_LIGHT_SHINE.convert_alpha(), (shine_w, shine_h)) 24 | 25 | shine_frames = [] 26 | shine_alpha_min = int(255 * 0.38) # мин. непрозрачность 27 | shine_alpha_max = int(255 * 0.0) # макс. непрозрачность 28 | shine_frame_cnt = 80 # от 0 до 360 градусов 29 | _ratio = (shine_alpha_min - shine_alpha_max) / shine_frame_cnt * 2 30 | _half_shine_bloom_val_set = [] 31 | for i in range(int(shine_frame_cnt / 2)): 32 | _half_shine_bloom_val_set.append(int(shine_alpha_min - i * _ratio)) 33 | shine_bloom_val_set = list(reversed(_half_shine_bloom_val_set)) + _half_shine_bloom_val_set 34 | for i in range(shine_frame_cnt): 35 | res_sf = Surface((shine_w, shine_h), **SURFACE_ARGS).convert_alpha() 36 | _angle = int(360 / shine_frame_cnt) * i 37 | for angle in (_angle, -_angle): 38 | new_sf = Surface((shine_w, shine_h), **SURFACE_ARGS).convert_alpha() 39 | req_center_point = (shine_w / 2, shine_h / 2) 40 | blit_rotated(new_sf, img_light_shine, req_center_point, req_center_point, angle) 41 | new_sf.set_alpha(shine_bloom_val_set[i]) # 255 непрозрачный 42 | res_sf.blit(colorize_surface(new_sf, color), (0, 0)) 43 | shine_frames.append(res_sf) 44 | 45 | return tuple(shine_frames) 46 | 47 | 48 | def surface_background() -> Surface: 49 | return scale(IMG_MENU_BG.convert_alpha(), (SCREEN_WIDTH, SCREEN_HEIGHT)) 50 | 51 | 52 | def surface_label_game_name() -> Surface: 53 | font_sf = text_surface(FONT_MENU_GAME_NAME, 'Trig fall', '#FFD700') 54 | shine_size = int(FONT_MENU_GAME_NAME.get_height() * 0.05) 55 | shine_sf = shine_surface(font_sf, '#111111', shine_size, 15) 56 | textured_font_sf = texture_onto_sf(IMG_GAME_NAME_BG, font_sf, BLEND_RGBA_MULT) 57 | shine_sf.blit(textured_font_sf, (shine_size, shine_size)) 58 | return shine_sf 59 | 60 | 61 | def surface_input_name() -> Surface: 62 | pass 63 | 64 | 65 | def surface_text_about() -> Surface: 66 | return _text_ml_surface( 67 | GAME_NAME.upper() + 68 | I18N_SF_TEXT_ABOUT.replace('GAME_VERSION', GAME_VERSION) 69 | ) 70 | 71 | 72 | def surface_text_records() -> Surface: 73 | if PACKAGE_EDITION == PACKAGE_EDITION_FREE: 74 | text = I18N_SF_TEXT_FREE_VERSION 75 | else: 76 | text = '\n'.join([f' § {score} • {name} • {dt}' for score, name, dt in SETTINGS_STORAGE.records]) 77 | return _text_ml_surface( 78 | GAME_NAME.upper() + 79 | I18N_SF_TEXT_RECORDS + 80 | text 81 | ) 82 | 83 | 84 | def surface_text_guide() -> Surface: 85 | return _text_ml_surface( 86 | GAME_NAME.upper() + 87 | I18N_SF_TEXT_GUIDE 88 | ) 89 | 90 | 91 | def surface_text_settings() -> Surface: 92 | text = \ 93 | GAME_NAME.upper() + \ 94 | I18N_SF_TEXT_SETTINGS.replace( 95 | 'GRAPHIC_CAPTION', SETTING_GRAPHIC_CAPTION[SETTINGS_STORAGE.graphic] 96 | ).replace( 97 | 'SCREEN_MODE_CAPTION', SETTING_SCREEN_MODE_CAPTION[SETTINGS_STORAGE.screen_mode] 98 | ).replace( 99 | 'SOUND_CAPTION', SETTING_SOUND_CAPTION[SETTINGS_STORAGE.sound] 100 | ).replace( 101 | 'LANGUAGE_CAPTION', SETTING_LANGUAGE_CAPTION[SETTINGS_STORAGE.language] 102 | ) 103 | return text_ml_surface(FONT_TEXT_ML, text, '#696969', SCREEN_WIDTH * TEXT_ML_WIDTH) 104 | -------------------------------------------------------------------------------- /examples/trig/menu/systems.py: -------------------------------------------------------------------------------- 1 | from sys import exit # *for windows 2 | from typing import Callable, Optional 3 | 4 | import pygame 5 | from pygame import Surface, Rect, BLEND_RGBA_MULT 6 | from pygame.event import Event 7 | from pygame.locals import QUIT, KEYDOWN, K_ESCAPE, MOUSEBUTTONUP, MOUSEBUTTONDOWN, MOUSEMOTION, K_AC_BACK 8 | from pygame.math import Vector2 9 | from pygame.transform import smoothscale 10 | from ecs_pattern import System, EntityManager 11 | 12 | from common_tools.components import ComAnimated, ComSpeed 13 | from common_tools.consts import BS_STATIC, FPS_MAX, SETTINGS_STORAGE, MENU_SHINE_WIDTH, \ 14 | TEXT_ML_HEIGHT, TEXT_ML_WIDTH, SCREEN_WIDTH, SCREEN_HEIGHT, MENU_SCENE_ABOUT, MENU_SCENE_GUIDE, \ 15 | MENU_SCENE_RECORDS, MENU_SCENE_SETTINGS, SURFACE_ARGS, MENU_SCENE_ROOT, MENU_ROOT_AREA_GAME_NAME_HEIGHT, \ 16 | MENU_ROOT_AREA_BUTTONS_HEIGHT, MENU_ROOT_BUTTON_GROUP_GAP_WIDTH, MENU_ROOT_BUTTON_GROUP_WIDTH, BUTTON_WIDTH 17 | from common_tools.resources import IMG_BUTTON_ROOT_1, IMG_BUTTON_ROOT_2, IMG_BUTTON_ROOT_3, \ 18 | IMG_BUTTON_ROOT_4, IMG_BUTTON_ROOT_5, IMG_BUTTON_ROOT_6, IMG_ICON_RECORDS, IMG_ICON_SETTINGS, \ 19 | IMG_ICON_ABOUT, IMG_ICON_GUIDE, IMG_ICON_EXIT, IMG_ICON_PLAY, FONT_TEXT_ML, IMG_TRI_GRID, SOUND_START, \ 20 | set_sound_volume, SOUND_MENU, SOUND_BUTTON_CLICK, SOUND_DENY 21 | from common_tools.settings import SETTING_GRAPHIC_HIGH, SETTING_SCREEN_MODE_FULL, SETTING_SCREEN_MODE_WINDOW, \ 22 | SETTING_SOUND_DISABLED, SETTING_SOUND_NORMAL, SETTING_SOUND_QUIET, SETTING_GRAPHIC_LOW, SETTING_GRAPHIC_MIDDLE, \ 23 | SETTING_LANGUAGE_RU, SETTING_LANGUAGE_EN 24 | from common_tools.surface import colorize_surface, colored_block_surface, shine_surface, texture_onto_sf 25 | from common_tools.gui import gui_button_attrs, draw_button, draw_text_ml, control_button 26 | from common_tools.i18n import I18N_SETTING_GRAPHIC_LOW, I18N_SETTING_GRAPHIC_MIDDLE, I18N_SETTING_GRAPHIC_HIGH, \ 27 | I18N_SETTING_SOUND_DISABLED, I18N_SETTING_SOUND_QUIET, I18N_SETTING_SOUND_NORMAL, I18N_SETTING_SCREEN_MODE_FULL, \ 28 | I18N_SETTING_SCREEN_MODE_WINDOW, I18N_SETTING_LANGUAGE_RU, I18N_SETTING_LANGUAGE_EN, I18N_BUTTON_TO_MENU_ROOT 29 | from .entities import Background, ButtonAbout, ButtonExit, ButtonGraphicHigh, ButtonGraphicLow, ButtonGraphicMiddle, \ 30 | ButtonGuide, ButtonPlay, ButtonRecords, ButtonScreenModeFull, ButtonScreenModeWindow, ButtonSettings, \ 31 | ButtonSoundDisable, ButtonSoundNormal, ButtonSoundQuiet, ButtonToMenuRoot, MenuData, Shine, \ 32 | ShineLightAnimationSet, TextAbout, TextRecords, TextGuide, TextSettings, LabelGameName, ButtonLanguageRu, \ 33 | ButtonLanguageEn 34 | from .surfaces import surface_background, surface_shine_animation_set, surface_text_about, surface_text_records, \ 35 | surface_text_guide, surface_label_game_name, surface_text_settings 36 | 37 | 38 | def on_click_to_menu_root(entities: EntityManager, pointer_pos: Vector2): # noqa 39 | SOUND_BUTTON_CLICK.play() 40 | next(entities.get_by_class(MenuData)).scene_active = MENU_SCENE_ROOT 41 | 42 | 43 | def on_click_about(entities: EntityManager, pointer_pos: Vector2): # noqa 44 | SOUND_BUTTON_CLICK.play() 45 | next(entities.get_by_class(MenuData)).scene_active = MENU_SCENE_ABOUT 46 | 47 | 48 | def on_click_exit(entities: EntityManager, pointer_pos: Vector2): # noqa 49 | exit() 50 | 51 | 52 | def on_click_guide(entities: EntityManager, pointer_pos: Vector2): # noqa 53 | SOUND_BUTTON_CLICK.play() 54 | next(entities.get_by_class(MenuData)).scene_active = MENU_SCENE_GUIDE 55 | 56 | 57 | def on_click_play(entities: EntityManager, pointer_pos: Vector2): # noqa 58 | SOUND_START.play() 59 | next(entities.get_by_class(MenuData)).do_menu = False 60 | 61 | 62 | def on_click_records(entities: EntityManager, pointer_pos: Vector2): # noqa 63 | SOUND_BUTTON_CLICK.play() 64 | next(entities.get_by_class(MenuData)).scene_active = MENU_SCENE_RECORDS 65 | 66 | 67 | def on_click_settings(entities: EntityManager, pointer_pos: Vector2): # noqa 68 | SOUND_BUTTON_CLICK.play() 69 | next(entities.get_by_class(MenuData)).scene_active = MENU_SCENE_SETTINGS 70 | 71 | 72 | def on_click_graphic_high(entities: EntityManager, pointer_pos: Vector2): # noqa 73 | SOUND_BUTTON_CLICK.play() 74 | SETTINGS_STORAGE.graphic = SETTING_GRAPHIC_HIGH 75 | next(entities.get_by_class(TextSettings)).sf_text = surface_text_settings() 76 | 77 | 78 | def on_click_graphic_low(entities: EntityManager, pointer_pos: Vector2): # noqa 79 | if SETTINGS_STORAGE.screen_mode == SETTING_SCREEN_MODE_FULL: 80 | SOUND_BUTTON_CLICK.play() 81 | SETTINGS_STORAGE.graphic = SETTING_GRAPHIC_LOW 82 | next(entities.get_by_class(TextSettings)).sf_text = surface_text_settings() 83 | else: 84 | SOUND_DENY.play() 85 | 86 | 87 | def on_click_graphic_middle(entities: EntityManager, pointer_pos: Vector2): # noqa 88 | SOUND_BUTTON_CLICK.play() 89 | SETTINGS_STORAGE.graphic = SETTING_GRAPHIC_MIDDLE 90 | next(entities.get_by_class(TextSettings)).sf_text = surface_text_settings() 91 | 92 | 93 | def on_click_screen_mode_full(entities: EntityManager, pointer_pos: Vector2): # noqa 94 | SOUND_BUTTON_CLICK.play() 95 | SETTINGS_STORAGE.screen_mode = SETTING_SCREEN_MODE_FULL 96 | next(entities.get_by_class(TextSettings)).sf_text = surface_text_settings() 97 | 98 | 99 | def on_click_screen_mode_window(entities: EntityManager, pointer_pos: Vector2): # noqa 100 | if SETTINGS_STORAGE.is_android: 101 | SOUND_DENY.play() 102 | else: 103 | SOUND_BUTTON_CLICK.play() 104 | SETTINGS_STORAGE.screen_mode = SETTING_SCREEN_MODE_WINDOW 105 | next(entities.get_by_class(TextSettings)).sf_text = surface_text_settings() 106 | 107 | 108 | def on_click_sound_disable(entities: EntityManager, pointer_pos: Vector2): # noqa 109 | SETTINGS_STORAGE.sound = SETTING_SOUND_DISABLED 110 | next(entities.get_by_class(TextSettings)).sf_text = surface_text_settings() 111 | set_sound_volume(SETTING_SOUND_DISABLED) 112 | SOUND_BUTTON_CLICK.play() 113 | 114 | 115 | def on_click_sound_normal(entities: EntityManager, pointer_pos: Vector2): # noqa 116 | SETTINGS_STORAGE.sound = SETTING_SOUND_NORMAL 117 | next(entities.get_by_class(TextSettings)).sf_text = surface_text_settings() 118 | set_sound_volume(SETTING_SOUND_NORMAL) 119 | SOUND_BUTTON_CLICK.play() 120 | 121 | 122 | def on_click_sound_quiet(entities: EntityManager, pointer_pos: Vector2): # noqa 123 | SETTINGS_STORAGE.sound = SETTING_SOUND_QUIET 124 | next(entities.get_by_class(TextSettings)).sf_text = surface_text_settings() 125 | set_sound_volume(SETTING_SOUND_QUIET) 126 | SOUND_BUTTON_CLICK.play() 127 | 128 | 129 | def on_click_language_ru(entities: EntityManager, pointer_pos: Vector2): # noqa 130 | SOUND_BUTTON_CLICK.play() 131 | SETTINGS_STORAGE.language = SETTING_LANGUAGE_RU 132 | next(entities.get_by_class(TextSettings)).sf_text = surface_text_settings() 133 | 134 | 135 | def on_click_language_en(entities: EntityManager, pointer_pos: Vector2): # noqa 136 | SOUND_BUTTON_CLICK.play() 137 | SETTINGS_STORAGE.language = SETTING_LANGUAGE_EN 138 | next(entities.get_by_class(TextSettings)).sf_text = surface_text_settings() 139 | 140 | 141 | def _gui_button_root(icon: Surface, bg: Surface, root_num: int) -> dict: 142 | pass # todo CUT 143 | return dict( 144 | rect=Rect(x + x_corr, y + y_corr, w, h), 145 | sf_static=sf_static, 146 | sf_hover=sf_hover, 147 | sf_pressed=sf_pressed, 148 | mask=pygame.mask.from_surface(sf_static), 149 | state=BS_STATIC, 150 | ) 151 | 152 | 153 | class SysInit(System): 154 | 155 | def __init__(self, entities: EntityManager): 156 | self.entities = entities 157 | self.music_channel = None 158 | 159 | def start(self): 160 | set_sound_volume(SETTINGS_STORAGE.sound) 161 | 162 | _surface_text_about = surface_text_about() 163 | _surface_text_records = surface_text_records() 164 | _surface_text_guide = surface_text_guide() 165 | _surface_text_settings = surface_text_settings() 166 | _surface_label_game_name = surface_label_game_name() 167 | 168 | _tw = SCREEN_WIDTH * TEXT_ML_WIDTH 169 | _th = SCREEN_HEIGHT * TEXT_ML_HEIGHT 170 | _xpad = (SCREEN_WIDTH - _tw) / 2 171 | common_text_ml_rect = ( # (left, top, width, height) 172 | _xpad, 173 | _xpad, 174 | _tw, 175 | _th, 176 | ) 177 | 178 | _height_center_px_game_name = SCREEN_HEIGHT * MENU_ROOT_AREA_GAME_NAME_HEIGHT / 2 179 | _height_center_px_main_buttons = SCREEN_HEIGHT - SCREEN_HEIGHT * MENU_ROOT_AREA_BUTTONS_HEIGHT / 2 180 | 181 | _btn_x_gap = SCREEN_WIDTH * 0.02 182 | _btn_w = SCREEN_WIDTH * BUTTON_WIDTH 183 | _btn_pad = SCREEN_WIDTH * 0.04 184 | 185 | _text_linesize = FONT_TEXT_ML.get_linesize() 186 | _text_y_pad = (SCREEN_HEIGHT - SCREEN_HEIGHT * TEXT_ML_HEIGHT) / 2 - FONT_TEXT_ML.get_linesize() * 0.2 187 | 188 | self.entities.add( 189 | MenuData( 190 | do_menu=True, 191 | scene_active=MENU_SCENE_ROOT, 192 | music_channel=SOUND_MENU.play(-1), 193 | ), 194 | Background( 195 | surface_background(), 196 | x=0, 197 | y=0, 198 | ), 199 | LabelGameName( 200 | _surface_label_game_name, 201 | x=SCREEN_WIDTH / 2 - _surface_label_game_name.get_width() / 2, 202 | y=_height_center_px_game_name - _surface_label_game_name.get_height() / 2, 203 | ), 204 | ButtonToMenuRoot( 205 | scenes=[MENU_SCENE_ABOUT, MENU_SCENE_GUIDE, MENU_SCENE_RECORDS, MENU_SCENE_SETTINGS], 206 | on_click=on_click_to_menu_root, 207 | **gui_button_attrs( 208 | SCREEN_WIDTH * 0.25, SCREEN_HEIGHT * 0.85, 209 | I18N_BUTTON_TO_MENU_ROOT, 1.61), 210 | ), 211 | 212 | # root 213 | ButtonAbout( 214 | scenes=[MENU_SCENE_ROOT], 215 | on_click=on_click_about, 216 | **_gui_button_root(IMG_ICON_ABOUT, IMG_BUTTON_ROOT_3, 3), 217 | ), 218 | ButtonExit( 219 | scenes=[MENU_SCENE_ROOT], 220 | on_click=on_click_exit, 221 | **_gui_button_root(IMG_ICON_EXIT, IMG_BUTTON_ROOT_4, 4), 222 | ), 223 | ButtonGuide( 224 | scenes=[MENU_SCENE_ROOT], 225 | on_click=on_click_guide, 226 | **_gui_button_root(IMG_ICON_GUIDE, IMG_BUTTON_ROOT_1, 1), 227 | ), 228 | ButtonPlay( 229 | scenes=[MENU_SCENE_ROOT], 230 | on_click=on_click_play, 231 | **_gui_button_root(IMG_ICON_PLAY, IMG_BUTTON_ROOT_5, 5), 232 | ), 233 | ButtonRecords( 234 | scenes=[MENU_SCENE_ROOT], 235 | on_click=on_click_records, 236 | **_gui_button_root(IMG_ICON_RECORDS, IMG_BUTTON_ROOT_2, 2), 237 | ), 238 | ButtonSettings( 239 | scenes=[MENU_SCENE_ROOT], 240 | on_click=on_click_settings, 241 | **_gui_button_root(IMG_ICON_SETTINGS, IMG_BUTTON_ROOT_6, 6), 242 | ), 243 | 244 | # graphic 245 | ButtonGraphicLow( 246 | scenes=[MENU_SCENE_SETTINGS], 247 | on_click=on_click_graphic_low, 248 | **gui_button_attrs( 249 | _btn_pad + _btn_x_gap, _text_y_pad + _text_linesize * 3, 250 | I18N_SETTING_GRAPHIC_LOW), 251 | ), 252 | ButtonGraphicMiddle( 253 | scenes=[MENU_SCENE_SETTINGS], 254 | on_click=on_click_graphic_middle, 255 | **gui_button_attrs( 256 | _btn_pad + _btn_x_gap * 2 + _btn_w, _text_y_pad + _text_linesize * 3, 257 | I18N_SETTING_GRAPHIC_MIDDLE), 258 | ), 259 | ButtonGraphicHigh( 260 | scenes=[MENU_SCENE_SETTINGS], 261 | on_click=on_click_graphic_high, 262 | **gui_button_attrs( 263 | _btn_pad + _btn_x_gap * 3 + _btn_w * 2, _text_y_pad + _text_linesize * 3, 264 | I18N_SETTING_GRAPHIC_HIGH), 265 | ), 266 | 267 | # screen mode 268 | ButtonScreenModeFull( 269 | scenes=[MENU_SCENE_SETTINGS], 270 | on_click=on_click_screen_mode_full, 271 | **gui_button_attrs( 272 | _btn_pad + _btn_x_gap, _text_y_pad + _text_linesize * 8, 273 | I18N_SETTING_SCREEN_MODE_FULL), 274 | ), 275 | ButtonScreenModeWindow( 276 | scenes=[MENU_SCENE_SETTINGS], 277 | on_click=on_click_screen_mode_window, 278 | **gui_button_attrs( 279 | _btn_pad + _btn_x_gap * 2 + _btn_w, _text_y_pad + _text_linesize * 8, 280 | I18N_SETTING_SCREEN_MODE_WINDOW), 281 | ), 282 | 283 | # sound 284 | ButtonSoundDisable( 285 | scenes=[MENU_SCENE_SETTINGS], 286 | on_click=on_click_sound_disable, 287 | **gui_button_attrs( 288 | _btn_pad + _btn_x_gap, _text_y_pad + _text_linesize * 13, 289 | I18N_SETTING_SOUND_DISABLED), 290 | ), 291 | ButtonSoundQuiet( 292 | scenes=[MENU_SCENE_SETTINGS], 293 | on_click=on_click_sound_quiet, 294 | **gui_button_attrs( 295 | _btn_pad + _btn_x_gap * 2 + _btn_w, _text_y_pad + _text_linesize * 13, 296 | I18N_SETTING_SOUND_QUIET), 297 | ), 298 | ButtonSoundNormal( 299 | scenes=[MENU_SCENE_SETTINGS], 300 | on_click=on_click_sound_normal, 301 | **gui_button_attrs( 302 | _btn_pad + _btn_x_gap * 3 + _btn_w * 2, _text_y_pad + _text_linesize * 13, 303 | I18N_SETTING_SOUND_NORMAL), 304 | ), 305 | 306 | # language 307 | ButtonLanguageRu( 308 | scenes=[MENU_SCENE_SETTINGS], 309 | on_click=on_click_language_ru, 310 | **gui_button_attrs( 311 | _btn_pad + _btn_x_gap, _text_y_pad + _text_linesize * 18, 312 | I18N_SETTING_LANGUAGE_RU), 313 | ), 314 | ButtonLanguageEn( 315 | scenes=[MENU_SCENE_SETTINGS], 316 | on_click=on_click_language_en, 317 | **gui_button_attrs( 318 | _btn_pad + _btn_x_gap * 2 + _btn_w, _text_y_pad + _text_linesize * 18, 319 | I18N_SETTING_LANGUAGE_EN), 320 | ), 321 | 322 | # shine 323 | Shine( 324 | x=SCREEN_WIDTH / 2 - SCREEN_WIDTH * MENU_SHINE_WIDTH / 2, 325 | y=_height_center_px_main_buttons - SCREEN_WIDTH * MENU_SHINE_WIDTH / 2, 326 | animation_set=ShineLightAnimationSet(surface_shine_animation_set('#FFFF00')), 327 | animation_looped=True, 328 | animation_frame=0, 329 | animation_frame_float=0.0, 330 | animation_speed=10, 331 | ), 332 | 333 | # texts 334 | TextAbout( 335 | scenes=[MENU_SCENE_ABOUT], 336 | rect=common_text_ml_rect, 337 | sf_text=_surface_text_about, 338 | sf_bg=colored_block_surface('#FFE4B599', *_surface_text_about.get_size()), 339 | ), 340 | TextRecords( 341 | scenes=[MENU_SCENE_RECORDS], 342 | rect=common_text_ml_rect, 343 | sf_text=_surface_text_records, 344 | sf_bg=colored_block_surface('#FFE4B599', *_surface_text_records.get_size()), 345 | ), 346 | TextGuide( 347 | scenes=[MENU_SCENE_GUIDE], 348 | rect=common_text_ml_rect, 349 | sf_text=_surface_text_guide, 350 | sf_bg=colored_block_surface('#FFE4B599', *_surface_text_guide.get_size()), 351 | ), 352 | TextSettings( 353 | scenes=[MENU_SCENE_SETTINGS], 354 | rect=common_text_ml_rect, 355 | sf_text=_surface_text_settings, 356 | sf_bg=colored_block_surface('#FFE4B599', *_surface_text_settings.get_size()), 357 | ), 358 | ) 359 | 360 | def stop(self): 361 | next(self.entities.get_by_class(MenuData)).music_channel.stop() 362 | 363 | 364 | class SysLive(System): 365 | 366 | def __init__(self, entities: EntityManager, clock: pygame.time.Clock): 367 | self.entities = entities 368 | self.clock = clock 369 | self.md = None 370 | 371 | def start(self): 372 | self.md = next(self.entities.get_by_class(MenuData)) 373 | 374 | def update(self): 375 | now_fps = self.clock.get_fps() or FPS_MAX 376 | 377 | # движение 378 | for speed_obj in self.entities.get_with_component(ComSpeed): 379 | speed_obj.x += speed_obj.speed_x / now_fps 380 | speed_obj.y += speed_obj.speed_y / now_fps 381 | 382 | # анимация 383 | for ani_obj in self.entities.get_with_component(ComAnimated): 384 | ani_obj.animation_frame_float -= ani_obj.animation_speed / now_fps 385 | ani_obj.animation_frame = ani_obj.animation_frame_float.__trunc__() # быстрее int() 386 | if ani_obj.animation_frame_float < 0: 387 | if ani_obj.animation_looped: 388 | ani_obj.animation_frame = len(ani_obj.animation_set.frames) - 1 389 | ani_obj.animation_frame_float = float(ani_obj.animation_frame) 390 | else: 391 | self.entities.delete_buffer_add(ani_obj) 392 | 393 | self.entities.delete_buffer_purge() 394 | 395 | 396 | class SysControl(System): 397 | 398 | def __init__(self, entities: EntityManager): 399 | self.entities = entities 400 | self.event_getter: Callable[..., list[Event]] = pygame.event.get 401 | self.md = None 402 | self.mouse_event_set = (MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION) 403 | 404 | def start(self): 405 | self.md = next(self.entities.get_by_class(MenuData)) 406 | 407 | def update(self): 408 | for event in self.event_getter(): 409 | event_type = event.type 410 | event_key = getattr(event, 'key', None) 411 | 412 | # закрыть 413 | if event_type == QUIT: 414 | exit() 415 | 416 | # в корень или выйти из игры 417 | if event_type == KEYDOWN and event_key in (K_ESCAPE, K_AC_BACK): 418 | if self.md.scene_active == MENU_SCENE_ROOT: 419 | exit() 420 | else: 421 | self.md.scene_active = MENU_SCENE_ROOT 422 | 423 | # gui 424 | control_button(event, event_type, self.md.scene_active, self.entities) 425 | 426 | 427 | class SysDraw(System): 428 | 429 | def __init__(self, entities: EntityManager, display: Surface): 430 | self.entities = entities 431 | self.display = display 432 | self.md: Optional[MenuData] = None 433 | self.background = None 434 | self.label_game_name = None 435 | 436 | def start(self): 437 | self.md = next(self.entities.get_by_class(MenuData)) 438 | self.background = next(self.entities.get_by_class(Background)) 439 | self.label_game_name = next(self.entities.get_by_class(LabelGameName)) 440 | 441 | def update(self): 442 | # фон 443 | self.display.blit(self.background.surface, (self.background.x, self.background.y)) 444 | if self.md.scene_active == MENU_SCENE_ROOT: 445 | self.display.blit(self.label_game_name.surface, (self.label_game_name.x, self.label_game_name.y)) 446 | 447 | # сияние 448 | if self.md.scene_active == MENU_SCENE_ROOT: 449 | for shine in self.entities.get_by_class(Shine): 450 | self.display.blit(shine.animation_set.frames[shine.animation_frame], (shine.x, shine.y)) 451 | 452 | # gui 453 | draw_text_ml(self.display, self.md.scene_active, self.entities) 454 | draw_button(self.display, self.md.scene_active, self.entities) 455 | -------------------------------------------------------------------------------- /examples/trig/requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | pygame==2.5.0 # making multimedia apps like games built on top of the SDL library. C, Python, Native, OpenGL 3 | numpy==1.25.2 # fundamental package for scientific computing with Python 4 | ecs-pattern==1.1.2 # Implementation of the ECS pattern for creating games. 5 | pyjnius==1.5.0 # Access Java classes from Python 6 | 7 | pip==23.2.1 8 | setuptools==68.0.0 9 | wheel==0.40.0 10 | flake8==6.1.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import setuptools 4 | 5 | 6 | def get_version(package: str) -> str: 7 | """Return package version as listed in __version__ variable at __init__.py""" 8 | init_py = open(os.path.join(package, '__init__.py')).read() 9 | return re.search(r"__version__\s*=\s*['\"]([^'\"]+)['\"]", init_py).group(1) 10 | 11 | 12 | with open("README.rst", "r", encoding='utf-8') as fh: 13 | long_description = fh.read() 14 | 15 | setuptools.setup( 16 | name='ecs-pattern', 17 | version=get_version('ecs_pattern'), 18 | packages=setuptools.find_packages(exclude=['tests']), 19 | url='https://github.com/ikvk/ecs_pattern', 20 | license='Apache-2.0', 21 | license_files="LICENSE", 22 | long_description=long_description, 23 | long_description_content_type="text/x-rst", 24 | author='Vladimir Kaukin', 25 | author_email='KaukinVK@ya.ru', 26 | description='Implementation of the ECS pattern for creating games', 27 | keywords=['python3', 'python', 'ecs', 'pattern', 'architecture', 'games', 'gamedev'], 28 | classifiers=[ 29 | "Programming Language :: Python :: 3", 30 | "License :: OSI Approved :: Apache Software License", 31 | "Operating System :: OS Independent", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikvk/ecs_pattern/2b50ba1597bb06a3382f078eaab049e9c7170317/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_ecs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ecs_pattern import component, entity, EntityManager, System, SystemManager 4 | 5 | 6 | @component 7 | class ComPosition: 8 | x: int = 0 9 | y: int = 0 10 | 11 | 12 | @component 13 | class ComPerson: 14 | name: str 15 | health: int 16 | 17 | 18 | @entity 19 | class Player(ComPosition, ComPerson): 20 | pass 21 | 22 | 23 | @entity 24 | class Ball(ComPosition): 25 | pass 26 | 27 | 28 | class SysGravitation(System): 29 | def __init__(self, entities: EntityManager): 30 | self.entities = entities 31 | self._gravitation_enabled = False 32 | 33 | def start(self): 34 | self._gravitation_enabled = True 35 | 36 | def update(self): 37 | if not self._gravitation_enabled: 38 | return 39 | for entity_with_pos in self.entities.get_with_component(ComPosition): 40 | if entity_with_pos.y > 0: 41 | entity_with_pos.y -= 1 42 | 43 | def stop(self): 44 | self._gravitation_enabled = False 45 | 46 | 47 | class SysPersonHealthRegeneration(System): 48 | def __init__(self, entities: EntityManager): 49 | self.entities = entities 50 | self._regeneration_enabled = False 51 | 52 | def start(self): 53 | self._regeneration_enabled = True 54 | 55 | def update(self): 56 | if not self._regeneration_enabled: 57 | return 58 | for entity_with_health in self.entities.get_with_component(ComPerson): 59 | entity_with_health: ComPerson 60 | if entity_with_health.health < 100: 61 | entity_with_health.health += 1 62 | 63 | def stop(self): 64 | self._regeneration_enabled = False 65 | 66 | 67 | class EcsTest(unittest.TestCase): 68 | def test_component(self): 69 | position = ComPosition(1, 2) 70 | self.assertEqual((position.x, position.y), (1, 2)) 71 | person = ComPerson('Ivan', 100) 72 | self.assertEqual(person.name, 'Ivan') 73 | self.assertEqual(person.health, 100) 74 | with self.assertRaises(TypeError): 75 | ComPerson() 76 | 77 | def test_entity(self): 78 | player = Player('Vladimir', 33, 3, 4) 79 | self.assertEqual((player.name, player.health, player.x, player.y), ('Vladimir', 33, 3, 4)) 80 | ball = Ball(13, 14) 81 | self.assertEqual((ball.x, ball.y), (13, 14)) 82 | with self.assertRaises(TypeError): 83 | Player() 84 | with self.assertRaises(TypeError): 85 | @entity 86 | class PlayerWrongOrderSuperClass(ComPerson, ComPosition): # noqa 87 | pass 88 | 89 | def test_entitymanager(self): 90 | player1 = Player('Ivan', 20, 1, 2) 91 | player2 = Player('Vladimir', 30, 3, 4) 92 | ball = Ball(13, 24) 93 | entities = EntityManager() 94 | 95 | with self.assertRaises(KeyError): 96 | next(entities.get_by_class(Ball)) 97 | entities.init(Ball(1, 1)) 98 | self.assertEqual(next(entities.get_by_class(Ball), None), None) 99 | entities.delete(*tuple(entities.get_by_class(Ball))) # no balls, no raise 100 | 101 | entities.add(player1, player2, ball) 102 | self.assertEqual(len(list(entities.get_by_class(Player))), 2) 103 | self.assertEqual(len(list(entities.get_by_class(Ball))), 1) 104 | self.assertEqual(len(list(entities.get_by_class(Ball, Player))), 3) 105 | self.assertEqual(list(entities.get_by_class(Ball, Player)), [ball, player1, player2]) 106 | self.assertEqual(len(list(entities.get_with_component(ComPosition))), 3) 107 | self.assertEqual(len(list(entities.get_with_component(ComPerson))), 2) 108 | self.assertEqual(len(list(entities.get_with_component(ComPerson, ComPosition))), 2) # *and 109 | self.assertEqual(len(list(entities.get_with_component(ComPerson, ComPerson, ComPerson))), 2) # *not uniq coms 110 | 111 | entities.delete(player1, player2) 112 | self.assertEqual(len(list(entities.get_by_class(Player))), 0) 113 | self.assertEqual(len(list(entities.get_by_class(Ball))), 1) 114 | self.assertEqual(len(list(entities.get_with_component(ComPosition))), 1) 115 | self.assertEqual(len(list(entities.get_with_component(ComPerson))), 0) 116 | self.assertEqual(len(list(entities.get_with_component(ComPerson, ComPosition))), 0) 117 | 118 | entities.delete(ball) 119 | self.assertEqual(len(list(entities.get_by_class(Player))), 0) 120 | self.assertEqual(len(list(entities.get_by_class(Ball))), 0) 121 | self.assertEqual(len(list(entities.get_with_component(ComPosition))), 0) 122 | self.assertEqual(len(list(entities.get_with_component(ComPerson))), 0) 123 | 124 | # mark to del 125 | entities.add(player1, player2) 126 | self.assertEqual(len(list(entities.get_by_class(Player))), 2) 127 | entities.delete_buffer_purge() 128 | self.assertEqual(len(list(entities.get_by_class(Player))), 2) 129 | entities.delete_buffer_add(player1, player2) 130 | entities.delete_buffer_purge() 131 | self.assertEqual(len(list(entities.get_by_class(Player))), 0) 132 | 133 | # mark to del twice 134 | entities.add(player1, player2) 135 | self.assertEqual(len(list(entities.get_by_class(Player))), 2) 136 | entities.delete_buffer_purge() 137 | self.assertEqual(len(list(entities.get_by_class(Player))), 2) 138 | entities.delete_buffer_add(player1, player2) 139 | entities.delete_buffer_add(player1, player2) 140 | entities.delete_buffer_purge() 141 | self.assertEqual(len(list(entities.get_by_class(Player))), 0) 142 | 143 | def test_system(self): 144 | player1 = Player('Ivan', 20, 1, 2) 145 | player2 = Player('Vladimir', 30, 3, 4) 146 | ball = Ball(13, 24) 147 | entities = EntityManager() 148 | entities.add(player1, player2, ball) 149 | sys_regen = SysPersonHealthRegeneration(entities) 150 | i: ComPerson 151 | 152 | sys_regen.update() 153 | self.assertEqual(set(i.health for i in entities.get_with_component(ComPerson)), {20, 30}) 154 | 155 | sys_regen.start() 156 | sys_regen.update() 157 | self.assertEqual(set(i.health for i in entities.get_with_component(ComPerson)), {21, 31}) 158 | sys_regen.update() 159 | sys_regen.update() 160 | self.assertEqual(set(i.health for i in entities.get_with_component(ComPerson)), {23, 33}) 161 | 162 | sys_regen.stop() 163 | sys_regen.update() 164 | self.assertEqual(set(i.health for i in entities.get_with_component(ComPerson)), {23, 33}) 165 | 166 | def test_system_manager(self): 167 | player1 = Player('Ivan', 20, 1, 2) 168 | player2 = Player('Vladimir', 30, 3, 4) 169 | ball = Ball(0, 7) 170 | entities = EntityManager() 171 | entities.add(player1, player2, ball) 172 | sys_regen = SysPersonHealthRegeneration(entities) 173 | sys_grav = SysGravitation(entities) 174 | system_manager = SystemManager([sys_regen, sys_grav]) 175 | per: ComPerson 176 | pos: ComPosition 177 | 178 | self.assertEqual(sys_regen._regeneration_enabled, False) 179 | self.assertEqual(sys_grav._gravitation_enabled, False) 180 | 181 | system_manager.start_systems() 182 | self.assertEqual(sys_regen._regeneration_enabled, True) 183 | self.assertEqual(sys_grav._gravitation_enabled, True) 184 | 185 | system_manager.update_systems() 186 | self.assertEqual(set(per.health for per in entities.get_with_component(ComPerson)), {21, 31}) 187 | self.assertEqual(set(pos.y for pos in entities.get_with_component(ComPosition)), {1, 3, 6}) 188 | system_manager.update_systems() 189 | system_manager.update_systems() 190 | self.assertEqual(set(per.health for per in entities.get_with_component(ComPerson)), {23, 33}) 191 | self.assertEqual(set(pos.y for pos in entities.get_with_component(ComPosition)), {0, 1, 4}) 192 | 193 | system_manager.stop_systems() 194 | self.assertEqual(sys_regen._regeneration_enabled, False) 195 | self.assertEqual(sys_grav._gravitation_enabled, False) 196 | system_manager.update_systems() 197 | system_manager.update_systems() 198 | self.assertEqual(set(per.health for per in entities.get_with_component(ComPerson)), {23, 33}) 199 | self.assertEqual(set(pos.y for pos in entities.get_with_component(ComPosition)), {0, 1, 4}) 200 | 201 | 202 | if __name__ == "__main__": 203 | unittest.main() 204 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py3.3,py3.4,py3.5,py3.6,py3.7,py3.8,py3.9,py3.10,py3.11,py3.12,py3.13 4 | 5 | [testenv] 6 | commands= 7 | python -m unittest discover -s {toxinidir}/tests -t {toxinidir}/tests 8 | ; python -m unittest tests.test_query -v 9 | 10 | ; dataclasses 3.7 11 | ; typing 3.5 12 | --------------------------------------------------------------------------------