├── .gitignore ├── assets ├── 1-1.1-1.png ├── 2-2.1-1.png ├── 2-2.1-2.png ├── 2-2.1-3.png ├── 2-2.1-4.png ├── 2-3-1.1.png ├── 2-3-1.2.png ├── 2-3-1.3.png ├── 2-3-1.4.png ├── 2-3.1-1.png ├── 2-3.1-2.png ├── 2-3.1-3.png ├── 2-4-1.1.png ├── 2-4-1.2.png ├── 2-4-1.3.png ├── 2-4-1.4.png ├── 2-4-1.5.png ├── 2-4-1.6.png ├── 2-6-1.1.png ├── 2-6-1.2.png ├── 3-1-1.1.png ├── 3-1-1.2.png ├── 3-1-1.3.png ├── 3-1-1.4.png ├── 3-2-1.1.png ├── 3-2-1.2.png ├── 3-2-1.3.png ├── 3-2-1.4.png ├── 3-3-1.1.png ├── 3-3-1.2.png ├── 4-1-1.1.png ├── 4-1-1.2.png ├── 4-1-1.3.png ├── 4-1-1.4.png ├── 4-1-1.5.png ├── 4-1-1.6.png ├── 4-1-1.7.png ├── 4-1-1.8.png ├── 4-3-1.1.png ├── 4-3-1.2.png ├── 4-3-1.3.png ├── 4-3-1.4.png ├── 5-1-1.1.png ├── 5-2-1.1.png ├── 5-2-1.2.png ├── 5-2-1.3.png ├── 5-2-1.4.png ├── 5-2-1.5.png ├── 6-1-1.1.png ├── 6-1-1.2.png ├── 6-1-1.3.png ├── 6-1-1.4.png ├── 6-1-1.5.png ├── 6-2-1.1.png ├── 6-2-1.2.png ├── 6-2-1.3.png ├── 6-2-1.4.png ├── 6-2-1.5.png ├── 6-3-1.1.png ├── 6-4-1.1.png ├── 6-4-1.2.png ├── 6-4-1.3.png ├── 6-4-1.4.png ├── 6-4-1.5.png ├── 6-4-1.6.png ├── cover.jpg └── gpp-logo.png ├── shabloni_igrovogo_programmirovaniya.md ├── README.md ├── posledovatelnie-shabloni-sequencing-patterns.md ├── povedencheskie-shabloni-behavioral-patterns.md ├── obzor-shablonov-proektirovaniya.md ├── shabloni-snizheniya-svyaznosti-decoupling-patterns.md ├── shabloni-optimizatsii.md ├── SUMMARY.md ├── predislovie.md ├── obzor-shablonov-proektirovaniya ├── prisposoblenets-flyweight.md ├── komanda-command.md └── prototip-prototype.md ├── predislovie └── arhitektura-proizvoditelnost-i-igri.md ├── povedencheskie-shabloni-behavioral-patterns └── podklass-pesochnitsa-subclass-sandbox.md ├── shabloni-optimizatsii └── pul-obektov-object-pool.md └── posledovatelnie-shabloni-sequencing-patterns └── metodi-obnovleniya-update-methods.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | .DS_Store 3 | .realsync 4 | .vagrant 5 | /.idea/* -------------------------------------------------------------------------------- /assets/1-1.1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/1-1.1-1.png -------------------------------------------------------------------------------- /assets/2-2.1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-2.1-1.png -------------------------------------------------------------------------------- /assets/2-2.1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-2.1-2.png -------------------------------------------------------------------------------- /assets/2-2.1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-2.1-3.png -------------------------------------------------------------------------------- /assets/2-2.1-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-2.1-4.png -------------------------------------------------------------------------------- /assets/2-3-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-3-1.1.png -------------------------------------------------------------------------------- /assets/2-3-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-3-1.2.png -------------------------------------------------------------------------------- /assets/2-3-1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-3-1.3.png -------------------------------------------------------------------------------- /assets/2-3-1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-3-1.4.png -------------------------------------------------------------------------------- /assets/2-3.1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-3.1-1.png -------------------------------------------------------------------------------- /assets/2-3.1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-3.1-2.png -------------------------------------------------------------------------------- /assets/2-3.1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-3.1-3.png -------------------------------------------------------------------------------- /assets/2-4-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-4-1.1.png -------------------------------------------------------------------------------- /assets/2-4-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-4-1.2.png -------------------------------------------------------------------------------- /assets/2-4-1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-4-1.3.png -------------------------------------------------------------------------------- /assets/2-4-1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-4-1.4.png -------------------------------------------------------------------------------- /assets/2-4-1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-4-1.5.png -------------------------------------------------------------------------------- /assets/2-4-1.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-4-1.6.png -------------------------------------------------------------------------------- /assets/2-6-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-6-1.1.png -------------------------------------------------------------------------------- /assets/2-6-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/2-6-1.2.png -------------------------------------------------------------------------------- /assets/3-1-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/3-1-1.1.png -------------------------------------------------------------------------------- /assets/3-1-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/3-1-1.2.png -------------------------------------------------------------------------------- /assets/3-1-1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/3-1-1.3.png -------------------------------------------------------------------------------- /assets/3-1-1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/3-1-1.4.png -------------------------------------------------------------------------------- /assets/3-2-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/3-2-1.1.png -------------------------------------------------------------------------------- /assets/3-2-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/3-2-1.2.png -------------------------------------------------------------------------------- /assets/3-2-1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/3-2-1.3.png -------------------------------------------------------------------------------- /assets/3-2-1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/3-2-1.4.png -------------------------------------------------------------------------------- /assets/3-3-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/3-3-1.1.png -------------------------------------------------------------------------------- /assets/3-3-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/3-3-1.2.png -------------------------------------------------------------------------------- /assets/4-1-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-1-1.1.png -------------------------------------------------------------------------------- /assets/4-1-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-1-1.2.png -------------------------------------------------------------------------------- /assets/4-1-1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-1-1.3.png -------------------------------------------------------------------------------- /assets/4-1-1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-1-1.4.png -------------------------------------------------------------------------------- /assets/4-1-1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-1-1.5.png -------------------------------------------------------------------------------- /assets/4-1-1.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-1-1.6.png -------------------------------------------------------------------------------- /assets/4-1-1.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-1-1.7.png -------------------------------------------------------------------------------- /assets/4-1-1.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-1-1.8.png -------------------------------------------------------------------------------- /assets/4-3-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-3-1.1.png -------------------------------------------------------------------------------- /assets/4-3-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-3-1.2.png -------------------------------------------------------------------------------- /assets/4-3-1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-3-1.3.png -------------------------------------------------------------------------------- /assets/4-3-1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/4-3-1.4.png -------------------------------------------------------------------------------- /assets/5-1-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/5-1-1.1.png -------------------------------------------------------------------------------- /assets/5-2-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/5-2-1.1.png -------------------------------------------------------------------------------- /assets/5-2-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/5-2-1.2.png -------------------------------------------------------------------------------- /assets/5-2-1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/5-2-1.3.png -------------------------------------------------------------------------------- /assets/5-2-1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/5-2-1.4.png -------------------------------------------------------------------------------- /assets/5-2-1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/5-2-1.5.png -------------------------------------------------------------------------------- /assets/6-1-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-1-1.1.png -------------------------------------------------------------------------------- /assets/6-1-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-1-1.2.png -------------------------------------------------------------------------------- /assets/6-1-1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-1-1.3.png -------------------------------------------------------------------------------- /assets/6-1-1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-1-1.4.png -------------------------------------------------------------------------------- /assets/6-1-1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-1-1.5.png -------------------------------------------------------------------------------- /assets/6-2-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-2-1.1.png -------------------------------------------------------------------------------- /assets/6-2-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-2-1.2.png -------------------------------------------------------------------------------- /assets/6-2-1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-2-1.3.png -------------------------------------------------------------------------------- /assets/6-2-1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-2-1.4.png -------------------------------------------------------------------------------- /assets/6-2-1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-2-1.5.png -------------------------------------------------------------------------------- /assets/6-3-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-3-1.1.png -------------------------------------------------------------------------------- /assets/6-4-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-4-1.1.png -------------------------------------------------------------------------------- /assets/6-4-1.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-4-1.2.png -------------------------------------------------------------------------------- /assets/6-4-1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-4-1.3.png -------------------------------------------------------------------------------- /assets/6-4-1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-4-1.4.png -------------------------------------------------------------------------------- /assets/6-4-1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-4-1.5.png -------------------------------------------------------------------------------- /assets/6-4-1.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/6-4-1.6.png -------------------------------------------------------------------------------- /assets/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/cover.jpg -------------------------------------------------------------------------------- /assets/gpp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabocrack1/game-programming-patterns/HEAD/assets/gpp-logo.png -------------------------------------------------------------------------------- /shabloni_igrovogo_programmirovaniya.md: -------------------------------------------------------------------------------- 1 | # Шаблоны игрового программирования 2 | 3 | Эй, Разработчик Игр! 4 | 5 | * Борешься с тем, чтобы компоненты кода сливались в единое целое? 6 | * Тяжело вносить изменения с ростом кодовой базы? 7 | * Чувствуешь, что твоя игра как гигантский клубок, в котором все переплетается друг с другом? 8 | * Интересно, как применять шаблоны проектирования в играх? 9 | * Слышал понятия "когерентность кэша" и "пул объектов", но не знаешь, как их применить, чтобы сделать игру быстрее? 10 | 11 | Я здесь, чтобы помочь! **Шаблоны Игрового Программирования** это коллекция игровых паттернов, которые **делают код чище**, **понятнее** и **быстрее**. 12 | 13 | Это книга, которой мне не хватало, когда я начинал делать игры и теперь я хочу, чтобы она была у тебя. 14 | 15 | > _Данная гит-книга является адаптацией [перевода](http://live13.livejournal.com/462582.html) (откорректированного) веб версии [Game Programming Patterns](http://gameprogrammingpatterns.com/contents.html) Боба Найстрома (Bob Nystrom) и была создана для удобного чтения на русском языке в формате электронной книги. Автор данной гит-книги не является автором оригинала и перевода._ 16 | > 17 | > _О неточностях в переводе можно сообщать по почте al.lizurchik@gmail.com_ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Шаблоны игрового программирования 2 | 3 | ![](/assets/gpp-logo.png) 4 | 5 | Эй, Разработчик Игр! 6 | 7 | * Борешься с тем, чтобы компоненты кода сливались в единое целое? 8 | * Тяжело вносить изменения с ростом кодовой базы? 9 | * Чувствуешь, что твоя игра как гигантский клубок, в котором все переплетается друг с другом? 10 | * Интересно, как применять шаблоны проектирования в играх? 11 | * Слышал понятия "когерентность кэша" и "пул объектов", но не знаешь, как их применить, чтобы сделать игру быстрее? 12 | 13 | Я здесь, чтобы помочь! **Шаблоны Игрового Программирования** это коллекция игровых паттернов, которые **делают код чище**, **понятнее** и **быстрее**. 14 | 15 | Это книга, которой мне не хватало, когда я начинал делать игры и теперь я хочу, чтобы она была у тебя. 16 | 17 | --- 18 | 19 | > _Данная гит-книга является адаптацией _[_перевода_](http://live13.livejournal.com/462582.html)_ веб версии _[_Game Programming Patterns_](http://gameprogrammingpatterns.com/contents.html)_ Боба Найстрома \(Bob Nystrom\) и была создана для удобного чтения на русском языке в формате электронной книги._ 20 | > 21 | > _Исправлено форматирование; исправлены перепутанные картинки; расставлены недостающие внутренние ссылки; внешние ссылки, где это возможно, заменены на соотв. русскоязычные._ 22 | > 23 | > _Автор данной гит-книги не является автором оригинала и перевода._ 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /posledovatelnie-shabloni-sequencing-patterns.md: -------------------------------------------------------------------------------- 1 | # Последовательные шаблоны \(Sequencing Patterns\) 2 | 3 | Видеоигры прекрасны по большей мере потому, что позволяют нам побывать где-то еще. На несколько минут (а если быть более честным гораздо на дольше) мы становимся обитателями виртуального мира. Создание такого мира - это самое прекрасное, что есть в игровом программировании. 4 | 5 | Один из аспектов создания таких игровых миров - это _время_: искусственный мир живет и дышит в своем собственном ритме. Как строители миров мы должны самостоятельно изобретать время и шестеренки, управляющие работой часов игры. 6 | 7 | В данном разделе собраны шаблоны, которые могут нам в этом помочь. [Игровой цикл(Game Loop)](posledovatelnie-shabloni-sequencing-patterns/igrovoi-tsikl-game-loop.md) - это центральная ось, на которую опирается игровое время. Объекты слышат его тиканье через [Методы обновления (Update Methods)](posledovatelnie-shabloni-sequencing-patterns/metodi-obnovleniya-update-methods.md). Мы можем спрятать последовательную сущность компьютера за фасадом снимков отдельных моментов времени с помощью [Двойной буферизации (Double Buffering)](posledovatelnie-shabloni-sequencing-patterns/dvoinaya-buferizatsiya-double-buffering.md) и в результате обновление игрового мира будет казаться плавным. 8 | 9 | ## Шаблоны 10 | 11 | - [Двойная буферизация (Double Buffering)](posledovatelnie-shabloni-sequencing-patterns/dvoinaya-buferizatsiya-double-buffering.md) 12 | 13 | - [Игровой цикл(Game Loop)](posledovatelnie-shabloni-sequencing-patterns/igrovoi-tsikl-game-loop.md) 14 | 15 | - [Методы обновления (Update Methods)](posledovatelnie-shabloni-sequencing-patterns/metodi-obnovleniya-update-methods.md) -------------------------------------------------------------------------------- /povedencheskie-shabloni-behavioral-patterns.md: -------------------------------------------------------------------------------- 1 | # Поведенческие шаблоны \(Behavioral Patterns\) 2 | 3 | Как только вы сформируете основу игры и украсите ее актерами и декорациями, все что вам остается это запустить сцену. Для этого нам нужно поведение - сценарий, из которого игровые сущности будут знать что им делать. 4 | 5 | Конечно весь код - это уже "поведение" и вообще все программы определяют именно поведение, но отличие игр от других программ как раз и заключается в том насколько широкий диапазон поведения вам нужно реализовать. И хотя у текстового процессора конечно тоже много функций, это количество просто меркнет перед тем, сколько обитателей, предметов и квестов в средней ролевой игре. 6 | 7 | Шаблоны в этом разделе помогут вам определить и улучшить широкий диапазон поведения быстрым и простым для поддержки способом. [Объект тип (Type Object)](povedencheskie-shabloni-behavioral-patterns/obekt-tip-type-object.md) создает категории поведения без необходимости создавать для этого отдельные классы. [Подкласс песочница (Subclass Sandbox)](povedencheskie-shabloni-behavioral-patterns/podklass-pesochnitsa-subclass-sandbox.md) предоставляет вам набор примитивов, с помощью которых можно составлять различное поведение. Самый продвинутый метод - это [Байткод (Bytecode)](povedencheskie-shabloni-behavioral-patterns/baitkod-bytecode.md), который выносит поведение из сущности и помещает его в данные. 8 | 9 | ## Шаблоны 10 | 11 | - [Байткод (Bytecode)](povedencheskie-shabloni-behavioral-patterns/baitkod-bytecode.md) 12 | 13 | - [Подкласс песочница (Subclass Sandbox)](povedencheskie-shabloni-behavioral-patterns/podklass-pesochnitsa-subclass-sandbox.md) 14 | 15 | - [Объект тип (Type Object)](povedencheskie-shabloni-behavioral-patterns/obekt-tip-type-object.md) -------------------------------------------------------------------------------- /obzor-shablonov-proektirovaniya.md: -------------------------------------------------------------------------------- 1 | # Обзор шаблонов проектирования {#обзор-шаблонов-проектирования} 2 | 3 | Книге _Паттерны проектирования: Приемы объектно-ориентированного проектирования_ уже лет двадцать. Если вы прямо сейчас не читаете книгу через мое плечо, есть вероятность, что, когда вы будете читать эту книгу, _Паттерны проектирования_ будут такими старыми, что за это будет стоить выпить. Для такой быстро меняющейся индустрии как программирование - это практически вечность. Неугасающая популярность книги свидетельствует о том, что проектирование - это значительно меньше подверженная влиянию времени вещь, чем большинство технологий, языков и методологий. 4 | 5 | И, хотя я до сих пор считаю _Паттерны проектирования_ актуальными, за прошедшие десятилетия мы все-таки кое-чему научились. В этом разделе мы пройдемся по самым удачным из оригинальных шаблонов, задокументированных "Бандой Четырех". Смею надеяться, что мне есть, что добавить полезного о каждом из них. 6 | 7 | Некоторые шаблоны я считаю слишком переоцененными \([Синглтон \(Singleton\)](/obzor-shablonov-proektirovaniya/singlton-singleton.md)\), а другие наоборот недооцененными \([Команда \(Command\)](/obzor-shablonov-proektirovaniya/komanda-command.md)\). Еще парочку я сюда включил, потому что они особенно удачно подходят для применения в играх \([Приспособленец \(Flyweight\)](/obzor-shablonov-proektirovaniya/prisposoblenets-flyweight.md) и [Наблюдатель \(Observer\)](/obzor-shablonov-proektirovaniya/nablyudatel-observer.md)\). И, наконец, на мой взгляд иногда просто интересно посмотреть, как шаблоны путают с другими областями программирования \([Прототип \(Protorype\)](/obzor-shablonov-proektirovaniya/prototip-prototype.md) и [Состояние \(State\)](/obzor-shablonov-proektirovaniya/sostoyanie-state.md)\). 8 | 9 | -------------------------------------------------------------------------------- /shabloni-snizheniya-svyaznosti-decoupling-patterns.md: -------------------------------------------------------------------------------- 1 | # Шаблоны снижения связности \(Decoupling Patterns\) 2 | 3 | Как только вы начинаете разбираться в языке программирования, написание кода, который вам нужен становится достаточно простым. Гораздо сложнее писать код, который будет легко _изменять_ в будущем. Очень редко бывает так что мы можем предполагать что произойдет в будущем, когда запускаем наш редактор. 4 | 5 | У нас есть мощный инструмент, упрощения изменений - _снижение связности (decoupling)_. Когда мы говорим два участка кода "слабо связаны (decoupled)", мы имеем в виду что изменение одного обычно не требует изменения другого. Когда вам нужно добавить новый функционал в игре, чем меньше частей кода вам придется затронуть - тем лучше. 6 | 7 | Шаблон [Компонент(Component)](shabloni-snizheniya-svyaznosti-decoupling-patterns/komponent-component.md) снижает связность различных областей вашей игры друг от друга с помощью единой сущности, обладающей всеми их аспектами. [Очередь событий (Event Queue)](shabloni-snizheniya-svyaznosti-decoupling-patterns/ochered-sobitii-event-queue.md) снижает связность двух общающихся друг с другом объектов, как статически так и _во время работы (in time)_. Шаблон [Поиск службы (Service Locator)](shabloni-snizheniya-svyaznosti-decoupling-patterns/poisk-sluzhbi-service-locator.md) позволяет коду обращаться к объекту, не привязываясь к коду, который его предоставляет. 8 | 9 | Шаблоны 10 | 11 | - [Компонент(Component)](shabloni-snizheniya-svyaznosti-decoupling-patterns/komponent-component.md) 12 | 13 | - [Очередь событий (Event Queue)](shabloni-snizheniya-svyaznosti-decoupling-patterns/ochered-sobitii-event-queue.md) 14 | 15 | - [Поиск службы (Service Locator)](shabloni-snizheniya-svyaznosti-decoupling-patterns/poisk-sluzhbi-service-locator.md) -------------------------------------------------------------------------------- /shabloni-optimizatsii.md: -------------------------------------------------------------------------------- 1 | # Шаблоны оптимизации 2 | 3 | Тогда как рост производительности железа уже давно удовлетворил потребности большинства программ, игры до сих пор остаются исключением. Игроки всегда желают получить еще более богатый, реалистичный и захватывающий игровой опыт. Игры пытаются любым способом привлечь внимание игрока и те кто выжимает из железа больше чем остальные зачастую выигрывают. 4 | 5 | Оптимизация для увеличения производительности - это глубокое искусство, затрагивающее все аспекты программирования. Низкоуровневые программисты учатся работать с самыми незначительными особенностями архитектуры железа. В то же время разработчики алгоритмов разрабатывают математические аппараты для повышения их эффективности. 6 | 7 | Здесь я затрону несколько среднеуровневых шаблонов, которые часто используются для ускорения работы игры. [Локальность данных (Data Locality)](shabloni-optimizatsii/lokalnost-dannih-data-locality.md) познакомит вас с современной иерархией организации памяти и как ее можно использовать в своих целях. Шаблон [Грязный флаг (Dirty Flag)](shabloni-optimizatsii/gryaznii-flag-dirty-flag.md) поможет избавиться от лишних вычислений, а [Пул объектов (Object Pool)](shabloni-optimizatsii/pul-obektov-object-pool.md) поможет избежать лишнего выделения памяти. [Разделение пространства (Spatial Partitioning)](shabloni-optimizatsii/prostranstvennoe-razbienie.md) ускорит виртуальный мир и размещение в нем его обитателей. 8 | 9 | Шаблоны 10 | 11 | - [Локальность данных (Data Locality)](shabloni-optimizatsii/lokalnost-dannih-data-locality.md) 12 | 13 | - [Грязный флаг (Dirty Flag)](shabloni-optimizatsii/gryaznii-flag-dirty-flag.md) 14 | 15 | - [Пул объектов (Object Pool)](shabloni-optimizatsii/pul-obektov-object-pool.md) 16 | 17 | - [Разделение пространства (Spatial Partitioning)](shabloni-optimizatsii/prostranstvennoe-razbienie.md) -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Шаблоны игрового программирования](README.md) 4 | * [Введение](predislovie.md) 5 | * [Архитектура, производительность и игры](predislovie/arhitektura-proizvoditelnost-i-igri.md) 6 | * [Обзор шаблонов проектирования](obzor-shablonov-proektirovaniya.md) 7 | * [Команда \(Command\)](obzor-shablonov-proektirovaniya/komanda-command.md) 8 | * [Приспособленец \(Flyweight\)](obzor-shablonov-proektirovaniya/prisposoblenets-flyweight.md) 9 | * [Наблюдатель \(Observer\)](obzor-shablonov-proektirovaniya/nablyudatel-observer.md) 10 | * [Прототип \(Prototype\)](obzor-shablonov-proektirovaniya/prototip-prototype.md) 11 | * [Синглтон \(Singleton\)](obzor-shablonov-proektirovaniya/singlton-singleton.md) 12 | * [Состояние \(State\)](obzor-shablonov-proektirovaniya/sostoyanie-state.md) 13 | * [Последовательные шаблоны \(Sequencing Patterns\)](posledovatelnie-shabloni-sequencing-patterns.md) 14 | * [Двойная буферизация \(Double Buffering\)](posledovatelnie-shabloni-sequencing-patterns/dvoinaya-buferizatsiya-double-buffering.md) 15 | * [Игровой цикл \(Game Loop\)](posledovatelnie-shabloni-sequencing-patterns/igrovoi-tsikl-game-loop.md) 16 | * [Методы обновления \(Update Methods\)](posledovatelnie-shabloni-sequencing-patterns/metodi-obnovleniya-update-methods.md) 17 | * [Поведенческие шаблоны \(Behavioral Patterns\)](povedencheskie-shabloni-behavioral-patterns.md) 18 | * [Байткод \(Bytecode\)](povedencheskie-shabloni-behavioral-patterns/baitkod-bytecode.md) 19 | * [Подкласс песочница \(Subclass Sandbox\)](povedencheskie-shabloni-behavioral-patterns/podklass-pesochnitsa-subclass-sandbox.md) 20 | * [Объект тип \(Type Object\)](povedencheskie-shabloni-behavioral-patterns/obekt-tip-type-object.md) 21 | * [Шаблоны снижения связности \(Decoupling Patterns\)](shabloni-snizheniya-svyaznosti-decoupling-patterns.md) 22 | * [Компонент \(Component\)](shabloni-snizheniya-svyaznosti-decoupling-patterns/komponent-component.md) 23 | * [Очередь событий \(Event Queue\)](shabloni-snizheniya-svyaznosti-decoupling-patterns/ochered-sobitii-event-queue.md) 24 | * [Поиск службы \(Service Locator\)](shabloni-snizheniya-svyaznosti-decoupling-patterns/poisk-sluzhbi-service-locator.md) 25 | * [Шаблоны оптимизации](shabloni-optimizatsii.md) 26 | * [Локальность данных \(Data Locality\)](shabloni-optimizatsii/lokalnost-dannih-data-locality.md) 27 | * [Грязный флаг \(Dirty Flag\)](shabloni-optimizatsii/gryaznii-flag-dirty-flag.md) 28 | * [Пул объектов \(Object Pool\)](shabloni-optimizatsii/pul-obektov-object-pool.md) 29 | * [Пространственное разбиение](shabloni-optimizatsii/prostranstvennoe-razbienie.md) 30 | 31 | -------------------------------------------------------------------------------- /predislovie.md: -------------------------------------------------------------------------------- 1 | # Введение {#введение} 2 | 3 | В пятом классе у нас с другом был доступ к заброшенному классу с парочкой стареньких `TSR-80`. Вдохновил нас учитель, который подарил нам брошюру с простыми `BASIC` программами, чтобы нам было с чем возиться. 4 | 5 | Привод для считывания аудиокассет для компьютеров был сломан, поэтому если нам хотелось запустить какую-нибудь из программ, нам каждый раз приходилось вводить ее вручную. В основном мы предпочитали программы всего из нескольких строк наподобие: 6 | 7 | ```BASIC 8 | 10 PRINT "BOBBY IS RADICAL!!!" 9 | 20 GOTO 10 10 | ``` 11 | 12 | > Как будто если компьютер выведет это сообщение достаточное количество раз, утверждение станет правдой. 13 | 14 | И даже в этом случае нас подстерегали всевозможные трудности. Мы не имели представления о том как программировать, поэтому любая допущенная синтаксическая ошибка оказывалась для нас непреодолимой преградой. Если программа не работала, что случалось довольно часто, мы просто вводили ее заново. 15 | 16 | В самом конце брошюры с кодами программ находился настоящий монстр - программа, которая занимала несколько полных страниц мелкого кода. Нам потребовалось немало времени прежде чем даже осмелиться за нее взяться. Но это все равно было неизбежно, потому что листинг был озаглавлен как "Тоннели и тролли". Мы не имели ни малейшего представления о том, что она делает, но название явно намекало на то что это игра. А что может быть интереснее чем собственноручно запрограммированная игра? Нам так и не удалось ее запустить, а через год нам пришлось освободить этот класс \(только гораздо позже когда я уже постиг основы `BASIC`, я понял что это была всего лишь программа-генератор персонажей для настольной игры, а не сама игра\). Тем не менее жребий был брошен - с тех пор я решил для себя что буду разработчиком игр. 17 | 18 | Когда я еще был подростком у нас дома был `Macintosh` с `QuickBASIC` и немного позже с `THINK C`. Я проводил за попытками написания игровых программ все свои летние каникулы. Учиться самому было сложно и даже мучительно. Начинать писать игру всегда было довольно легко - скажем экран с картой или небольшой паззл. Но по мере добавления новых возможностей программировать становилось все сложнее и сложнее. Как только у меня переставало получаться держать всю игру в голове целиком, все рушилось. 19 | 20 | Поначалу задача была просто получить что-то рабочее. Затем становилось понятнее как писать программы, которые не осмыслишь в голове целиком. Вместо того чтобы просто читать книги наподобие "Как программировать на C++", я начал искать книги о том как организовывать программный код. 21 | 22 | > Не одно свое лето я провел за ловлей змеек и черепашек в болотах южной Луизианы. И, если бы там не было так жарко, вполне возможно я бы занимался [герпентологией](https://ru.wikipedia.org/wiki/Герпетология) и писал бы другие книги. 23 | 24 | Через несколько лет друг дал мне книгу _Паттерны проектирования: Приемы объектно-ориентированного проектирования_. Наконец-то! Это была книга, о которой я мечтал еще с подросткового возраста. Я прочел ее от корки до корки за один присест. У меня все еще были проблемы со своими программами, но мне было приятно видеть что не только у меня одного возникают подобные сложности и есть люди которые нашли способ их преодолеть. Наконец-то у меня появилось ощущение что я работаю не просто голыми руками, а у меня появились _инструменты_. 25 | 26 | > Тогда мы с ним встретились впервые, и уже через пять минут после знакомства я сел на пол и провел несколько часов за чтением полностью его игнорируя. Надеюсь, что с тех пор мои социальные навыки хоть немного улучшились. 27 | 28 | В 2001-м я получил работу своей мечты: должность инженера-программиста в `Electronic Arts`. Я не мог дождаться момента, когда смогу увидеть как выглядят настоящие игры и как профессионалы собирают их целиком. Как им удается создавать такие громадные игры как `Madden Football`, которые ни у одного человека точно не поместятся в голове? На что похожа их архитектура? Как отделены друг от друга физика и рендеринг? Или как код ИИ _\(прим.: искусственный интеллект\)_ взаимодействует с анимацией? Как, имея единую кодовую базу, добиться ее работы на разных платформах? 29 | 30 | Разбираться в исходном коде было одновременно унизительно и удивительно. В графической части, ИИ, анимации, визуальных эффекта был восхитительный код. У нас были люди, умеющие выжать последние такты из ЦПУ _\(прим.: центральное процессорное устройство\)_ и высвободить их для более нужных вещей. Еще до обеда эти люди успевали делать такие вещи, про возможность которых я даже не подозревал. 31 | 32 | Но вот архитектура, соединяющая все эти отличные компоненты вместе, зачастую хромала и делалась в последнюю очередь. Они настолько концентрировались на _функциональности_, что на организацию кода обращалось слишком мало внимания. Высокая связанность \(coupling\) отдельных модулей была обычным делом. Новый функционал прикручивался к старой кодовой базе как попало. Для моего разочарованного взгляда это выглядело, как работа множества программистов, которые, если и открывали когда-либо _Паттерны проектирования_, то не продвинулись в чтении дальше раздела про [Синглтон \(Singleton\)](/obzor-shablonov-proektirovaniya/singlton-singleton.md). 33 | 34 | Конечно не все было настолько плохо. Я ведь представлял себе игровых программистов как сидящих в башне из слоновой кости мудрецов, неделями дискутирующих о каждой мелочи в архитектуре игры. Реальность заключалась в том что я видел код, написанный в условиях жесткого дедлайна для платформы, на которой каждый такт ЦПУ был на вес золота. Люди потрудились на славу и, как я понял впоследствии, они действительно во многом выбрали лучшее решение. Чем больше я тратил времени на работу с этим кодом, тем больше я понимал сколько брильянтов он в себе таит. 35 | 36 | К сожалению тут уместен именно термин "таит". Это были зарытые в коде алмазы, а прямо по ним топталось множество людей. Я наблюдал, как люди в мучениях переизобретали решения, прекрасные примеры которых уже находились в коде с которым они работали. 37 | 38 | Именно эта проблема и побудила меня на написание данной книги. Я откопал и отполировал для вас лучшие из найденных мной в различных играх шаблоны для того чтобы вы могли тратить свое время на изобретение чего-то нового, а не на _переизобретение_ уже существующего. 39 | 40 | ## Что есть в магазинах? {#что-есть-в-магазинах} 41 | 42 | Сейчас продаются десятки книг, посвященных игровому программированию. Для чего понадобилось писать еще одну? 43 | 44 | Большинство книг по программированию игр, которые я видел, делятся на две категории: 45 | 46 | * **Узко-специализированные книги**. Эти книги концентрируются на чем-то одном и глубоко описывают только этот конкретный аспект. Они учат вас `3D` графике, рендерингу в реальном времени, симуляции физики, искусственному интеллекту или работе со звуком. Многие разработчики игр вообще специализируются только в определенной области. 47 | 48 | * **Книги обо всем**. В противоположность первым, эти книги пытаются охватить все части игрового движка. Они объединяют их вместе и показывают как собрать законченный движок, обычно для `3D` шутер от первого лица. 49 | 50 | Мне нравятся книги из обеих категорий, но я считаю, что все они оставляют слишком много белых пятен. Книги, концентрирующиеся на отдельных аспектах редко описывают, как данный кусок кода будет взаимодействовать с остальной игрой. Вы можете быть волшебником в области физики или рендеринга, но как правильно связать эти две подсистемы? 51 | 52 | Вторая категория охватывает все, но зачастую подход получается очень монолитным и слишком жанрово-ориентированным. Сейчас, в период расцвета казуальных и мобильных игр, создаются игры самых различных жанров. Мы не можем больше просто продолжать клонировать Quake. Книги, показывающие вам создание движка под определенный жанр, не очень помогут вам если у вас совсем другая игра. 53 | 54 | В отличие от других моя книга построена по принципу _à la carte\(франц.: по желанию\)_. Каждая из глав в этой книге представляет собой законченную идею, которую вы можете использовать в своем коде. Таким образом, вы получаете возможность смешивать их и использовать только то что лучше всего подходит именно для вашей игры. 55 | 56 | > Есть еще один хороший пример принципа à la carte - хорошо известная серия [Жемчужины игрового программирования \(Game Programming Gems\)](http://www.satori.org/game-programming-gems/). 57 | 58 | ## Как все это связано с шаблонами проектирования {#как-все-это-связано-с-шаблонами-проектирования} 59 | 60 | Каждая книга, имеющая в заголовке слово "шаблоны", так или иначе связана с классической книгой _"_[_Паттерны проектирования: Приемы объектно-ориентированного проектирования_](https://ru.wikipedia.org/wiki/Design_Patterns)_"_, написанной Эрихом Гаммой, Ричардом Хелмом, Ральфом Джонсоном и Джоном Влиссидесом \(их еще часто называют "Банда четырех"\). 61 | 62 | Называя свою книгу "Шаблоны игрового программирования" я вовсе не имею в виду, что книга банды четырех неприменима в играх. Совсем наоборот: [вторая часть](/obzor-shablonov-proektirovaniya.md) этой книги как раз обозревает многие из шаблонов, впервые описанных в _Паттернах проектирования_, но с той точки зрения как они могут быть применены в программировании игр. 63 | 64 | Более того, я считаю что эта книга будет полезна и для тех, кто не занимается разработкой игр. Использование описанных шаблонов будет уместным во многих не игровых приложениях. Я вообще мог бы назвать книгу _Еще больше шаблонов проектирования_, но на мой взгляд игровые примеры выглядят выразительнее. Или вам интереснее в очередной раз читать книгу о списках сотрудников и банковских счетах _\(прим.: часто в качестве примеров применения шаблонов проектирования используют банки, клиентов, счета и пр.\)_? 65 | 66 | > _Паттерны проектирования_ сами по себе также были написаны под впечатлением от другой книги. Идея создания языка шаблонов, описывающих ничем не ограниченные решения проблем пришла из книги [Язык шаблонов \(A Pattern Language\)](https://en.wikipedia.org/wiki/A_Pattern_Language) [Кристофера Александера](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B5%D0%BA%D1%81%D0%B0%D0%BD%D0%B4%D0%B5%D1%80,_%D0%9A%D1%80%D0%B8%D1%81%D1%82%D0%BE%D1%84%D0%B5%D1%80) \(и его соавторов Сары Ишикавы и Мюррея Силверстейна\). 67 | > 68 | > Их книга была посвящена архитектуре \(в духе _настоящей_ архитектуры, которая помогает строить дома, стены и т.д.\), но они надеялись что и другие смогут использовать подобную структуру для описания решений в других областях. _Паттерны проектирования_ банды четырех стараются применить тот же подход в программировании. 69 | 70 | * Вместо того чтобы попытаться низвергнуть "Паттерны проектирования", я рассматриваю свою книгу как их расширение. Пускай многие представленные здесь шаблоны будут полезны и в других типах программного обеспечения, я считаю что лучше всего они применимы именно к игровым задачам. 71 | 72 | * Время и последовательность действий зачастую являются ключевыми частями игровой архитектуры. События должны происходить в правильной последовательности и в нужное время. 73 | 74 | * Цикл разработки предельно сжат и множеству разработчиков необходимо иметь возможность быстро внедрять и итерационно менять широкий набор поведения, не наступая друг другу на ноги и не оставляя после себя следов по всей кодовой базе. 75 | 76 | * После того как поведение определено, начинается взаимодействие. Монстры кусают героя, зелья смешиваются, а бомбы взрывают врагов и друзей. Все эти взаимодействия должны реализовываться без превращения кодовой базы в спутанный клубок шерсти. 77 | 78 | * И наконец, для игр критична производительность. Игровые разработчики постоянно участвуют в гонке за первенство по максимально эффективному использования своей платформы. Небольшой трюк по сбережению нескольких тактов может отделять игру с наивысшим рейтингом и миллионными продажами от проблем с падением `fps`_\(кадров в секунду\)_ и злыми рецензиями. 79 | 80 | ## Как читать эту книгу {#как-читать-эту-книгу} 81 | 82 | Вся книга разделена на три большие части. Первая - это введение и описание самой книги. Главу из этой части наряду со [следующей](/predislovie/arhitektura-proizvoditelnost-i-igri.md) вы сейчас и читаете. 83 | 84 | Вторая часть - [Обзор шаблонов проектирования](/obzor-shablonov-proektirovaniya.md) рассматривает несколько шаблонов из книги банды четырех. Относительно каждого я высказываю собственное мнение и описываю его применимость в игровом программировании. 85 | 86 | И, наконец, последняя часть - это сама соль данной книги. В ней описаны тринадцать новых шаблонов, которые я видел в играх. Она делится еще на четыре части: [Последовательные шаблоны](/posledovatelnie-shabloni-sequencing-patterns.md), [Поведенческие шаблоны](/povedencheskie-shabloni-behavioral-patterns.md), [Шаблоны уменьшения связности \(decoupling\)](/shabloni-snizheniya-svyaznosti-decoupling-patterns.md) и [Оптимизационные шаблоны](/shabloni-optimizatsii.md). 87 | 88 | Каждый шаблон внутри раздела описывается в виде стандартизирвоанной структуры так чтобы вам было проще использовать эту книгу для поиска того что вам нужно: 89 | 90 | * Секция **Задача** представляет собой краткое описание шаблона в терминах задачи, для решения которой он предназначен. Это первое на что вы будете обращать внимание, когда будете искать в книге решение возникших у вас трудностей. 91 | 92 | * Секция **Мотивация** описывает пример проблемы, которую позволяет решить шаблон. В отличие от алгоритма, без приложения к конкретной проблеме шаблон сам по себе не имеет формы. Изучать шаблоны без примеров - это как учиться печь хлеб не упоминая того как месить тесто. Этот раздел - тесто, которое мы будем печь дальше. 93 | 94 | * Секция **Шаблон** описывает сущность шаблона из предшествующего примера. Если вам нужно формализованное описание шаблона - вы найдете его здесь. Также вам будет полезно заглянуть сюда, если вы уже знакомы с шаблоном, но подзабыли детали. 95 | 96 | * Итак, шаблон у нас уже описан на конкретном примере. Но, как теперь понять что шаблон подходит именно для той проблемы, решением которой вы сейчас заняты? Секция **Когда использовать** содержит рекомендацию когда шаблон стоит использовать, а когда его лучше избегать. Секция **Имейте в виду** посвящена последствиям, которые вы получите применив шаблон в своем коде. 97 | 98 | * Если вам, как и мне, нужен конкретный пример, как именно чего-либо добиться, тогда секция **Пример кода** для вас. Здесь подробно разбирается реализация шаблона чтобы вы точно смогли понять как он работает. 99 | 100 | * Шаблон - это собственно шаблон решения. Каждый раз когда вы его используете, вы реализовываете его немного по-другому. Следующая секция - **Архитектурные решения**, раскрывает некоторые варианты применения шаблона. 101 | 102 | * В конце находится еще одна коротенькая секция **Смотрите также**, в которой описывается связь шаблона с другими и с первоисточниками из Паттернов проектирования. Здесь вы получите более ясную картину о том, какое место занимает шаблон в экосистеме остальных шаблонов. 103 | 104 | ## О примерах кода {#о-примерах-кода} 105 | 106 | Примеры кода в этой книге приводятся на `C++`, но это совсем не значит, что шаблоны можно реализовывать только на этом языке, или что `C++` лучше всех остальных. Для наших целей годится любой ООП язык. 107 | 108 | `C++` я выбрал по нескольким причинам. Самая главная из которых заключается в том что на сегодняшний день это самый популярный для написания коммерческих игр язык. Для индустрии это [_lingua franca_](https://ru.wikipedia.org/wiki/Лингва_франка). Более того синтаксис `C`, на который опирается `C++` является также основой для `Java`, `C#`, `JavaScript` и многих других языков. Даже, если вы не знаете `C++`, приведенный в книге код будет понятен без приложения особых усилий. 109 | 110 | Цель этой книги не в том чтобы научить вас `C++`. Примеры наоборот максимально упрощены и даже не демонстрируют хороший стиль использования `C++`. Они предназначены для того чтобы в них лучше читалась идея, а не просто читался хороший код. 111 | 112 | В частности код не использует "модные" решения из `С++11` или более новых реализаций. В нем не используются стандартные библиотеки и довольно редко используются шаблоны. В результате `C++` код получился "плохим", но я не считаю это недостатком потому что таким образом он стал проще и его будет легче понять людям, использующим `C`, `Objective C`, `Java` и другие языки. 113 | 114 | Чтобы не тратить место на код который вы уже видели и который не относится к самому шаблону, он иногда будет приведен с сокращениями. В этом случае пример кода будет сопровождаться пояснением о том, что делает отсутствующая в листинге часть кода. 115 | 116 | Представим себе функцию, которая выполняет некоторые действия и возвращает результат. Описываемый шаблон зависит только от возвращаемого значения, а не от того что делает функция. В таком случае пример кода будет выглядеть следующим образом: 117 | 118 | ```cpp 119 | bool update() 120 | { 121 | // Do work... 122 | return isDone(); 123 | } 124 | ``` 125 | 126 | ## Куда двигаться дальше {#куда-двигаться-дальше} 127 | 128 | Шаблоны - это постоянно обновляющаяся и расширяющаяся часть программирования. Эта книга продолжает начинание банды четырех в области документирования и демонстрации найденных шаблонов и этот процесс не будет остановлен, когда высохнут чернила на этих страницах. 129 | 130 | Именно вы ключевая часть этого процесса. По мере того как вы будете изобретать новые шаблоны, улучшать \(или опровергать!\) уже существующие, вы будете приносить пользу всему сообществу разработчиков. Так что если у вас есть свои суждения, дополнения и другие комментарии касательно написанного, прошу выходить на связь. 131 | 132 | -------------------------------------------------------------------------------- /obzor-shablonov-proektirovaniya/prisposoblenets-flyweight.md: -------------------------------------------------------------------------------- 1 | # Приспособленец \(Flyweight\) 2 | 3 | Туман рассеивается, открывая нашему взгляду величественный старый лес. Бесчисленные кедры образуют над вами зеленый свод. Ажурная листва пронизывается отдельными лучиками света, окрашивая туман в желтые цвета. Меж гигантских стволов виден бесконечный лес вокруг. 4 | 5 | О таких сценах внутри игры мы, как игровые разработчики, и мечтаем. И именно для таких сцен как нельзя лучше подходит скромный шаблон с именем Приспособленец \(Flyweight\). 6 | 7 | ## Лес для деревьев 8 | 9 | Я могу описать целый лес всего несколькими предложениями, но _реализация_ его в настоящей игре - совсем другая история. Если вам захочется вывести на экране весь лес из индивидуальных деревьев целиком, любой графический программист сразу увидит миллионы полигонов, которые придется обработать видеокарте на каждом кадре. 10 | 11 | Мы говорим именно о тысячах деревьев, геометрия каждого из которых достаточно детализирована и насчитывает тысячи полигонов. Даже если у вас найдется достаточно _памяти_ чтобы это все уместить, то для того чтобы отрендерить лес целиком вам нужно будет пропустить это все через шины процессора и видеокарты. 12 | 13 | С каждым деревом связаны следующие данные: 14 | 15 | * Полигональная сетка, описывающая его ствол, ветви и листву. 16 | * Текстуры коры и листьев. 17 | * Положение и ориентация в лесу. 18 | * Индивидуальные настройки, такие как размер и оттенок, благодаря которым каждое дерево в лесу будет выглядеть индивидуально. 19 | 20 | Если набросать описывающий все это код, получится нечто подобное: 21 | 22 | ```cpp 23 | class Tree 24 | { 25 | private: 26 | Mesh mesh_; 27 | Texture bark_; 28 | Texture leaves_; 29 | Vector position_; 30 | double height_; 31 | double thickness_; 32 | Color barkTint_; 33 | Color leafTint_; 34 | }; 35 | ``` 36 | 37 | Целая куча данных и здоровенные полигональная сетка и текстура. Весь лес определенно не получится запихнуть целиком в видеокарту на каждом кадре. К счастью есть проверенный временем способ обойти это ограничение. 38 | 39 | Дело тут в том, что даже несмотря на то, что деревьев в лесу тысячи, выглядят они довольно похоже. Потому что все используют одни и те же сетку и текстуру. Это значит что большинство полей в объекта идентичны для всех его экземпляров. 40 | 41 | > Я бы посчитал безумцем или миллиардером того, кто стал бы делать для каждого дерева отдельную модель. 42 | 43 | ![](/assets/2-3.1-1.png) 44 | 45 | > Обратите внимание, что данные в прямоугольнике одинаковы для всех деревьев. 46 | 47 | Мы можем смоделировать это явным образом путем разделения объекта пополам. Во-первых, мы достанем данные, общие для всех деревьев, и переместим их в отдельный класс: 48 | 49 | ```cpp 50 | class TreeModel 51 | { 52 | private: 53 | Mesh mesh_; 54 | Texture bark_; 55 | Texture leaves_; 56 | }; 57 | ``` 58 | 59 | Игре нужен только один экземпляр этого класса, потому что нам незачем иметь тысячи копий одной и той же сетки и текстуры. Поэтому каждый _экземпляр_ дерева в мире должен иметь _ссылку_ на общий `TreeModel`. В `Tree` останутся данные, индивидуальные для каждого экземпляра: 60 | 61 | ```cpp 62 | class Tree 63 | { 64 | private: 65 | TreeModel* model_; 66 | 67 | Vector position_; 68 | double height_; 69 | double thickness_; 70 | Color barkTint_; 71 | Color leafTint_; 72 | }; 73 | ``` 74 | 75 | Результат можно изобразить следующим образом: 76 | 77 | ![](/assets/2-3.1-2.png) 78 | 79 | > Это очень напоминает шаблон Объект тип \([Type Object](file:///D:/Users/a.lemekhov/Documents/game_programming_patterns_content/4.3-type-object.html)\). Оба основаны на делегировании части состояния объекта другому объекту, разделяемому между многими экземплярами. Только области применения у этих шаблонов разные. 80 | > 81 | > Область применения шаблона Объект тип - это минимизация количества классов, которые вам нужно определять при добавлении "типов" в свою модель объектов. В качестве бонуса вы получаете разделение памяти. А вот шаблон Приспособленец в первую очередь предназначен для увеличения эффективности использования памяти. 82 | 83 | Это конечно все хорошо и экономит нам кучу памяти, но как это поможет нам в рендеринге? Прежде чем лес появится на экране, его нужно пропустить через память видеокарты. Нам нужно организовать разделение ресурсов таким образом чтобы это было понятно видеокарте. 84 | 85 | ## Тысяча экземпляров 86 | 87 | Для того чтобы минимизировать количество данных, передаваемых видеокарте, мы будем передавать общие данные, т.е. `TreeModel` только _один раз_. После этого мы будем по отдельности передавать индивидуальные для каждого экземпляра данные - позицию, цвет и размер. А затем просто скомандуем видеокарте "Используй эту модель для отрисовки всех этих экземпляров". 88 | 89 | К счастью API современных видеокарт такую возможность поддерживает. Детали конечно гораздо сложнее и выходят за рамки рассмотрения этой книги, однако и в Direct3D и в OpenGL присутствует возможность рендеринга экземпляров \([_instanced rendering_](https://ru.wikipedia.org/wiki/Geometry_Instancing)\). 90 | 91 | В обеих API вы формируете два потока данных. Первый - это общие данные, которые используются много раз - сетки и текстуры, как в нашем примере. Второй - список экземпляров и их параметры, которые позволяют варьировать данные из первой группы данных во время отрисовки. Весь лес появляется после единственного вызова отрисовки. 92 | 93 | > Сам по себе этот API видеокарты свидетельствует о том что шаблон Приспособленец - единственный из шаблонов банды четырех, получивший аппаратную реализацию. 94 | 95 | ## Шаблон приспособленец 96 | 97 | Теперь когда у нас есть хороший пример, я могу рассказать вам о самом шаблоне. Приспособленец, как следует из его имени, вступает в игру когда нам требуется максимально облегченный объект, обычно потому, что нам нужно очень много таких объектов. 98 | 99 | При использовании метода рендеринга экземпляров \(instanced rendering\), дело даже не в том, что нужно много памяти, а в том, что требуется слишком много _времени_, чтобы прокачать данные о каждом дереве через шину видеокарты. Главное что базовая идея общая. 100 | 101 | Шаблон решает эту проблемы с помощью разделения данных объекта на два типа: первый тип данных - это неуникальные для каждого экземпляра объекта данные, которые можно иметь в одном _экземпляре_ для всех объектов. Банда четырех называет их _внутренним_ \(intrinsic\) состоянием, но мне больше нравится название "контекстно-независимые". В нашем примере это геометрия и текстура дерева. 102 | 103 | Оставшиеся данные - это _внешнее_ \(extrinsic\) состояние, все то, что уникально для каждого экземпляра. В нашем случае это позиция, масштаб и цвет каждого из деревьев. Также как и в приведенном выше фрагменте кода, этот шаблон предотвращает перерасход памяти с помощью разделения одной копии внутреннего состояния между всеми местами, где она появляется. 104 | 105 | То что мы до сих пор видели, выглядит как простое разделение ресурсов и вряд ли заслуживает того, чтобы называться шаблоном. Частично это вызвано тем, что в нашем примере присутствует очевидное _свойство_ \(identity\), выделяемое в качестве разделяемого ресурса - `TreeModel`. 106 | 107 | Я считаю этот шаблон менее очевидным \(и поэтому более хитрым\), когда он используется в случае, где выделить свойства для разделяемого объекта не так легко. В таком случае возникает впечатление что объект магическим образом оказывается в нескольких местах одновременно. Давайте я продемонстрирую это на примере. 108 | 109 | ## Место где можно пустить корни 110 | 111 | Земля, на которой растут наши деревья, тоже должна быть представлена в игре. Это могут быть участки травы, грязи, холмов, озер и рек и любой другой тип местности, который вы только сможете придумать. Мы сделаем нашу землю на _основе тайлов_: вся поверхность будет состоять их отдельных маленьких тайлов. Каждый тайл будет относиться к какому-либо типу местности. 112 | 113 | Каждый из типов местности имеет ряд параметров, влияющих на геймплей: 114 | 115 | * Стоимость перемещения, определяющая скорость с которой игроки могут по ней двигаться. 116 | * Флаг, означающий что местность залита водой и по ней можно перемещаться на лодке. 117 | * Используемая для рендеринга текстура. 118 | 119 | Так как все мы - игровые программисты параноики в плане эффективности, мы точно не станем хранить все эти данные для каждого тайла в игровом мире. Вместо этого обычно используется перечисление для описания типов местности: 120 | 121 | > В конце концов мы усвоили наш урок с этим лесом. 122 | 123 | ```cpp 124 | enum Terrain 125 | { 126 | TERRAIN_GRASS, 127 | TERRAIN_HILL, 128 | TERRAIN_RIVER 129 | 130 | // Other terrains... 131 | 132 | }; 133 | ``` 134 | 135 | А сам мир хранит здоровенный массив этих значений: 136 | 137 | ```cpp 138 | class World 139 | { 140 | private: 141 | Terrain tiles_[WIDTH][HEIGHT]; 142 | }; 143 | ``` 144 | 145 | > Я использую для хранения 2D сетки многомерный массив. В C++ это эффективно, потому что все элементы упакованы в одном месте. В Java и других языках с управлением памятью, мы бы получили просто массив строк, каждый из элементов которого был бы _ссылкой_ на массив элементов столбика. Т.е. особой эффективностью в работе с памятью здесь не пахнет. 146 | > 147 | > В любом случае в реальном коде реализацию 2D сетки лучше спрятать. В примере такой подход выбран исключительно ради простоты. 148 | 149 | Чтобы получить полезную информацию о тайле используется нечто наподобие: 150 | 151 | ```cpp 152 | int World::getMovementCost(int x, int y) 153 | { 154 | switch (tiles_[x][y]) 155 | { 156 | case TERRAIN_GRASS: return 1; 157 | case TERRAIN_HILL: return 3; 158 | case TERRAIN_RIVER: return 2; 159 | // Other terrains... 160 | } 161 | } 162 | 163 | 164 | bool World::isWater(int x, int y) 165 | { 166 | switch (tiles_[x][y]) 167 | { 168 | case TERRAIN_GRASS: return false; 169 | case TERRAIN_HILL: return false; 170 | case TERRAIN_RIVER: return true; 171 | // Other terrains... 172 | } 173 | } 174 | ``` 175 | 176 | Ну, идею вы поняли. Такой подход работает, но я нахожу его уродливым. Когда я думаю о скорости перемещения по местности я предполагаю увидеть _данные_ о местности, а они вместо этого внедрены в код. Еще хуже то, что данные об одном типе местности размазаны по целой куче методов. Хотелось бы видеть все это инкапсулированным в одном месте. В конце концов именно для этого и предназначены объекты. 177 | 178 | Было бы здорово иметь настоящий класс для местности наподобие такого: 179 | 180 | ```cpp 181 | class Terrain 182 | { 183 | public: 184 | Terrain(int movementCost, bool isWater, Texture texture) 185 | : movementCost_(movementCost), 186 | isWater_(isWater), 187 | texture_(texture) 188 | {} 189 | 190 | int getMovementCost() const { return movementCost_; } 191 | bool isWater() const { return isWater_; } 192 | const Texture& getTexture() const { return texture_; } 193 | 194 | private: 195 | int movementCost_; 196 | bool isWater_; 197 | Texture texture_; 198 | }; 199 | ``` 200 | 201 | > Вы можете заметить, что все методы здесь объявлены как const. Это не совпадение. Так как один и тот же объект используется в различном контексте, если мы его изменим, изменения одновременно произойдут и во всех остальных местах. 202 | > 203 | > Возможно это не то, что вам нужно. Разделение объектов в целях экономии памяти - это оптимизация, не влияющая на видимое поведение приложения. Вот поэтому объект Приспособленец практически всегда делают неизменным \(immutable\). 204 | 205 | Но мы не можем согласиться с тем, что нам придется платить производительностью за роскошь иметь экземпляр данного класса для каждого тайла. Если вы внимательно посмотрите на класс, вы обратите внимание на то, что здесь нет вообще _никакой_ специфической информации, определяющей _где_ находится тайл. Т.е. в терминах приспособленца, _все_ состояние местности является внутренним \(intrinsic\) или "контекстно-независимым". 206 | 207 | Принимая это во внимание, у нас нет причин иметь больше одного объекта для каждого типа местности. Каждый тайл травы идентичен любому другому. Вместо того, чтобы иметь массив перечислений или объектов `Terrain`, мы будем хранить массив _указателей_ на объекты `Terrain`: 208 | 209 | ``` 210 | class World 211 | { 212 | private: 213 | Terrain* tiles_[WIDTH][HEIGHT]; 214 | // Другие вещи... 215 | }; 216 | ``` 217 | 218 | Каждый из тайлов, относящихся к одному типу местности указывает на один и тот же экземпляр местности. 219 | 220 | ![](/assets/2-3.1-3.png) 221 | 222 | Так как экземпляры местности используются в нескольких местах, управлять их временем жизни будет сложнее, чем если бы мы создавали их динамически. Вместо этого мы будем прямо хранить их в мире: 223 | 224 | ```cpp 225 | class World 226 | { 227 | public: 228 | World() 229 | : grassTerrain_(1, false, GRASS_TEXTURE), 230 | hillTerrain_(3, false, HILL_TEXTURE), 231 | riverTerrain_(2, true, RIVER_TEXTURE) 232 | {} 233 | 234 | private: 235 | Terrain grassTerrain_; 236 | Terrain hillTerrain_; 237 | Terrain riverTerrain_; 238 | 239 | // Other stuff... 240 | 241 | }; 242 | ``` 243 | 244 | Теперь мы можем использовать их для отрисовки земли следующим образом: 245 | 246 | ```cpp 247 | void World::generateTerrain() 248 | { 249 | // Fill the ground with grass. 250 | for (int x = 0; x < WIDTH; x++) { 251 | for (int y = 0; y < HEIGHT; y++) { 252 | // Sprinkle some hills. 253 | if (random(10) == 0) { 254 | tiles_[x][y] = &hillTerrain_; 255 | } 256 | else { 257 | tiles_[x][y] = &grassTerrain_; 258 | } 259 | } 260 | } 261 | 262 | // Lay a river. 263 | int x = random(WIDTH); 264 | for (int y = 0; y < HEIGHT; y++) { 265 | tiles_[x][y] = &riverTerrain_; 266 | } 267 | } 268 | ``` 269 | 270 | > Могу сказать что это не самый лучший в мире алгоритм процедурной генерации местности. 271 | 272 | Дальше вместо методов в `World` для доступа к параметрам местности мы можем просто возвращать объект `Terrain` напрямую: 273 | 274 | ``` 275 | const Terrain& World::getTile(int x, int y) const 276 | { 277 | return *tiles_[x][y]; 278 | } 279 | ``` 280 | 281 | Таким образом `World` больше не перегружен различной информацией о местности. Если вам нужны некоторые параметры тайла, вы можете получить их у самого объекта: 282 | 283 | ``` 284 | int cost = world.getTile(2, 3).getMovementCost(); 285 | ``` 286 | 287 | Мы снова вернулись к приятному API работы с реальным объектом и добились этого практически без дополнительных накладных расходов: указатель обычно занимает не больше места чем перечисление. 288 | 289 | ## Что насчет производительности? 290 | 291 | Я говорю "почти" потому, что для точной оценки производительности нужно проводить точное сравнение по сравнению с использованием перечислений. Обращение к местности через указатели предполагает косвенное обращение. Чтобы получить данные о местности, например скорость перемещения, вам сначала потребуется получить указатель в массиве местностей, чтобы через него получить объект местности и только потом получить у него скорость перемещения. Попытка обращения через массив может привести к промаху кеша и, как следствие, к общему замедлению работы. 292 | 293 | > Более подробно о прыганье по указателям \(pointer chasing\) и про промахи кеша \(cache misses\) можно почитать в главе [Локализация данных \(Data Locality\)](/shabloni-optimizatsii/lokalnost-dannih-data-locality.md). 294 | 295 | Как всегда при оптимизации стоит пользоваться золотым правилом сперва _выполняем профилирование_. Современное железо настолько сложно, что производительность уже нельзя оценивать на глазок. Мои собственные тесты показали, что применение шаблона приспособленец не дает падения производительности по сравнению с применением последовательностей. Более того приспособленец даже быстрее. Но тут стоит учитывать и то, на сколько часто данные выгружаются из памяти. 296 | 297 | В чем _я_ точно уверен, так это в том, что использование объекта приспособленца скидывать со счетов не стоит. Он дает вам преимущества объектно-ориентированного стиля без расходов на кучу дополнительных объектов. Если вы ловите себя на создании множества последовательностей, а затем организовываете по ним выбор с помощью `switch`, попробуйте использовать шаблон. Ну а если боитесь за производительность, то по крайней мере проверьте свои опасения профайлером прежде, чем приводить свой код в менее поддерживаемую форму. 298 | 299 | ## Смотрите также 300 | 301 | * В примере с тайлами мы сразу создали экземпляры для каждого типа местности и сохранили их в 302 | `World`. Таким образом мы получили возможность переиспользовать и разделять экземпляры. Во многих других случаях у вас не будет желания создавать сразу _всех_ возможных приспособленцев. 303 | 304 | Если вы не можете предугадать, какие из них вам понадобятся, возможно лучше создавать их по запросу. Чтобы воспользоваться преимуществом разделения ресурсов вы можете организовать проверку, не загружен ли уже нужный вам экземпляр. Если загружен, вы просто возвращаете этот экземпляр. 305 | 306 | Обычно это значит что вам следует инкапсулировать их создание внутри некоторого интерфейса, который вначале будет производить поиск уже загруженных объектов. Пример такого сокрытия конструктора демонстрирует шаблон Фабричный метод \([Factory Method](https://ru.wikipedia.org/wiki/%D0%A4%D0%B0%D0%B1%D1%80%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BC%D0%B5%D1%82%D0%BE%D0%B4_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F))\). 307 | 308 | Чтобы иметь возможность возвратить ранее созданного приспособленцы, вам нужно хранить пул уже загруженных объектов. Если называть имена, то для их хранения можно использовать [Пул объектов \(Object Pool\)](/shabloni-optimizatsii/pul-obektov-object-pool.md). 309 | 310 | * Когда вы используете шаблон [Состояние \(State\)](/obzor-shablonov-proektirovaniya/sostoyanie-state.md) у вас часто возникает объект "состояние", который не имеет никаких полей, специфичных для машины, на которой это состояние используется. Для этого вполне достаточна сущность и методы состояния. В этом случае вы можете легко применять этот шаблон и переиспользовать один и тот же экземпляр состояния одновременное во множестве машин состояний. 311 | 312 | 313 | 314 | -------------------------------------------------------------------------------- /obzor-shablonov-proektirovaniya/komanda-command.md: -------------------------------------------------------------------------------- 1 | # Команда \(Command\) 2 | 3 | Команда - это один из моих любимых шаблонов. В большинстве программ, которые я писал - и в играх, и в других программах так или иначе находилось применение для этого шаблона. Не раз с его помощью мне удавалось распутать довольно корявый код. 4 | 5 | Для такого важного шаблона "Банда Четырех" приготовила ожидаемо заумное описание: 6 | 7 | > Инкапсуляция запроса в виде объекта, тем самым позволяя пользователям параметризировать клиенты с помощью различных запросов, организовывать в очереди или регистрировать запросы или организовывать поддержку отменяемых операций. 8 | 9 | Можно ли согласиться с таким ужасным приговором? Прежде всего, он искажает все, что данная метафора способна предложить. За пределами странного мира программ, где слова могут означать что угодно, слово "клиент" означает личность - кого-то с кем вы имеете дело. Причем, обычно, других людей не принято "параметризировать". 10 | 11 | Дальше идет просто перечисление того, где можно применять шаблон. Не слишком очевидно, конечно, если ваш вариант использования прямо не указан в этом перечне. _Мое_ краткое определение данного шаблона гораздо проще: 12 | 13 | **Команда - _это материализация вызова метода_.** 14 | 15 | > Материализовать \(Reify\) происходит от латинского "res", что значит "вещь \(thing\)" с английским суффиксом "-fy". Т.е. можно было бы использовать слово "овеществить \(thingfy\)". Мне даже нравится такой термин. 16 | 17 | Конечно "краткое" не всегда означает "достаточное", так что толку от этого по-прежнему мало. Давайте немного углубимся в суть дела. "Материализовать", чтобы вы знали, означает буквально "сделать реальным". Еще один термин материализации - это объявление чего либо "объектом первого класса". 18 | 19 | > _Система отражений \(Reflection systems\)_ в некоторых языках позволяют вам работать с типами в программе императивно. Вы можете создать объект, представляющий собой класс другого объекта и с его помощью понимать, что этот объект может делать. Т.е. мы получаем _овеществленную систему типизации_. 20 | 21 | Оба термина означают, что мы возьмем некую концепцию и превратим ее в _набор данных_ - объект, который можно поместить в переменную, передать в функцию и т.д. Таким образом если мы говорим, что Команда - это "материализация вызова метода" - это означает, что вызов метода оборачивается некоторым объектом. 22 | 23 | Есть много разных названий: "обратный вызов", "функция первого класса", "указатель на функцию", "замыкание \(closure\)", "частично примененная функция \(partially applied function\)", в зависимости от языка, к которому вы привыкли. Однако, все это одного поля ягоды. "Банда четырех" немного дальше уточняет: 24 | 25 | > Команда - это объектно-ориентированная замена обратного вызова. 26 | 27 | Это уже гораздо полезнее для осмысленного выбора шаблона. 28 | 29 | Но пока это все абстрактно и слишком туманно. Я хочу начать главу с чего-то конкретного, и сейчас я это сделаю. Чтобы это сделать мне понадобится пример, в котором применение Команды будет смотреться идеально. 30 | 31 | ## Настройка ввода 32 | 33 | Внутри каждой игры есть код, отвечающий за считывание пользовательского ввода - нажатия на кнопки, клавиатурные события, нажатия мыши и т.д. Этот код обрабатывает ввод и преобразует его в соответствующие действия в игре: 34 | 35 | ![](/assets/2-2.1-1.png) 36 | 37 | Самая примитивная реализация выглядит следующим образом: 38 | 39 | ```cpp 40 | void InputHandler::handleInput() 41 | { 42 | if (isPressed(BUTTON_X)) jump(); 43 | else if (isPressed(BUTTON_Y)) fireGun(); 44 | else if (isPressed(BUTTON_A)) swapWeapon(); 45 | else if (isPressed(BUTTON_B)) lurchIneffectively(); 46 | } 47 | ``` 48 | 49 | > Совет профессионала: Не нажимайте B слишком часто. 50 | 51 | Такая функция обычно вызывается на каждом кадре внутри [игрового цикла \(Game Loop\)](/posledovatelnie-shabloni-sequencing-patterns/igrovoi-tsikl-game-loop.md). Думаю, вам понятно что она делает. Здесь мы видим жесткую привязку пользовательского ввода с действиями в игре. Однако многие игры позволяют пользователям _настраивать_, какие кнопки за что отвечают. 52 | 53 | Для того, чтобы это стало возможным, нам нужно преобразовать прямые вызовы `jump()` и `fireGun()` в нечто, что мы сможем свободно менять местами. "Менять местами" звучит, как присвоение значений переменным, поэтому нам нужен _объект_, который будет представлять игровое действие. В дело вступает шаблон Команда. 54 | 55 | Для начала определим базовый класс, представляющий запускаемую игровую команду: 56 | 57 | ```cpp 58 | class Command 59 | { 60 | public: 61 | virtual ~Command() {} 62 | virtual void execute() = 0; 63 | }; 64 | ``` 65 | 66 | > Когда у вас появляется интерфейс с единственным методом, который ничего не возвращает - с большой долей вероятности можно предположить что это шаблон Команда. 67 | 68 | Теперь создадим дочерние классы для каждой из различных игровых команд: 69 | 70 | ```cpp 71 | class JumpCommand : public Command 72 | { 73 | public: 74 | virtual void execute() { jump(); } 75 | }; 76 | 77 | class FireCommand : public Command 78 | { 79 | public: 80 | virtual void execute() { fireGun(); } 81 | }; 82 | 83 | 84 | // Ну вы поняли... 85 | ``` 86 | 87 | В нашем обработчике ввода мы будем хранить указатели на команду для каждой кнопки: 88 | 89 | ```cpp 90 | class InputHandler 91 | { 92 | 93 | public: 94 | void handleInput(); 95 | 96 | // Методы для привязки команд... 97 | private: 98 | Command* buttonX_; 99 | Command* buttonY_; 100 | Command* buttonA_; 101 | Command* buttonB_; 102 | }; 103 | ``` 104 | 105 | Теперь обработка ввода сводится к делегированию такого вида: 106 | 107 | ```cpp 108 | void InputHandler::handleInput() 109 | { 110 | if (isPressed(BUTTON_X)) buttonX_->execute(); 111 | else if (isPressed(BUTTON_Y)) buttonY_->execute(); 112 | else if (isPressed(BUTTON_A)) buttonA_->execute(); 113 | else if (isPressed(BUTTON_B)) buttonB_->execute(); 114 | } 115 | ``` 116 | 117 | > Обратите внимание что проверки на `null` здесь нет. Подразумевается, что к каждой кнопке привязана определенная команда. 118 | > 119 | > Если мы и в сам деле хотим иметь кнопку, которая ничего не делает, то нам все равно не нужно добавлять проверку на `null`. Вместо этого нам нужно реализовать команду, метод `execute()` которой ничего не делает. И, потом, вместо установки обработчика кнопки в null, мы будем подставлять указатель на этот объект. Такой шаблон носит название [Нулевой объект \(Null Object\)](http://bit.ly/null_object). 120 | 121 | Там, где раньше пользовательский ввод напрямую вызывал функции, теперь, у нас появился промежуточный слой косвенности: 122 | 123 | ![](/assets/2-2.1-2.png) 124 | 125 | В этом и заключается сущность шаблона Команда. Если вы уже оценили его по достоинству, оставшуюся часть главы можете рассматривать как бонус. 126 | 127 | ## Указания для акторов 128 | 129 | Классы команд, которые мы только что определили, отлично работают для примера выше, но их возможности все-таки сильно ограничены. Проблема в том, что мы предполагаем, что у нас уже есть готовые функции высокого уровня `jump()`, `fireGun()` и т.д., которые сами знают, как найти реализацию игрока и заставить его плясать под нашу дудку. 130 | 131 | Такое предположение значительно снижает применимость наших команд. Получается что команда `JumpCommand` - это единственное, что способно заставить прыгать только нашего игрока. Давайте избавимся от этого ограничения. Вместо того чтобы запускать функцию, которая будет сама искать объект для воздействия, мы сами _передадим_ ей объект, которым хотим управлять: 132 | 133 | ```cpp 134 | class Command 135 | { 136 | public: 137 | virtual ~Command() {} 138 | virtual void execute(GameActor& actor) = 0; 139 | }; 140 | ``` 141 | 142 | Здесь в качестве `GameActor` выступает наш класс "игровой объект", представляющий игрока в игровом мире. Мы передаем его в `execute()` и, таким образом, изолированная команда получает возможность вызвать метод выбранного нами актора: 143 | 144 | ```cpp 145 | class JumpCommand : public Command 146 | { 147 | public: 148 | virtual void execute(GameActor& actor) 149 | { 150 | actor.jump(); 151 | } 152 | }; 153 | ``` 154 | 155 | Теперь мы можем использовать этот единственный класс, чтобы заставить прыгать любого в нашей игре. Правда, у нас пока еще нет прослойки между обработчиком ввода и командой, которая, собственно, получает команду и применяет ее к нужному объекту. Для начала мы изменим `handleInput()` таким образом, чтобы она _возвращала_ команду: 156 | 157 | ```cpp 158 | Command* InputHandler::handleInput() 159 | { 160 | 161 | if (isPressed(BUTTON_X)) return buttonX_; 162 | if (isPressed(BUTTON_Y)) return buttonY_; 163 | if (isPressed(BUTTON_A)) return buttonA_; 164 | if (isPressed(BUTTON_B)) return buttonB_; 165 | 166 | // Если ничего не передано, то ничего и не делаем. 167 | return NULL; 168 | } 169 | ``` 170 | 171 | Функция не может выполнить команду немедленно, потому что не знает какого актора ей передать. Зато мы можем воспользоваться тем преимуществом команды, что это материализованный вызов - мы можем _отложить_ выполнение. 172 | 173 | Теперь нам нужен код, который получит команду и передаст в нее актора, представляющего игрока. Нечто наподобие: 174 | 175 | ```cpp 176 | Command* command = inputHandler.handleInput(); 177 | 178 | if (command) { 179 | command->execute(actor); 180 | } 181 | ``` 182 | 183 | Предполагая что `actor` указывает на персонажа игрока, мы получаем корректную реализацию того, чего добивались, т.е. мы вернулись к тому же поведению, что и в самом первом примере. Добавив слой косвенности между командой и актором, который ее выполняет, мы получили еще одну приятную способность: _теперь мы можем позволить игроку управлять любым актором в игре, просто подменяя актора, к которому применяется команда_. 184 | 185 | На практике такая возможность используется не слишком часто. Но похожий вариант использования все равно часто _всплывает_. До сих пор мы упоминали только управляемых игроком персонажей. А что насчет остальных? Тех, которые управляются игровым AI. Мы можем использовать тот же самый шаблон в качестве интерфейса между движком AI и акторами: код AI просто будет вызывать объекты `Command`. 186 | 187 | Уменьшение связности в данном случае, когда AI выбирает команду, а код актора ее выполняет, дает нам дополнительную гибкость. Мы получаем возможность использовать разные модули AI для разных акторов. Или же, мы можем их смешивать и выстраивать AI для разных стилей поведения. Вам нужен более агрессивный противник? Просто подключите более агрессивный AI чтобы им управлять. На самом деле мы можем даже передать на попечение AI персонаж _игрока_, что довольно удобно для демо-режима, когда игра работает на автопилоте. 188 | 189 | Делая команду, управляющую актором объектов первого класса, мы избавляемся от жесткой привязки прямого вызова методов. Вместо этого можете думать об этом как об очереди или потоке команд. 190 | 191 | > Более подробно о такой очередности можно почитать в [Очереди событий \(Event Queue\)](/shabloni-snizheniya-svyaznosti-decoupling-patterns/ochered-sobitii-event-queue.md) 192 | 193 | ![](/assets/2-2.1-3.png) 194 | 195 | > И почему интересно мне захотелось изобразить для вам такой "поток"? И почему он имеет форму трубки? 196 | 197 | Некоторый код \(обработчик ввода или AI\) генерирует команды и добавляет их в поток. Другой код \(диспетчер или сам актор\) поглощает команды и вызывает их. Поместив такую очередь в середину, мы уменьшили связность между производителем с одной стороны и потребителем с другой. 198 | 199 | > Если мы сделаем такую команду _сериализуемой_, мы сможем пересылать их очередность по сети. Сможем взять пользовательский ввод, передать его по сети и воспроизвести на другой машине. Именно такой механизм лежит в основе многопользовательских игр. 200 | 201 | ## Отмена и повтор 202 | 203 | Последний пример - это самый известный способ применения данного шаблона. Если объект команда может _выполнять_ действия, значит мы уже сделали маленький шаг к тому, чтобы получить возможность их _отменять_. Отмену можно встретить в некоторых стратегических играх, когда вы имеете возможность отменить последнее не понравившееся вам действие. Такая _функциональность обязательно присутствует_ и в инструментах, которые используются для _создания_ игр. Лучший способ заставить гейм-дизайнера ненавидеть вас - это выдать ему инструментарий, в котором нельзя отменить того, что он наворотил своими толстенькими пальчиками. 204 | 205 | > Это я могу утверждать на собственном опыте. 206 | 207 | Без шаблона команда, реализация отмены довольно сложна. С ним - пара пустяков. Для примера предположим что мы разрабатываем однопользовательскую пошаговую игру и хотим разрешить игроку отменять ходы чтобы он мог больше сосредоточиться на стратегии, а не на угадывании. 208 | 209 | Мы уже оценили удобство использования команды для абстрагирования пользовательского ввода, поэтому каждый ход игрока у нас уже инкапсулирован в команду. Например, движение юнита может выглядеть следующим образом: 210 | 211 | ```cpp 212 | class MoveUnitCommand : public Command 213 | { 214 | public: 215 | MoveUnitCommand(Unit* unit, int x, int y): 216 | unit_(unit), 217 | x_(x), 218 | y_(y) 219 | {} 220 | 221 | 222 | virtual void execute() 223 | { 224 | unit_->moveTo(x_, y_); 225 | } 226 | 227 | private: 228 | Unit* unit_; 229 | int x_, y_; 230 | }; 231 | ``` 232 | 233 | Обратите внимание на небольшое отличие от нашей предыдущей команды. В предыдущем примере мы хотели _абстрагировать_ команду от актора, на которого она действует. В этом же случае, мы специально хотим привязать ее к актору, которого она двигает. Экземпляр этой команды - это не обобщенная операция "перемещающая что либо", которую можно применить в самом разном контексте, а конкретный отдельный шаг в очереди шагов игры. 234 | 235 | Это показывает, насколько вариативным может быть применение данного шаблона. В некоторых случаях, как наша первая парочка примеров, команда - это переиспользуемый объект, представляющий _действие, которое можно выполнить_. Наш первый пример обработки ввода сводился к единственному вызову метода `execute()` по нажатию нужной кнопки. 236 | 237 | А вот более специфическая команда. Она описывает вещи, которые можно сделать в определенный момент. Это значит, что код обработчика ввода будет _создаваться_ каждый раз, когда игрок решит двинутся. Выглядеть это будет следующим образом: 238 | 239 | ```cpp 240 | Command* handleInput() 241 | { 242 | 243 | // Выбираем юнит... 244 | Unit* unit = getSelectedUnit(); 245 | 246 | if (isPressed(BUTTON_UP)) { 247 | // Перемещаем юнит на единицу вверх. 248 | int destY = unit->y() - 1; 249 | return new MoveUnitCommand(unit, unit->x(), destY); 250 | } 251 | 252 | if (isPressed(BUTTON_DOWN)) { 253 | // Перемещаем юнит на единицу вниз. 254 | int destY = unit->y() + 1; 255 | return new MoveUnitCommand(unit, unit->x(), destY); 256 | } 257 | 258 | // Другие шаги... 259 | return 260 | NULL; 261 | } 262 | ``` 263 | 264 | > Конечно, в языках без сборщика мусора, наподобие `C++`, это означает, что выполняющий команды код также должен заботиться и об освобождении памяти, занимаемой командами. 265 | 266 | Тот факт, что команды получаются одноразовыми, дает нам определенные преимущества. Чтобы сделать команды отменяемыми, мы определим еще одну операцию, которую должен реализовывать каждый класс команд: 267 | 268 | ```cpp 269 | class Command 270 | { 271 | public: 272 | virtual ~Command() {} 273 | virtual void execute() = 0; 274 | 275 | virtual void undo() = 0; 276 | }; 277 | ``` 278 | 279 | Метод `undo()` возвращает игру в то состояние, в котором она была до выполнения соответствующего метода `execute()`. Вот наша последняя команда, дополненная поддержкой отмены: 280 | 281 | ```cpp 282 | class MoveUnitCommand : public Command 283 | { 284 | 285 | public: 286 | MoveUnitCommand(Unit* unit, int x, int y): 287 | unit_(unit), 288 | xBefore_(0), 289 | yBefore_(0), 290 | x_(x), 291 | y_(y) 292 | {} 293 | 294 | virtual void execute() { 295 | 296 | // Запоминаем позицию юнита перед ходом 297 | // чтобы потом ее восстановить. 298 | 299 | xBefore_ = unit_->x(); 300 | yBefore_ = unit_->y(); 301 | unit_->moveTo(x_, y_); 302 | } 303 | 304 | virtual void undo() { 305 | unit_->moveTo(xBefore_, yBefore_); 306 | } 307 | 308 | private: 309 | Unit* unit_; 310 | int xBefore_, yBefore_; 311 | int x_, y_; 312 | }; 313 | ``` 314 | 315 | Обратите внимание, что мы добавили в класс больше состояний. После того как мы переместили юнит, ему неоткуда узнать где он был раньше. Чтобы иметь возможность отменить перемещение, нам нужно запомнить предыдущую позицию самостоятельно. Вот для этого мы и добавляем в команду `xBefore_` и `yBefore_`. 316 | 317 | > Похоже, здесь хорошо смотрелся бы шаблон [Хранитель \(Memento pattern\)](https://ru.wikipedia.org/wiki/%D0%A5%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D1%8C_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)), но мне не удалось заставить его работать эффективно. Так как целью команды является изменение только небольшой части состояния объекта, сохранение всех остальных данных - напрасная трата памяти. Дешевле просто вручную хранить только необходимые биты, которые вы меняете. 318 | > 319 | > Еще один вариант - [_постоянные структуры данных\(Persistent data structures\)_](https://en.wikipedia.org/wiki/Persistent_data_structure). При этом, при каждом изменении объекта возвращается новый объект, а старый остается неизменным. В разумной реализации эти новые объекты разделяют данные со старыми и в результате такой подход гораздо разумнее, чем клонирование объектов целиком. 320 | > 321 | > При использовании постоянных структур данных каждая команда хранит ссылку на объект перед выполнением команды и отмена означает просто возврат к старому объекту. 322 | 323 | Чтобы позволить игроку отменить движение, нам нужно сохранить последнюю выполненную им команду. И потом когда мы жмакнем `Ctrl-Z`, мы просто вызовем метод `undo()`. \(Если мы уже выполнили отмену, то по нажатию на ту же кнопку можно выполнить команду повтор и выполнить команду снова.\) 324 | 325 | Поддержка множественной отмены не намного сложнее. Вместо того, чтобы просто запоминать последнюю команду, мы будем хранить список команд и ссылку на "текущую". Когда игрок выполняет команду, она добавляется в список команд и помечается как "текущая". 326 | 327 | ![](/assets/2-2.1-4.png) 328 | 329 | Когда игрок выбирает "Отмена", мы отменяем текущую команду и сдвигаем указатель на одну позицию назад. Когда мы выполняем повтор, мы перемещаем указатель на позицию вперед и выполняем команду. Когда игрок после отмены выполняет новую команду, все содержимое списка после текущей команды выбрасывается. 330 | 331 | Когда я первый раз реализовал это в редакторе уровней, я почувствовал себя волшебником. Я был изумлен насколько прямолинейным является это решение и насколько хорошо оно работает. Конечно, вам потребуется некоторая дисциплина чтобы оформить все модификации в виде команд, но как только вы с этим справитесь - дальше все будет просто. 332 | 333 | > Повтор встречается в играх не часто, а вот _повторное проигрывание_ - очень часто. Прямолинейная реализация могла бы записывать состояние всей игры целиком на каждом кадре. Но такой подходи потребует слишком много памяти. 334 | > 335 | > Вместо этого многие игры записывают набор команд, выполненных на каждом кадре. Чтобы проиграть игру заново, движок просто запускает игру в обычном режиме и выполняет предварительно записанные команды. 336 | 337 | ## Круто или бесполезно? 338 | 339 | Как я уже говорил, команды похожи на функции первого класса или замыкания, однако, во всех примерах мы использовали определение классов. Если вы знакомы с функциональным программированием, вам наверное интересно где же функции. 340 | 341 | Я написал примеры таким образом потому что поддержка функций первого класса в `C++` весьма ограничена. Указатели на функции не имеют состояния, функторы - странные и все равно требуют определения классов, а лямбды в `C++11` сложны в работе из-за ограничений ручного управления памятью. 342 | 343 | При этом я _не утверждаю_ что вы не можете использовать для реализации шаблона Команда функции в других языках. Если вам доступна роскошь в виде языка с поддержкой настоящих замыканий - используйте их конечно! В некоторых случаях шаблон команда вообще используется в языках, не поддерживающих замыкания для их эмуляции. 344 | 345 | > Я говорю о _некоторых_ способах, потому что разработка настоящих классов или структур для команд может быть полезна даже в языках, поддерживающих замыкания. Если в вашей команде поддерживается много операций \(как в отменяемых командах\), привязка их к единственной функции будет смотреться неуклюже. 346 | > 347 | > Определение настоящего класса с полями дает возможность читающему код явно видеть какие данные содержит команда. Замыкания прекрасны в своей немногословности для оборачивания состояния, но они могут быть настолько автоматизированными, что будет сложно понять что собственно они хранят. 348 | 349 | Например, если мы пишем игру на `JavaScript`, мы можем написать команду движения следующим образом: 350 | 351 | ```js 352 | function makeMoveUnitCommand(unit, x, y) 353 | { 354 | // эта функция представляет собой объект команды: 355 | return function() 356 | { 357 | unit.moveTo(x, y); 358 | } 359 | } 360 | ``` 361 | 362 | С помощью пары замыканий мы можем реализовать отмену и повтор: 363 | 364 | ```js 365 | function makeMoveUnitCommand(unit, x, y) 366 | { 367 | var xBefore, yBefore; 368 | return { 369 | undo: function() 370 | { 371 | xBefore = unit.x(); 372 | yBefore = unit.y(); 373 | unit.moveTo(x, y); 374 | }, 375 | redo: function() 376 | { 377 | unit.moveTo(xBefore, yBefore); 378 | } 379 | }; 380 | } 381 | ``` 382 | 383 | Если вам комфортно работать в функциональном стиле, такой способ покажется для вас естественным. Если нет, я надеюсь, эта глава вам немного помогла. Для меня осознание полезности шаблона команда стало важным шагом в понимании полезности всей парадигмы функционального программирования в целом. 384 | 385 | ## Смотрите также 386 | 387 | * Вы можете наплодить достаточно большое количество классов Команд. Чтобы упростить их определение, можно создать общий базовый класс с кучей удобных высокоуровневых методов, которые наследующие его классы могут комбинировать для формирования своего поведения. В таком случае, главный метод команды `execute()` превращается в [подкласс Песочница \(Subclass Sandbox\)](/povedencheskie-shabloni-behavioral-patterns/podklass-pesochnitsa-subclass-sandbox.md). 388 | 389 | * В наших примерах мы явно указывали, какой актор должен выполнять команду. В некоторых случаях, особенно когда модель объекта организована иерархически, все может быть не столь очевидно. Объект может ответить на команду, а может перепоручить ее выполнение какому либо другому подчиненному объекту. Если вы это сделаете, вы получите [Цепочку ответственности \(Chain of Responsibility\)](https://ru.wikipedia.org/wiki/Цепочка_обязанностей). 390 | 391 | * Некоторые команды представляют собой прямолинейное поведение как в примере с `JumpCommand`. В этом случае иметь больше одного экземпляра класса - пустая трата памяти, потому что все экземпляры идентичны. В такой ситуации вам пригодится класс [Приспособленец \(Flyweight\)](/obzor-shablonov-proektirovaniya/prisposoblenets-flyweight.md). 392 | 393 | > Можно применять и [Синглтон \(Singleton\)](/obzor-shablonov-proektirovaniya/singlton-singleton.md), но друзья не позволяют создавать друзьям синглтоны. 394 | 395 | 396 | 397 | -------------------------------------------------------------------------------- /predislovie/arhitektura-proizvoditelnost-i-igri.md: -------------------------------------------------------------------------------- 1 | # Архитектура, производительность и игры {#архитектура-производительность-и-игры} 2 | 3 | Прежде чем мы с головой нырнем в кучу шаблонов, я думаю обрисовать для вас общую картину того, что я думаю об архитектуре программного обеспечения в целом и игр в частности. Это поможет вам легче понять остальное содержимое книги. По крайней мере у вас появятся аргументы для непрекращающихся споров о том - хорошая вещь шаблоны или полный отстой. 4 | 5 | > Обратите внимание, что я не настаиваю на том, чтобы вы приняли одну или другую сторону в этом противоборстве. Как у любого торговца оружием у меня есть, что предложить всем комбатантам. 6 | 7 | ## Что такое архитектура программы? {#что-такое-архитектура-программы} 8 | 9 | Если вы прочтете эту книгу от корки до корки, вы не подчерпнете для себя новых знаний по алгебре, используемой в 3D графике или вычислениями, используемыми в игровой физике. Вы не увидите реализацию альфа/бета отсечения \(alpha/beta pruning\) для вашего ИИ или симуляцию реверберации комнаты для звукового движка. 10 | 11 | > Вау! Не параграф получился, а просто готовая реклама для книги. 12 | 13 | Вместо этого мы уделим внимание коду _между_ всем этим. Не столько написанию кода, сколько его организации. В каждой программе есть своя **организация**, даже если это просто "давайте запихаем весь код в функцию `main()` и посмотрим, что получится". Поэтому я считаю что гораздо интереснее поговорить о том, как получается хорошая организация. Как отличить хорошую архитектуру от плохой? 14 | 15 | Я обдумывал этот вопрос не меньше пяти лет. Конечно, как и у каждого из вас, у меня есть интуитивное представление о хорошей архитектуре. Мы все страдаем от настолько плохой кодовой базы, что лучшее, что с ней можно сделать это немного разгрести ее и продолжить страдать дальше. 16 | 17 | > Давайте признаем что большинство из нас хотя бы в какой-то степени за это _отвечает_. 18 | 19 | У некоторых счастливчиков есть противоположный опыт: возможность работать с прекрасно спроектированным кодом. Такой тип кодовой базы ощущается как прекрасно меблированный отель с услужливыми консьержами, следящими за каждым вашим шагом. В чем же заключается разница между ними? 20 | 21 | ### Что такое _хорошая_ архитектура программы? {#что-такое-хорошая-архитектура-программы} 22 | 23 | Для меня хорошее проектирование заключается в том, что когда мне нужно внести изменение, вся остальная часть программы как будто специально сделана так чтобы мне было легко. Я могу добиться желаемого результата с помощью всего нескольких вызовов функций, делающихся так просто, что они не оставляют ни малейшей ряби на глади остального кода. 24 | 25 | Звучит прекрасно, но не слишком конкретно. "Пиши такой код, чтобы его изменения не порождали рябь на глади воды". Мда. 26 | 27 | Давайте немного углубимся в детали. Первая ключевая особенность _архитектуры - это приспособленность к изменениям_. Кому-то обязательно придется перерабатывать кодовую базу. Если никому больше к коду прикасаться не придется - по причине того что он совершенен, закончен или наоборот настолько ужасен, что никто не решится открыть его в своем редакторе - проектирование не важно. Проектирование оценивается по простоте внесения изменений. Без изменений это все равно что бегун, никогда не покидавший стартовой линии. 28 | 29 | ### Как вносить изменения? {#как-вносить-изменения} 30 | 31 | Прежде чем изменять код и добавлять новый функционал или исправлять ошибки или вообще запускать по какой-либо причине свой редактор, вам нужно иметь представление о том, что делает уже существующий код. Конечно, вам не нужно понимать всю программу целиком, но нужно по крайней мере загрузить в свой мозг примата все связанные части. 32 | 33 | > Странно об этом говорить, но фактически это Оптическое распознавание образов \(OCR\). 34 | 35 | Мы все склонны недооценивать важность этого шага, но зачастую он оказывается самой затратной в плане времени частью программирования. Если вы думаете, что вас тормозит сброс данных из памяти на диск, задумайтесь лучше о скорости работы вашего обезьяньего мозга с оптическими нервами. 36 | 37 | Как только вам удается загрузить всю нужную информацию в свою черепушку, вы немного думаете и выдаете решение. Конечно, вам приходится обдумывать некоторые детали, но, в целом, процесс достаточно прямолинеен. Как только вы понимаете проблему и часть кода, которую она затрагивает, сам процесс написания становится тривиальной задачей. 38 | 39 | Вы начинаете тыкать своими пальчиками в клавиатуру, пока на экране не появляются нужные вам черточки и на этом все, верно? А вот и нет! Прежде чем писать тесты и отсылать код на ревью, вам нужно кое-что подчистить. 40 | 41 | Вы засунули в игру еще немного кода, но не хотите чтобы следующий кто будет работать с этим кодом спотыкался о следы вашей деятельности. Если это не совсем мелкое изменение, вам нужно предпринять некоторые меры по реорганизации кода, чтобы новый код естественным образом вписывался в уже существующий. Если вы это сделаете, следующий, кто будет после вас работать с этим кодом, даже и не поймет, когда и какая часть кода была написана. 42 | 43 | > Я сказал "тесты"? А, да, сказал. Для многих частей кодовой базы игрового кода сложно написать юнит тесты, но некоторая его доля все таки отлично тестируется. 44 | > 45 | > Я не собираюсь лезть на трибуну, но все-таки призываю вас делать больше автоматизированных тестов. Неужели у вас нет более важных дел, чем тестировать одно и то же в ручном режиме? 46 | 47 | Упрощенно диаграмма потоков в программировании выглядит следующим образом: 48 | 49 | ![](/assets/1-1.1-1.png "Диаграмма потоков в программировании") 50 | 51 | > Меня даже несколько пугает что цикл на диаграмме не имеет выхода. 52 | 53 | ### Как нам может помочь уменьшение связности \(decoupling\)? {#как-нам-может-помочь-уменьшение-связности-decoupling} 54 | 55 | Хотя это и не очевидно, архитектура программы больше всего влияет на фазу изучения кода. Загрузка кода в нейроны настолько мучительно медленна, что стоит предпринимать любые стратегии для уменьшения его объема. В этой книге есть целый раздел, посвященный шаблонам [уменьшения связности \(decoupling\)](/shabloni-snizheniya-svyaznosti-decoupling-patterns.md) и большая часть книги _Паттерны проектирования_ посвящена той же идее. 56 | 57 | Уменьшение связности можно определять по-всякому, но лично я считаю два куска кода связанными, если я не могу понять как работает один кусок без понимания работы другого. Если уменьшить их _связность \(decouple\)_, каждый из них можно будет рассматривать независимо. И это прекрасно, потому что, если к решаемой вами проблеме имеет отношение только один кусок кода, вам не придется загружать в свой обезьяний мозг второй кусок. 58 | 59 | Для меня это является главной задачей архитектуры программы: **минимизация количества знаний, которые нужно поместить в свою черепушку прежде, чем двигаться дальше.** 60 | 61 | Последующие этапы, конечно, тоже вступают в игру. Еще одно определение уменьшения связности состоит в том, что _изменение_ одного куска кода не вызывает необходимость изменять другой. Нам обязательно придется что-то изменить, но чем меньше у нас связность, тем меньше частей игры это изменение затронет. 62 | 63 | ## Какой ценой? {#какой-ценой} 64 | 65 | Звучит здорово, верно? Избавимся от связности и начнем кодить со скоростью ветра. Каждое изменение будет затрагивать всего один или несколько методов и вы будете порхать над кодовой базой, практически не отбрасывая на нее тень. 66 | 67 | Именно благодаря этому чувству людей так привлекает абстрагирование, модульность, шаблоны проектирования и вообще архитектура программ. Программа с хорошей архитектурой превращает работу над собой в удовольствие, потому что все любят разработчиков с высокой производительностью. А хорошая архитектура дает _громадный_ прирост производительности. Тяжело переоценить получаемый на выходе эффект. 68 | 69 | Однако, как и все хорошее в жизни, ничего не дается бесплатно. Хорошая архитектура требует значительных усилий и дисциплины. Каждый раз, когда вы вносите изменения или добавляете новую функциональность, вам нужно прикладывать усилия к тому, чтобы эти изменения изящно интегрировались в остальную часть программы. Вам нужно приложить большие усилия к организации кода и _поддерживать_ эту организованность на протяжении тысяч маленьких изменений, которые предстоит совершить на протяжении всего цикла разработки. 70 | 71 | > Второй этап этого процесса - поддержка архитектуры - требует особого внимания. Я видел множество примеров того как программисты начинали за здравие с блестящим кодом и заканчивали за упокой, когда насыщали код тысячами хаков "чуть подправить здесь и готово". И так раз за разом. 72 | > 73 | > Также как в садоводстве здесь не достаточно просто посадить новые растения. Нужно еще бороться с сорняками и подстригать деревья. 74 | 75 | Вам нужно решить связность между каким частями программы вы хотите уменьшить и добавить необходимое абстрагирование. Кроме того вам нужно предусмотреть пути расширения функциональности чтобы было проще работать в будущем. 76 | 77 | Люди приходят от этого в восторг. Они представляют себе как разработчики будущего \(или они сами в будущем\) открывают кодовую базу и видят какая она вся понятная, мощная и так и просит себя расширить. Они представляют себе Один Игровой Движок Который Всем Повелевает. 78 | 79 | Но вот тут-то и кроется сложность. Добавляете ли вы новый уровень абстракции или предусматриваете место для расширения, вы должны _предполагать_ что эта гибкость понадобится вам в будущем. Вы добавляете код и усложняете игру, тратя при этом время на разработку, отладку и поддержку. 80 | 81 | Эти затраты с лихвой окупятся в том случае, если вы угадали и будете изменять код в этом направлении в дальнейшем. К сожалению предсказывать будущее довольно _сложно_ и если модульность вам в дальнейшем не понадобится, вскоре она начнет вам активно вредить. В конце концов вам просто придется работать с более громоздким кодом. 82 | 83 | > Какие-то умники даже придумали термин "YAGNI" - [Вам это не понадобится](https://ru.wikipedia.org/wiki/YAGNI) \(You aren’t gonna need it\) - специальную мантру, которая поможет вам бороться со злоупотреблениями в предположениях о том, что может понадобиться вам в будущем. 84 | 85 | Когда люди проявляют в этом чрезмерные усилия, в результате получается кодовая база, архитектура которой все больше выходит из-под контроля. У вас повсюду будут сплошные интерфейсы и абстракции. Системы плагинов, абстрактные базовые классы, изобилие виртуальных методов и куча точек для расширения. 86 | 87 | Потребуется вечность, чтобы прорваться через завалы всего этого богатства и добраться до настоящего кода, который хоть что-то делает. Конечно, если вам нужно внести какие-то изменения, у вас скорее сего найдется интерфейс, который вам поможет, но вы еще попробуйте его найти. В теории такое уменьшения связности означает, что вам нужно понимать меньше кода для того, чтобы его расширять, но само по себе нагромождение абстракций закончится тем, что кеш вашего мозга просто переполнится. 88 | 89 | Кодовые базы такого типа только _отталкивают_ людей от работы над архитектурой программ и от шаблонов проектирования в частности. Зарыться в код довольно просто, только не нужно забывать о том, что мы все-таки занимаемся созданием _игры_. Сладкие песни сирен про расширяемость поймали в свои сети множество игровых разработчиков, которые годами занимаются работой над "движком", даже не понимая какой _конкретно_ движок им нужен. 90 | 91 | ## Производительность и скорость {#производительность-и-скорость} 92 | 93 | Довольно часто увлечение архитектурой программы и абстракциями критикуют, особенно в игровом программировании за то, что это вредит производительности игры. Многие шаблоны, делающие ваш код более гибким используют виртуальную диспетчеризацию, интерфейсы, указатели, сообщения и другие механизмы, за которые приходится платить производительностью работы приложения. 94 | 95 | Такая критика имеет все основания. Зачастую архитектура программы предназначена для того чтобы сделать ее более гибкой. Для того чтобы ее было легче изменять. Это значит, что при кодинге вы допускаете меньше допущений. Вы используете интерфейсы для того чтобы можно было работать с _любыми_ классами их реализующими, вместо того чтобы ограничиться тем, что необходимо сегодня. Вы используете шаблоны [наблюдатель \(observer\)](/obzor-shablonov-proektirovaniya/nablyudatel-observer.md) и [сообщения \(messaging\)](/shabloni-snizheniya-svyaznosti-decoupling-patterns/ochered-sobitii-event-queue.md) для того чтобы между собой могли легко общаться не только два куска кода сегодня, но и три и четыре в будущем. 96 | 97 | Тем не менее производительность предполагает допущения. Искусство оптимизации основывается на введении конкретных ограничений. Можем ли мы предположить что у нас никогда не будет больше 256 противников? Отлично, значит для ID каждого из них достаточно всего одного байта. Будем ли мы вызывать здесь метод конкретного класса? Отлично, значит его можно вызвать статически или использовать inline вызов. Принадлежат ли все сущности к одному классу? Замечательно, значит мы можем организовать их в виде [непрерывного массива \(contiguous array\)](/shabloni-optimizatsii/lokalnost-dannih-data-locality.md). 98 | 99 | При этом не подразумевается вообще никакой гибкости! Мы можем быстро изменять игру, а для того чтобы сделать хорошую игру жизненно важна именно скорость _разработки_. Никто, даже Уилл Райт не способен создать сбалансированный игровой дизайн на бумаге. Необходимы итерации и эксперименты. 100 | 101 | > Еще один интересный контр-пример - это шаблоны в `С++`. Метапрограммирование на основе шаблонов зачастую позволяет организовать абстрактный интерфейс без ущерба для производительности. 102 | > 103 | > Гибкость здесь довольно высока. Когда вы пишете код вызова конкретного метода в некотором классе, вы фиксируете этот класс во время _написания_ - жестко вшиваете в код, какой класс вы вызываете. Когда же вы используете виртуальные методы или интерфейсы, вызываемый класс становится неизвестным до момента _выполнения_. Такой подход достаточно гибкий, но требует накладных расходов в плане производительности. 104 | > 105 | > Метапрограммирование на основе шаблонов представляет собой нечто среднее. Здесь вы принимаете решение о том какой класс вызывать на _этапе компиляции_, когда создается экземпляр шаблона. 106 | 107 | Чем быстрее вы сможете попробовать идею и увидеть как она играется, тем больше вы сможете попробовать и с большей долей вероятности придумаете что-то действительно стоящее. Даже после того, как правильная игровая механика найдена, потребуется еще куча времени на ее тюнинг. Даже небольшой дисбаланс способен похоронить всю игру. 108 | 109 | Ответ здесь простой. Делая свою программу боле гибкой, вы ускоряете процесс прототипирования, но жертвуете производительностью. С другой стороны, любая оптимизация кода делает его менее гибким. 110 | 111 | Мой собственный опыт показывает, что проще сделать интересную игру быстрой, чем сделать быструю игру интересной. Компромиссным решением здесь может быть принцип стараться делать код гибким до тех пор, пока дизайн игры более менее не устаканится, а затем избавиться от некоторых уровней абстракции в целях увеличения производительности. 112 | 113 | ## Чем хорош плохой код {#чем-хорош-плохой-код} 114 | 115 | Теперь мы переходим к следующему ключевому вопросу относительно стилей кодинга. Большая часть этой книги посвящена тому, как писать легко поддерживаемый чистый код, так что можно считать, что я сторонник "правильного" подхода. Однако неряшливый метод кодинга тоже не стоит забывать. 116 | 117 | Написание кода с хорошей архитектурой требует больше усилий и выливается в трату большего количества времени. Более того, поддержание кода с хорошей архитектурой на протяжении всей жизни проекта также требует много усилий. Вы должны обращаться со своей кодовой базой также, как порядочный турист, который, покидая стоянку, старается оставить ее в лучшем состоянии, чем нашел. 118 | 119 | Это замечательно в случае, если вам придется жить и работать с этим кодом на протяжении долгого времени. Но, как было сказано выше, _поддержание_ игрового дизайна требует проведения множества исследований и экспериментов. Особенно на ранних этапах, когда вы точно знаете что большую часть кода вы просто выкините. 120 | 121 | Если вы просто хотите найти наиболее удачное для геймплея решение, следить за красотой архитектуры бессмысленно, потому что так вы потеряете больше времени прежде, чем увидите результат на экране и получите обратную связь. Если то, что было сделано, не заработает, какой вам будет прок от того, что вы потратили столько времени на элегантный код, который вы _все равно_ выбрасываете. 122 | 123 | Прототипирование - это просто лепка кода в кучу, достаточно функционального для того чтобы геймдизайнер мог понять насколько идея хороша - т.е. совершенно легитимная в программировании практика. Здесь главное не забывать о самом важном принципе прототипирования. Если вы пишите код для выкидывания, вы обязаны его выкинуть. К сожалению я раз за разом вижу менеджеров, пренебрегающих этим правилом. 124 | 125 | > _Босс: "Слушай, есть идея которую нужно опопробовать. Сделай прототип по-быстренькому. Можешь сильно не стараться. Сколько времени тебе нужно?"_ 126 | > 127 | > _Разработчик: "Ну, если совсем по-быстрому, ничего не тестировать и не документировать и с кучей багов, то можно написать временный код за несколько дней."_ 128 | > 129 | > _Босс: "Отлично!"_ 130 | 131 | Через пару дней... 132 | 133 | > _Босс: "Слушай, прототип классный. Можешь за несколько дней его подлатать и мы возьмем его за основу?"_ 134 | 135 | Вам нужно быть уверенным что люди использующие код, написанный на выброс понимали бы, что даже если он выглядит рабочим, его _невозможно_ поддерживать и его обязательно _нужно_ переписать. Если есть хотя бы малейшая _возможность_ того, что его придется оставить, вам обязательно нужно, несмотря ни на что, писать его уже правильно. 136 | 137 | > Хорошим трюком можно признать привычку написания прототипа на другом языке программирования. Не том, на котором будет писаться игра. В этом случае вам обязательно придется переписать его прежде, чем он попадет в настоящую игру. 138 | 139 | ## Подведем итоги {#подведем-итоги} 140 | 141 | Как мы увидили в игру вступает несколько сил: 142 | 143 | 1. Нам нужна хорошая архитектура для того, чтобы легче было понимать код во время цикла разработки проекта. 144 | 2. Нам нужна хорошая производительность. 145 | 3. Нам нужно иметь возможность быстро внедрять новую функциональность. 146 | 147 | > Я нахожу даже забавным тот факт, что в любом случае нам нужно думать о скорости: скорости разработки в долгосрочной перспективе, скорости работы игры, скорости разработки в короткосрочной перспективе. 148 | 149 | Между этими целями наблюдаются некоторые противоречия. Хорошая архитектура ускоряет производительность труда в длительной перспективе, но ее поддержание требует затрат дополнительных усилий после каждого изменения. 150 | 151 | Быстрее всего написанная реализация совсем не обязательно самая быстрая в плане _производительности_. Наоборот, оптимизация требует дополнительного времени разработки. И как только она выполнена, кодовая база сразу начинает костенеть: высокооптимизированный код крайне негибок и его очень сложно менять. 152 | 153 | Всегда существует соблазн закончить сегодняшнюю работу сегодня, а завтра заняться чем-то еще. Но, если добавлять функциональность так быстро, насколько это возможно, кодовая база быстро превратится в месиво хаков, багов и противоречий, которые замедлят нашу продуктивность в будущем. 154 | 155 | Здесь нет простого ответа, возможны лишь компромиссы. Судя по почте, которую я получаю, многих людей это просто обескураживает. Особенно новичков, которые просто хотят сделать игру. Согласитесь, звучит пугающе, когда слышишь: "Правильного ответа не существует, есть только разные варианты неправильных". 156 | 157 | Но на мой взгляд это просто замечательно! Посмотрите на любую другую область человеческой деятельности и, скорее всего, вы увидите в основе набор непреложных истин. В конце концов, если бы существовал простой ответ, все бы только так и делали. Область, в которой мастером можно стать за неделю просто скучна. Вы никогда не услышите потрясающих карьерных историй от копателя канав. 158 | 159 | > А может быть и можно. Я не особо задумывался об этой аналогии. Всегда найдутся энтузиасты, которые копают так глубоко, насколько это только возможно. Даже целые субкультуры организуют. Кто я такой чтобы об этом судить? 160 | 161 | На мой взгляд все это очень похоже на сами игры. В игре наподобие шахмат никогда нельзя стать непревзойденным мастером потому что все части игры отлично сбалансированы. Это значит что вы можете потратить целую жизнь на перебор всех возможных стратегий. Игра с плохим дизайном наоборот очень быстро скатывается к одной выигрышной тактике, которой начинает придерживаться игрок пока она ему не надоест. 162 | 163 | ## Упрощение {#упрощение} 164 | 165 | Гораздо позднее я открыл для себя еще один метод, смягчающий эти ограничения - _упрощение_. Сейчас я в своем коде стараюсь писать чистое, максимально прямолинейное решение проблемы. Это такой тип кода, после прочтения которого у вас не остается ни малейших сомнений относительно того, что именно он делает и вы не можете представить никакого другого решения. 166 | 167 | Я стараюсь выбрать правильные структуры данных и алгоритмы \(именно в такой очередности\) и в дальнейшем от них отталкиваюсь. При этом я заметил, что чем проще решение - тем меньше кода на выходе. А это значит, что и свою голову мне приходится забивать меньшим количеством кода, когда приходит время его менять. 168 | 169 | Зачастую код получается быстрым, потому что не требует слишком больших накладных расходов и сам объем кода невелик. \(Конечно, это вовсе не правило. Даже в совсем маленький участок кода можно поместить кучу циклов и рекурсий.\) 170 | 171 | Однако, обратите внимание, что я не говорю, что написание простого кода требует мало времени. Вы могли бы так предположить, потому что в результате кода будет совсем немного, однако хорошее решение - это не просто разрастание кода - это его _дистиллят_. 172 | 173 | > Блез Паскаль закончил свое знаменитое письмо следующими словами "Я хотел написать письмо покороче, но мне не хватило времени". 174 | > 175 | > Еще одну интересную мысль можно найти у Антуана де Сент-Экзюпери "Совершенство достижимо, но не тогда, когда уже нечего добавить, а когда уже нечего убавить". 176 | > 177 | > И еще один пример ближе к телу. Каждый раз когда я просматривал главы этой книги, они становились все короче и короче. Некоторые главы потеряли до 20% объема. 178 | 179 | Мы редко сталкиваемся с элегантными проблемами. Вместо этого у нас обычно есть набор вариантов использования. Нам нужно заставить X делать Y когда выполняется условие Z, а W когда выполняется A и т.д. Другими словами все, что у нас есть - это длинный список примеров поведения. 180 | 181 | Решение, требующее меньше всего мыслительных усилий - это просто закодировать все эти условия по отдельности. Если вы посмотрите на новичков - они зачастую именно так и делают: они разбиваю решение на большое дерево отдельных случаев. 182 | 183 | Никакой элегантности здесь конечно нет и код, написанный в таком стиле имеет тенденцию падать при входных данных, хоть немного отличающихся от тех, на которые рассчитывал программист. Когда мы говорим об элегантном решении, мы чаще всего имеем в виду _обобщенное_ решение: небольшой логический блок, который покрывает большую область вариантов использования. 184 | 185 | Поиск такого блока похож на подбор нужного шаблона или разгадывание паззла. Требуются большие усилия чтобы увидеть сквозь разрозненное множество примеров вариантов использования скрытую закономерность, объединяющую их все. И какое же это замечательное чувство, когда разгадка находится. 186 | 187 | ## Просто смиритесь {#просто-смиритесь} 188 | 189 | Большинство людей предпочитают пропускать вступление, так что я поздравляю вас с тем, что вы его одолели. Мне нечем отблагодарить вас за это, кроме нескольких советов, которые я надеюсь будут вам полезны: 190 | 191 | * Абстрагирование и уменьшение связности позволяет вашей программе эволюционировать быстрее, но не увлекайтесь этим если не уверены в том, что данный код требует гибкости. 192 | 193 | * Во время разработки помните и про дизайн и про производительность, только откладывайте по возможности всяческие тонкие оптимизации на самый конец цикла разработки. 194 | 195 | > Поверьте мне, два месяца до даты релиза - это не тот срок когда нужно, наконец, приступать к решению проблемы "игра работает, но выдает только 1 FPS ". 196 | 197 | * Старайтесь исследовать поле дизайнерских решений быстрее, но не настолько, чтобы оставлять после себя месиво в коде. В конце концов, вам ведь еще с этим кодом жить. 198 | 199 | * Если собираетесь выбросить код, не тратьте много времени на его совершенствование. Рок звезды ведь именно потому так часто устраивают погромы в номерах отелей, что знают о том, что на следующий день оттуда уедут. 200 | 201 | * И самое главное - **если хотите сделать что-то интересное, получайте удовольствие от процесса.** 202 | -------------------------------------------------------------------------------- /povedencheskie-shabloni-behavioral-patterns/podklass-pesochnitsa-subclass-sandbox.md: -------------------------------------------------------------------------------- 1 | # Подкласс песочница \(Subclass Sandbox\) {#подкласс-песочница-subclass-sandbox} 2 | 3 | ## Задача 4 | 5 | _Определение поведения в подклассе с помощью набора операций, предоставляемых базовым классом._ 6 | 7 | ## Мотивация 8 | 9 | 10 | Каждый мальчишка хотел в детстве быть супергероем, но к сожалению с космическими лучами у нас на Земле не густо. Игры помогают хотя бы немного почувствовать себя супергероем. Так как никто из наших дизайнеров так и не научился говорить "нет", в каждой игре планируются дюжины, если не сотни различных суперспособностей для героев. 11 | 12 | Наш план заключается в том чтобы иметь базовый класс `Superpower`. Далее мы создаем класс наследник для каждой суперсилы. Делим дизайн документ между программистами поровну и начинаем кодить. Когда все закончили у нас появилась сотня классов суперспособностей. 13 | 14 | Мы хотим поразить нашего игрока разнообразием нашего мира. Мы хотим чтобы в игре была каждая способность, о которой он только мог мечтать в детстве. Это значит что эти подклассы суперсил могут делать практически все: проигрывать звуки, порождать визуальные эффекты, взаимодействовать с AI, создавать и уничтожать другие игровые сущности и вмешиваться в работу физики. Нет ни одного уголка кодобазы, до которого они не могли бы добраться. 15 | 16 | > Когда вы видите что у вас образуется слишком много подклассов, как в этом примере, это обычно свидетельствует о том что лучше применить подход управления через данные (data-driven approach). Вместо того чтобы использовать огромное количество кода для определения различных суперсил, попробуйте определить их поведение с помощью данных. 17 | > 18 | > В этом вам могут помочь шаблоны типа [Объект тип (Type Object)](obekt-tip-type-object.md), [Байткод (Bytecode)](baitkod-bytecode.md) или [Интерпретатор (Interpreter) GOF](https://ru.wikipedia.org/wiki/Interpreter). 19 | 20 | 21 | 22 | Предположим что мы дадим нашей команде волю и позволим заняться написанием классов суперсил. Что в этом случае произойдет? 23 | 24 | 25 | - _У нас будет куча избыточного кода_. Хоть разные суперсилы и различаются между собой довольно сильно, их действие все равно частично перекрывает друг друга. Многие из них будут проигрывать звуки и запускать визуальные эффекты похожими способами. Замораживающий луч, испепеляющий луч и луч горчицы Джинна - довольно похожи если рассмотреть их подробнее. Если реализующие все это люди не будут между собой взаимодействовать, они потратят кучу лишнего времени и сгенерируют кучу дублирующего кода. 26 | 27 | - _Каждый уголок кода будет связан с этими классами_. Не обладая достаточными знаниями, люди будут писать код, вызывающий подсистемы, которые изначально и не предполагалось связывать с классами суперсил. Если наш рендер организован в виде нескольких хитрых слоев, только один из которых предполагается взаимодействующим с кодом за пределами графического движка, мы можем обнаружить что некоторые суперсилы будут обращаться к каждому из его слоев. 28 | 29 | - _Когда эти внешние системы потребуется изменить, связанные с ними суперсилы внезапно могут начать работать неправильно_. Как только мы начинаем связывать различные классы суперсил с самыми разными частями нашего движка, не стоит удивляться потом, что их изменение будет влиять на работу классов суперсил. И это совсем не весело, потому что ваши программисты графики, аудио и пользовательского интерфейса совсем не хотят быть еще и программистами геймплея. 30 | 31 | - _Сложно определить инварианты, которым подчиняются все суперсилы_. Предположим что мы хотим чтобы все звуки, проигрываемые нашими суперсилами попадали в очередь с правильными приоритетами. Если каждый из сотни ваших классов будет самостоятельно обращаться к звуковой системе, сделать это будет довольно проблематично. 32 | 33 | 34 | Чего мы на самом деле хотим - так это возможность выдать каждому программисту геймплея набор примитивов, из которых он сможет конструировать суперсилы. Хотите чтобы суперсила проиграла звук? Вот вам функция `playSound()`. Хотите частиц? Вот `spawnParticles()`. Нам нужно удостовериться что эти операции покрывают все что вам нужно и вам не нужно будет беспорядочно прописывать `#include` для заголовков и совать нос во все уголки остальной кодобазы. 35 | 36 | Мы добьемся этого сделав эти операции _защищенными методами (protected methods) базового класса_ `Superpower`. То что мы поместили их все прямо в базовый класс, означает что у всех классов наследников будет простой к ним доступ. Объявление их защищенными (т.е. не виртуальными) говорит о том что они предназначены для того чтобы _вызываться_ только из классов наследников. 37 | 38 | Теперь когда у нас есть игрушки для игры, нам нужно место где можно с ними играть. Специально для этого определим _метод песочницу (sandbox method)_: абстрактный защищенный метод, который должны реализовывать подклассы. Таким образом для реализации новой силы нам нужно: 39 | 40 | 1. Создать новый класс, унаследованный от `Superpower`. 41 | 42 | 1. Переопределить метод песочницу `activate()`. 43 | 44 | 1. Реализовать его тело с помощью вызовов методов, предоставляемых классом `Superpower`. 45 | 46 | Проблему избыточности кода мы можем решить, сделав эти операции как можно больше высокоуровневыми. Каждый раз когда мы видим код, дублирующийся в нескольких подклассах, мы всегда можем поместить его в `Superpower` в качестве новой операции, которую смогут использовать подклассы. 47 | 48 | С проблемой излишней связности мы боремся, сосредотачивая всю связность в одном месте. `Superpower` в результате сама будет связана с самыми разными системами игры, а вот сотни унаследованных классов - нет. Вместо этого они будут связаны _только_ со своим базовым классом. Когда одна из систем игры изменится, нам придется изменить и `Superpower`, а вот к десяткам подклассов можно будет не притрагиваться. 49 | 50 | Этот шаблон подводит нас к архитектуре с неглубокой, но широкой иерархией классов. Цепочка экземпляров _неглубокая_, но у нас есть просто уйма классов, завязанных на `Superpower`. Имея один класс со множеством прямых подклассов, мы получаем в нашей кодовой базе точку приложения усилий. Время и усилия, затраченные на `Superpower`, окупятся при создании широкого набора классов в игре. 51 | 52 | > В будущем вы наверняка встретите еще множество людей, критикующих наследование в объектно-ориентированных языках. Наследование сулит проблемы - не существует связывания сильнее в кодобазе, чем между базовым классом и подклассом. При этом работать проще с _широким_, а не _глубоким_ деревом наследования. 53 | 54 | 55 | ## Шаблон 56 | 57 | **Базовый класс** определяет абстрактный **метод песочницу** и несколько **предоставляемых операций (provided operations)**. Объявление их защищенными явно означает что они предназначены только для использования классами наследниками. Каждый унаследованный **подкласс песочницы** реализует метод песочницы с помощью предоставляемых операций. 58 | 59 | ## Когда использовать 60 | 61 | 62 | Все очень просто. Шаблон легко найти во множестве кодовых баз, даже за пределами игровой индустрии. Если у вас часто встречаются невиртуальные защищенные методы, вы возможно уже используете его подобие. Подкласс песочница следует использовать когда: 63 | 64 | - У вас есть базовый класс и множество дочерних. 65 | 66 | - Базовый класс способен реализовывать все операции, которые нужны для работы дочерним. 67 | 68 | - В поведении подклассов наблюдается много совпадений и вы хотели бы упростить кодовую базу за счет повторного использования кода. 69 | 70 | - Вы хотите минимизировать связность между этими дочерними классами и остальной программой. 71 | 72 | 73 | ## Имейте в виду 74 | 75 | 76 | "Наследование" во многих программистских кругах сегодня стало чуть ли не ругательством и одна из причин заключается в том что базовые классы имеют тенденцию обрастать все большим и большим количеством кода. И этот шаблон как никакой другой подвержен этой тенденции. 77 | 78 | Так как подклассам приходится общаться с остальной игрой через базовый класс, базовый класс оказывается связанным со всеми системами, с которыми вынужден общаться хотя бы один его дочерний класс. Конечно подклассы настолько же сильно связаны со своим базовым классом. Эта паутина связей не даст вам легко изменить кодовую базу без риска что-либо разрушить - классическая проблема [хрупкости базового класса](https://ru.wikipedia.org/wiki/%D0%A5%D1%80%D1%83%D0%BF%D0%BA%D0%B8%D0%B9_%D0%B1%D0%B0%D0%B7%D0%BE%D0%B2%D1%8B%D0%B9_%D0%BA%D0%BB%D0%B0%D1%81%D1%81). 79 | 80 | Обратной стороной монеты является то что связывание сосредоточено на базовом классе, а классы наследники гораздо более явным образом отделены от остального мира. В идеале основная часть вашего поведения будет сосредоточена в этих подклассах. А это значит что большая часть вашей кодобазы изолирована и ее легче поддерживать. 81 | 82 | Так что если вы видите что ваша кодовая база превращается в гигантскую миску тушенки, попробуйте выделить часть предоставляемых операций в отдельные классы, с которыми базовый класс сможет частично разделить ответственность. В этом вам поможет шаблон [Компонент](../shabloni-snizheniya-svyaznosti-decoupling-patterns/komponent-component.md). 83 | 84 | ## Пример кода 85 | 86 | Так как этот шаблон довольно прост, примеров кода не будет слишком много. Это не значит что он бесполезен. Этот шаблон о _намерении_, а не о сложности реализации. 87 | 88 | Начнем с базового класса `Superpower`: 89 | 90 | ```cpp 91 | class Superpower 92 | { 93 | public: 94 | virtual ~Superpower() {} 95 | 96 | protected: 97 | virtual void activate() = 0; 98 | 99 | void move(double x, double y, double z) 100 | { 101 | // Здесь код... 102 | } 103 | 104 | void playSound(SoundId sound, double volume) 105 | { 106 | // Здесь код... 107 | } 108 | 109 | void spawnParticles(ParticleType type, int count) 110 | { 111 | // Здесь код... 112 | } 113 | }; 114 | ``` 115 | 116 | Метод `activate()` - это метод песочница. Так как он виртуальный и абстрактный, подклассы должны его переопределять. Таким образом это будет очевидно для тех кто будет работать над нашими классами сил. 117 | 118 | Остальные защищенные методы `move()`, `playSound()` и `spawnParticles()` - это предоставляемые операции. Это именно их подклассы будут вызывать в своей реализации `activate()`. 119 | 120 | Мы не реализуем предоставляемые операции в этом примере, но в настоящей игре здесь был бы реальный код. Именно в этих методах будет проявляться связность `Superpower` с остальными частями игры: `move()` работает с физическим кодом, `playSound()` общается с аудио движком, и т.д. Так как это все находится в _реализации_ базового класса, вся связность инкапсулируется внутри самого Superpower. 121 | 122 | А теперь выпускаем наших радиоактивных пауков и получаем суперсилу. Вот она: 123 | 124 | > Ну ладно. Возможно способность прыгать - это не слишком _супер_. Я просто не хочу слишком переусложнять пример. 125 | 126 | ```cpp 127 | class SkyLaunch : public Superpower 128 | { 129 | protected: 130 | virtual void activate() 131 | { 132 | // Взмываем в небо. 133 | playSound(SOUND_SPROING, 1.0f); 134 | spawnParticles(PARTICLE_DUST, 10); 135 | move(0, 0, 20); 136 | } 137 | }; 138 | ``` 139 | 140 | Эта сила подбрасывает супергероя в воздух, проигрывает сопроводительный звук и порождает облачко пыли. Если бы все суперсилы были такими простыми - просто комбинация звука, эффекта с частицами и движения - нам бы вообще шаблон не понадобился. Вместо этого `Superpower` мог бы просто содержать готовую реализацию `activate()`, получающую доступ к полям ID звука, типу частиц и движения. Но это могло бы сработать если бы силы работали одинаково, но просто с разными данными. Доработаем его немного: 141 | 142 | ```cpp 143 | class Superpower 144 | { 145 | protected: 146 | double getHeroX() 147 | { 148 | // Здесь код... 149 | } 150 | 151 | double getHeroY() 152 | { 153 | // Здесь код... 154 | } 155 | 156 | double getHeroZ() 157 | { 158 | // Здесь код... 159 | } 160 | 161 | // Остальное... 162 | }; 163 | ``` 164 | 165 | Здесь мы добавляем несколько методов для получения позиции игрока. Теперь наш подкласс `SkyLaunch` может их использовать: 166 | 167 | ```cpp 168 | class SkyLaunch : public Superpower 169 | { 170 | protected: 171 | virtual void activate() 172 | { 173 | if (getHeroZ() == 0) 174 | { 175 | // Мы на земле, значит можем прыгать. 176 | playSound(SOUND_SPROING, 1.0f); 177 | spawnParticles(PARTICLE_DUST, 10); 178 | move(0, 0, 20); 179 | } 180 | else if (getHeroZ() < 10.0f) 181 | { 182 | // Невысоко над землей, значит можем делать двойной прыжок. 183 | playSound(SOUND_SWOOP, 1.0f); 184 | move(0, 0, getHeroZ() - 20); 185 | } 186 | else 187 | { 188 | // Находимся в воздухе и можем выполнить подкат. 189 | playSound(SOUND_DIVE, 0.7f); 190 | spawnParticles(PARTICLE_SPARKLES, 1); 191 | move(0, 0, -getHeroZ()); 192 | } 193 | } 194 | }; 195 | ``` 196 | 197 | Так как у нас появился доступ к части состояния, теперь наш метод песочница может эффективнее управлять потоком выполнения. Всего несколько простых выражений if и вы можете реализовать все что захотите. Когда у вас в качестве метода песочницы будет полноценный метод с необходимым кодом, вас только небо остановит. 198 | 199 | > Ранее я предлагал применить для суперсил подход с описанием с помощью данных (data-driven approach). А вот и причина почему не стоит этого делать. Если ваше поведение достаточно сложное и императивное, его сложнее будет задавать с помощью данных. 200 | 201 | 202 | ## Архитектурные решения 203 | 204 | Как вы видите это довольно "мягкий" шаблон. Он описывает базовую идею, но не слишком акцентируется на деталях механики. Это значит что каждый раз, когда вы его применяете, вы можете делать интересные решения. Вот над чем стоит поразмыслить. 205 | 206 | ### Какие операции нужно предоставить? 207 | 208 | Это самый большой вопрос. От него зависит насколько шаблоном будет удобно пользоваться и насколько он будет полезен. В самом минималистичном варианте, базовый класс вообще не предоставляет никаких операций. Все что у него есть - это метод песочница. Чтобы его реализовать вам нужно вызывать системы за пределами базового класса. Если вы выберете такую тактику поведения, можно сказать что вы вообще не используете сам шаблон. 209 | 210 | Другая крайность - это базовый класс, предоставляющий _любые_ операции, которые могут понадобиться подклассам. Подклассы привязаны _только_ к базовому классу и вообще не общаются с внешними системами. 211 | 212 | > В частности это значит что в исходнике каждого такого подкласса будет только один `#include`, подключающий базовый класс. 213 | 214 | Между этими двумя крайностями лежит некое среднее решение, когда часть операций предоставляется базовым классом, а остальные используются из других систем напрямую. Чем больше операций вы представляете, тем меньше у вас связности подклассов со внешним системами, но зато _больше_ связность с базовым классом. Мы снижаем связность у классов наследников, но увеличиваем ее у самого базового класса. 215 | 216 | Это очень выгодно, когда у вас есть множество классов наследников, связанных со внешними системами. Перенося ее в предоставляемые операции, вы концентрируете связность в одном месте: в базовом классе. И чем чаще вы это делаете, тем больше и сложнее для поддержки становится базовый класс. 217 | 218 | Так где же провести черту? Вот несколько основных правил: 219 | 220 | - Если предоставляемые операции используются только несколькими подклассами, вы не получите большого выхлопа за свои вложения. Вы увеличите сложность базового класса, которая скажется на всем прочем, но от этого снизится связность всего нескольких наследников. 221 | 222 | Так стоит делать, если эти операции пересекаются с уже существующими. Но возможно проще и очевиднее будет просто позволить подклассам обратиться к внешним системам напрямую. 223 | 224 | - Когда вы вызываете метод в каком-либо другом месте игры, лучше если этот метод не изменяет никакого состояния. Связность все равно увеличивается, но это "безопасная" связность, потому что она ничего в игре не ломает. 225 | 226 | Вызовы, которые меняют состояние в свою очередь гораздо сильнее связывают части вашей кодобазы и вам сложнее будет анализировать такие связи. Этот факт делает их хорошими кандидатами на включение в список предоставляемых операций в более видимый для анализа базовый класс. 227 | 228 | > Я не зря беру слово"безопасность" в кавычки, потому что технически даже получение данных может добавить вам проблем. Если ваша игра многопоточная, вы можете пытаться читать какое-то значение в то время когда оно изменяется. И если не будете достаточно осторожны, у вас окажутся некорректные данные. 229 | > 230 | > Еще один хитрый случай - это когда состояние вашей игры строго детерминировано (что практикуют многие онлайновые игры для сохранения синхронизации между игроками). Если вы получаете доступ к чему либо за пределами синхронизированного состояния игры, у вас могут образоваться крайне опасные недетерминированные баги. 231 | 232 | - Если реализация предоставляемых операций сводится просто к вызову какой-либо внешней системы - большой пользы она не несет. В этом случае может быть проще просто вызвать внешнюю систему напрямую. 233 | 234 | Однако даже простейшее перенаправление может быть полезным: такие методы зачастую обращаются к состояниям, которые нежелательно напрямую видеть классам наследникам. Предположим что `Superpower` предоставляет такую операцию: 235 | 236 | ```cpp 237 | void playSound(SoundId sound, double volume) 238 | { 239 | soundEngine_.play(sound, volume); 240 | } 241 | ``` 242 | 243 | Это просто обращение к одному из полей `soundEngine_` из `Superpower`. Выигрыш здесь в том что поле осталось инкапсулированным в `Superpower` и подклассы его не видят. 244 | 245 | 246 | ### Следует ли предоставлять методы напрямую или через содержащий их объект? 247 | 248 | 249 | Сложность этого шаблона заключается в том что в результате у вас образуется огромное количество методов, сосредоточенное в одном базовом классе. Этого можно избежать, переместив часть методов в отдельные классы. А предоставляемые операции будут просто возвращать один из этих объектов. 250 | 251 | Например, чтобы позволить силе проигрывать звук, мы можем добавить такую возможность прямо в `Superpower`: 252 | 253 | ```cpp 254 | class Superpower 255 | { 256 | protected: 257 | void playSound(SoundId sound, double volume) 258 | { 259 | // Здесь код... 260 | } 261 | 262 | void stopSound(SoundId sound) 263 | { 264 | // Здесь код... 265 | } 266 | 267 | void setVolume(SoundId sound) 268 | { 269 | // Здесь код... 270 | } 271 | 272 | // Метод песочница и другие операции... 273 | }; 274 | ``` 275 | 276 | Но у нас ведь и так слишком много всего в `Superpower`, а нам хотелось бы этого избежать. Поэтому мы сделаем отдельный класс `SoundPlayer` и перенесем эту функциональность в него: 277 | 278 | ```cpp 279 | class SoundPlayer 280 | { 281 | void playSound(SoundId sound, double volume) 282 | { 283 | // Здесь код... 284 | } 285 | 286 | void stopSound(SoundId sound) 287 | { 288 | // Здесь код... 289 | } 290 | 291 | void setVolume(SoundId sound) 292 | { 293 | // Здесь код... 294 | } 295 | }; 296 | ``` 297 | 298 | А теперь `Superpower` будет просто предоставлять доступ к этому классу: 299 | 300 | ```cpp 301 | class Superpower 302 | { 303 | protected: 304 | SoundPlayer& getSoundPlayer() 305 | { 306 | return soundPlayer_; 307 | } 308 | 309 | // Метод песочница и другие операции... 310 | 311 | private: 312 | SoundPlayer soundPlayer_; 313 | }; 314 | ``` 315 | 316 | Подобный перенос предоставляемых операций во вспомогательные классы имеет следующие преимущества: 317 | 318 | - _Уменьшается количество методов в базовом классе_. В нашем примере мы избавились от трех методов за счет одного получателя класса (getter). 319 | 320 | - _Код во вспомогательном классе обычно легче поддерживать_. Ключевые базовые классы типа `Superpower`, несмотря на наши лучшие намерения, может быть сложно изменять, потому что от них слишком много всего зависит. Перенос функциональности в другой менее связанный дополнительный класс, упрощает ее изменение без ущерба для других вещей. 321 | 322 | - _Снижается связность между базовым классом и остальными системами_. Когда метод `playSound()` находился прямо в `Superpower`, это значило что наш класс был напрямую связан с `SoundId` и остальным аудио кодом, вызываемым реализацией. Перенос всего этого в `SoundPlayer` снижает связность `Superpower` до одного класса `SoundPlayer`, в котором теперь сосредоточены все остальные зависимости. 323 | 324 | 325 | ### Как базовый класс будет получать нужно ему состояние? 326 | 327 | Вашему базовому классу часто придется получать данные, которые он хочет инкапсулировать и держать невидимыми для своих подклассов. В нашем первом примере, класс `Superpower` предоставлял метод `spawnParticles()`. Если для его реализации нужен объект системы частиц, то как нам его получить? 328 | 329 | - **Передаем в конструктор базового класса:** 330 | 331 | Проще всего передать его в качестве аргумента конструктора базового класса: 332 | 333 | ```cpp 334 | class Superpower 335 | { 336 | public: 337 | Superpower(ParticleSystem* particles) 338 | : particles_(particles) 339 | {} 340 | 341 | // Метод песочница и другие операции... 342 | 343 | private: 344 | ParticleSystem* particles_; 345 | }; 346 | ``` 347 | 348 | В этом случае мы можем быть уверенными, что у каждой суперсилы будет возможность воспользоваться эффектами сразу после создания. Но давайте посмотрим на класс наследник: 349 | 350 | ```cpp 351 | class SkyLaunch : public Superpower 352 | { 353 | public: 354 | SkyLaunch(ParticleSystem* particles) 355 | : Superpower(particles) 356 | {} 357 | }; 358 | ``` 359 | 360 | Здесь видна очевидная проблема. Каждому классу наследнику придется иметь конструктор, вызывающий конструктор базового и передающий в него этот аргумент. А это значит что каждый класс наследник буде частью состояния, о котором мы хотим чтобы он вообще не знал. 361 | 362 | Для поддержки это тоже сплошная головная боль. Если позже мы захотим добавить еще одну часть состояния в базовый класс, нам придется изменить и все конструкторы всех унаследованных от него классов. 363 | 364 | - **Выполняем двухшаговую инициализацию:** 365 | 366 | Чтобы не передавать все через конструктор, мы можем разделить инициализацию на два шага. Конструктор не будет принимать никаких параметров и просто создает объект. После этого мы вызываем отдельный метод, объявленный прямо в базовом классе и передаем ему оставшуюся часть необходимых ему данных. 367 | 368 | ```cpp 369 | Superpower* power = new SkyLaunch(); 370 | power->init(particles); 371 | ``` 372 | 373 | Обратите внимание, что раз мы ничего не передаем в конструктор `SkyLaunch`, он не связан ни с чем, что мы хотели бы оставить личным (private) в `Superpower`. Проблема с этим подходом в том, что вам всегда нужно помнить о необходимости вызова `init()`. Если вы когда-нибудь об этом забудете, у вас будет сила, застрявшая в некоем полусозданном состоянии и ничего не делающая. 374 | 375 | Чтобы исправить это мы можем инкапсулировать весь процесс внутри одной функции следующим образом: 376 | 377 | ```cpp 378 | Superpower* createSkyLaunch(ParticleSystem* particles) 379 | { 380 | Superpower* power = new SkyLaunch(); 381 | power->init(particles); 382 | return power; 383 | } 384 | ``` 385 | 386 | > Использовав здесь трюк с приватным конструктором и дружественным классом, вы можете быть уверены что функция `createSkylaunch()` является _единственным_ способом создания силы. Таким образом вы никогда не забудете ни об одном этапе инициализации. 387 | 388 | - **Сделаем состояние статичным:** 389 | 390 | В предыдущем примере мы инициализировали каждый _экземпляр_ `Superpower` системой частиц. Это имеет смысл, если каждая сила нуждается в собственном уникальном состоянии. Но что если наша система частиц реализована как [Синглтон (Singleton)](../obzor-shablonov-proektirovaniya/singlton-singleton.md) и используется всеми силами совместно. 391 | 392 | В этом случае мы можем сделать состояние приватным для базового класса и даже _статичным_. Игра все равно будет проверять инициализацию состояния, но _класс_ `Superpower` придется инициализировать только один раз, а не для каждого экземпляра. 393 | 394 | > Имейте в виду, что при этом у нас появляется множество проблем из-за синглтона: единое состояние оказывается общим для большого множества объектов (всех экземпляров `Superpower`). Система частиц инкапсулирована и не _видна_ глобально, что есть хорошо, но она все равно усложняет понимание работы суперсил, потому что они все работают с одним и тем же объектом. 395 | 396 | ```cpp 397 | class Superpower 398 | { 399 | public: 400 | static void init(ParticleSystem* particles) 401 | { 402 | particles_ = particles; 403 | } 404 | 405 | // Метод песочница и другие операции... 406 | 407 | private: 408 | static ParticleSystem* particles_; 409 | }; 410 | ``` 411 | 412 | Обратите внимание что здесь статичны и `init()` и `particles_`. Пока игра вызывает `Superpower::init()` перед всем остальным, каждая сила сможет получить доступ к системе частиц. В тоже время экземпляры `Superpower` можно свободно создавать просто вызывая конструктор класса наследника. 413 | 414 | Что еще лучше, теперь `particles_` является статической переменной и нам не нужно сохранять ее в каждом экземпляре `Superpower`, так что наш класс будет расходовать меньше памяти. 415 | 416 | - **Использование поиска службы(service locator):** 417 | 418 | Предыдущий вариант требовал чтобы внешний код обязательно не забывал о том, чтобы передать состояние в базовый класс, прежде чем его можно будет использовать. Таким образом на окружающий код налагаются определенные обязанности. Еще как вариант можно позволить базовому классу обрабатывать это, получая нужное состояние самостоятельно. Для этого можно использовать [поиск службы (Service Locator)](../shabloni-snizheniya-svyaznosti-decoupling-patterns/poisk-sluzhbi-service-locator.md). 419 | 420 | ```cpp 421 | class Superpower 422 | { 423 | protected: 424 | void spawnParticles(ParticleType type, int count) 425 | { 426 | ParticleSystem& particles = ServiceLocator::getParticles(); 427 | particles.spawn(type, count); 428 | } 429 | 430 | // Метод песочница и другие операции... 431 | }; 432 | ``` 433 | 434 | Здесь для `spawnParticles()` нам нужна система частиц. Вместо того чтобы нам ее _давали_ из внешнего кода, мы сами получаем ее через поиск службы. 435 | 436 | 437 | ## Смотрите также 438 | 439 | 440 | - Когда вы применяете шаблон [Метод обновления (Update Method)](../posledovatelnie-shabloni-sequencing-patterns/metodi-obnovleniya-update-methods.md), ваш метод обновления часто будет представлять из себя и метод песочницу. 441 | 442 | - Роль этого шаблона сходна с ролью шаблона [Метод шаблон (Template Method)GoF](https://ru.wikipedia.org/wiki/%D0%A8%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BC%D0%B5%D1%82%D0%BE%D0%B4_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)). В обеих шаблонах вы реализуете метод с помощью набора примитивных операций. В Методе песочнице, метод находится в шаблоне наследнике, а операции примитивы в базовом классе. А в Методе шаблоне, метод содержится в базовом классе, а примитивы операций реализуются в классах _наследниках_. 443 | 444 | - Также этот шаблон можно рассматривать как вариацию шаблона [Фасад (Facade)GoF](https://ru.wikipedia.org/wiki/%D0%A4%D0%B0%D1%81%D0%B0%D0%B4_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)). Этот шаблон скрывает несколько различных систем за единым упрощенным API. В Подклассе песочнице базовый класс работает как фасад, скрывающий весь движок от подклассов. -------------------------------------------------------------------------------- /obzor-shablonov-proektirovaniya/prototip-prototype.md: -------------------------------------------------------------------------------- 1 | # Прототип \(Prototype\) 2 | 3 | Первый раз я узнал о существовании слова "прототип" из _Паттернов проектирования_. Сейчас это слово достаточно популярно. Но обычно его используют без привязки к [шаблону проектирования GOF](http://ru.wikipedia.org/wiki/Prototype_pattern). Мы еще к этому вернемся, но для начала я хочу показать вам другое, более интересные области, где можно встретить термин "прототип" и стоящую за ним концепцию. А для начала давайте рассмотрим оригинальный шаблон. 4 | 5 | 6 | ## Шаблон проектирования прототип 7 | 8 | 9 | Давайте представим что мы делаем игру в стиле Gauntlet. У нас есть всякие существа и демоны, роящиеся вокруг героя и норовящие откусить кусочек его плоти. Эти незванные сотрапезники появляются через "спаунер (spawners)" и для каждого типа врагов есть отдельный тип спаунера. 10 | 11 | Для упрощения примера давайте сделаем предположение что для каждого типа монстра в игре имеется отдельный тип. Т.е. у нас есть C++ классы для `Ghost`, `Demon`, `Sorcerer` и т.д.: 12 | 13 | > Я умышленно не пишу здесь "оригинальный". Паттерны проектирования цитируют легендарный проект [Sketchpad](http://en.wikipedia.org/wiki/Sketchpad) 1963-го года за авторством Ивана Сазерленда, который можно считать первым примером применения шаблона в природе. Когда все остальные слушали Дилана и Битлз, Сазерленд был занят всего навсего изобретением базовых концепций CAD, интерактивной графики и объектно-ориентированного программирования. 14 | > 15 | > Можете посмотреть [демо](https://www.youtube.com/watch?v=USyoT_Ha_bA) и впечатлиться 16 | 17 | ```cpp 18 | class Monster 19 | { 20 | // Stuff... 21 | }; 22 | 23 | class Ghost : public Monster {}; 24 | class Demon : public Monster {}; 25 | class Sorcerer : public Monster {}; 26 | ``` 27 | 28 | Спаунер конструирует экземпляры одного из типов монстров. Для поддержки всех монстров в игре мы можем использовать прямолинейный подход и заведем класс спаунер для каждого класса монстра. В результате получится следующая иерархия: 29 | 30 | ![](/assets/2-4-1.1.png) 31 | 32 | Реализация будет выглядеть так: 33 | 34 | ```cpp 35 | class Spawner 36 | { 37 | public: 38 | virtual ~Spawner() {} 39 | virtual Monster* spawnMonster() = 0; 40 | }; 41 | 42 | class GhostSpawner : public Spawner 43 | { 44 | public: 45 | virtual Monster* spawnMonster() 46 | { 47 | return new Ghost(); 48 | } 49 | }; 50 | 51 | class DemonSpawner : public Spawner 52 | { 53 | public: 54 | virtual Monster* spawnMonster() 55 | { 56 | return new Demon(); 57 | } 58 | }; 59 | 60 | // Ну вы поняли... 61 | 62 | ``` 63 | 64 | Если вам конечно не платят за каждую строчку кода, использовать такой подход совсем не весело. Куча классов, куча похожего кода, куча избыточности, куча дублей, куча самоповторов... 65 | 66 | Шаблон прототип предлагает решение. Ключевой мыслью является создание _объекта, который может порождать объекты, похожие на себя_. Если у вас есть один призрак, вы можете с его помощью получить кучу призраков. Если есть демон, можно сделать больше демонов. Любого монстра можно трактовать как _прототипируемого_ монстра, используемого для генерации новых версий его самого. 67 | 68 | Для реализации этой идеи, мы дадим нашему базовому классу Monster абстрактный метод `clone()`: 69 | 70 | ```cpp 71 | class Monster 72 | { 73 | public: 74 | virtual ~Monster() {} 75 | virtual Monster* clone() = 0; 76 | 77 | // Другие вещи... 78 | }; 79 | ``` 80 | 81 | Каждый подкласс монстра предоставляет свою реализацию, которая возвращает объект, идентичный по классу и состоянию ему самому. Например: 82 | 83 | ```cpp 84 | class Ghost : public Monster { 85 | public: 86 | Ghost(int health, int speed) : health_(health), 87 | speed_(speed) {} 88 | 89 | virtual Monster* clone() 90 | { 91 | return new Ghost(health_, speed_); 92 | } 93 | 94 | private: 95 | int health_; 96 | int speed_; 97 | }; 98 | ``` 99 | 100 | Как только все монстры будут его поддерживать, нам больше не нужен будет отдельный класс спаунер для каждого класса монстров. Вместо этого мы обойдемся всего одним: 101 | 102 | ```cpp 103 | class Spawner 104 | { 105 | public: 106 | Spawner(Monster* prototype) : prototype_(prototype) {} 107 | 108 | Monster* spawnMonster() 109 | { 110 | return prototype_->clone(); 111 | } 112 | 113 | private: 114 | Monster* prototype_; 115 | }; 116 | ``` 117 | 118 | Внутри себя он содержит монстра, скрытого извне, который используется спаунером в качестве шаблона для штамповки новых монстров ему подобны. Получается нечто наподобие матки пчел, никогда не покидающей своего улья. 119 | 120 | ![](/assets/2-4-1.2.png) 121 | 122 | Для создания спаунера призраков, мы просто создаем прототипируемый экземпляр призрака и затем создаем спаунер, который будет хранить этот прототип: 123 | 124 | ```cpp 125 | Monster* ghostPrototype = new Ghost(15, 3); 126 | Spawner* ghostSpawner = new Spawner(ghostPrototype); 127 | ``` 128 | 129 | Интересна одна особенность этого шаблона заключается в том что он не просто клонирует класс прототипа, но и клонирует его _состояние_. Это значит что мы можем сделать спаунер для быстрых призраков, для слабых, для медленных, просто создавая соответствующего прототипируемого призрака. 130 | 131 | На мой взгляд этот шаблон одновременно и элегантен и удивителен. Я не могу представить чтобы дошел до него своим умом, но теперь я просто не могу себе представить что я мог бы о нем не знать. 132 | 133 | 134 | ## Насколько хорошо он работает? 135 | 136 | Итак нам не нужно создавать отдельный класс спаунер для каждого монстра и это хорошо. Но при этом нам _нужно_ реализовывать метод `clone()` в каждом классе монстров. Кода там примерно столько же сколько и в спаунере. 137 | 138 | К сожалению если вы попытаетесь написать корректную реализацию `clone()`, вы быстро наткнетесь на несколько подводных камней. Должен это быть глубокий клон или приблизительный? Другими словами, если демон держит вилы, должен ли клонированный демон тоже держать вилы? 139 | 140 | Это не просто выглядит как выдуманная проблема, это действительно _выдуманная проблема_. Нужно принять как должное то что у нас есть отдельные классы для каждого монстра. В наше время так игровые движки писать _не_ принято. 141 | 142 | Большинство из нас не раз убеждались на собственном опыте что поддержка такой организации иерархии классов крайне болезненна, поэтому вместо этого для моделирования различных сущностей без отведения под каждую отдельного класса мы используем шаблоны наподобие [Компонент(Component)](/shabloni-snizheniya-svyaznosti-decoupling-patterns/komponent-component.md) или [Тип объекта (Type Object)](/povedencheskie-shabloni-behavioral-patterns/obekt-tip-type-object.md). 143 | 144 | 145 | ### Функции спаунера 146 | 147 | 148 | Даже если у нас для каждого типа монстра имеется свой класс, есть другой способ поймать кота. Вместо того чтобы делать отдельный класс спаунер для каждого монстра, можно огранизовать _функцию_ спаунер: 149 | 150 | ```cpp 151 | Monster* spawnGhost() 152 | { 153 | return new Ghost(); 154 | } 155 | ``` 156 | 157 | Это уже не настолько примитивный подход, как создание отдельного класс для каждого нового типа монстров. Теперь единственный класс-спаунер может просто хранить указатель на функцию: 158 | 159 | ```cpp 160 | typedef Monster* (*SpawnCallback)(); 161 | 162 | class Spawner 163 | { 164 | public: 165 | Spawner(SpawnCallback spawn) : spawn_(spawn) {} 166 | 167 | Monster* spawnMonster() 168 | { 169 | return spawn_(); 170 | } 171 | 172 | private: 173 | SpawnCallback spawn_; 174 | }; 175 | ``` 176 | 177 | И для создания спаунера призраков нужно будет всего лишь вызвать: 178 | 179 | ```cpp 180 | Spawner* ghostSpawner = new Spawner(spawnGhost); 181 | ``` 182 | 183 | 184 | ### Шаблоны (Templates) 185 | 186 | 187 | Сейчас большинство C++ разработчиков знакомы с концепцией шаблонов. Нашему классу спаунеру нужно создать экземпляр определенного класса, но мы не хотим жестко прописывать в коде определенный класс монстра. Естественным решением этой задачи будет воспользоваться возможностями шаблонов и добавить _параметр_ типа: 188 | 189 | ```cpp 190 | class Spawner 191 | { 192 | public: 193 | virtual ~Spawner() {} 194 | virtual Monster* spawnMonster() = 0; 195 | }; 196 | 197 | template 198 | class SpawnerFor : public Spawner 199 | { 200 | public: 201 | virtual Monster* spawnMonster() { return new T(); } 202 | }; 203 | ``` 204 | 205 | > Я не могу утверждать что программисты C++ научились их любить или что некоторых они настолько пугают, что люди просто отказываются от C++. В любом случае все кто сегодня использует C++, используют и шаблоны тоже. 206 | > 207 | > Класс Spawner в данном коде не интересуется какой тип монстра он будет создавать. Он просто работает с указателем на `Monster`. 208 | > 209 | > Если бы у нас был только класс SpawnerFor, у нас не было бы ни одного экземпляра супертипа, разделяемого между шаблонами так что любому коду, работающему со спаунерами разных типов монстров, тоже пришлось бы принимать в качестве параметров шаблоны. 210 | 211 | Применение выглядит следующим образом: 212 | 213 | ```cpp 214 | Spawner* ghostSpawner = new SpawnerFor(); 215 | ``` 216 | 217 | ### Класс первого типа 218 | 219 | 220 | Предыдущие два решения требовали от нас иметь класс `Spawner`, параметризируемый типом. В C++ классы в общем не являются объектами первого класса, так что это требует некоторых усилий. А вот если вы используете язык с динамическими типами наподобие JavaScript, Python или Ruby, где классы - это просто _обычные_ объекты, которые можно как угодно передать, задача решается гораздо проще. 221 | 222 | Если вам нужно соорудить спаунер - просто передайте ему класс монстра, которых он должен клонировать, т.е. по сути обычный объект, представляющий класс монстра. Проще пареной репы. 223 | 224 | > В некотором роде шаблон [Объект тип (Type Object)](/povedencheskie-shabloni-behavioral-patterns/obekt-tip-type-object.md) - это очередной способ обхода проблемы отсутствия класса первого типа. В языке с таким типом он тоже может быт полезен, потому что позволяет вам самостоятельно определять что такое "тип". Вам может пригодится семантика отличная от той, что предоставляют встроенные классы. 225 | 226 | Имея столько возможностей, я не могу припомнить случай, в котором _паттерн проектирования_ прототип был бы лучшим вариантом. Может ваш опыт немного отличается от моего, но давайте лучше перейдем к следующей теме: прототипу как _языковой парадигме_. 227 | 228 | 229 | ## Прототип, как языковая парадигма 230 | 231 | 232 | Многие думают, что "объектно-ориентированное программирование" - это синоним слова "классы". Определения ООП напоминают кредо совершенно противоположных религий. Единственным бесспорным фактом является признание того факта что ООП _позволяет вам определять "объект", объединяющий данные и код в единое целое_. По сравнению со структурированными языками наподобие C и функциональными языками типа Scheme, ключевой особенностью ООП является способность связки состояния и поведения. 233 | 234 | Вам может показаться что единственным способом это осуществить является использование классов, но некоторые люди, включая Дейва Унгара и Ренделла Смита думают иначе. Еще в 80-е они создали язык Self. Несмотря на то что это ООП язык, классов в нем нет. 235 | 236 | ### Self 237 | 238 | На самом деле Self даже _более_ объектно-ориентированный, чем языки с классами. Под ООП мы подразумеваем неразлучность состояния и поведения, а в языках с классами между ними на самом деле есть большое разделение. 239 | 240 | Вспомните семантику своего любимого языка с классами. Чтобы получить доступ к состоянию объекта, вы ищете в памяти его экземпляр. Состояние _содержится_ в экземпляре. 241 | 242 | Для вызова метода вы сначала ищете класс экземпляра и затем ищете метод _в нем_. Поведение содержится в классе. Всегда присутствует этот уровень косвенности для доступа к методу, отделяющий поля от методов. 243 | 244 | > Например чтобы вызвать виртуальный метод в C++, вы ищете его через указатель на экземпляр в виртуальной таблице и затем уже ищете в нем метод. 245 | 246 | ![](/assets/2-4-1.3.png) 247 | 248 | Self убирает это различие. Чтобы найти _что угодно_, вы просто ищете это в объекте. Экземпляр может хранить как состояние так и поведение. Вы можете иметь отдельный объект с совершенно уникальным для него методом. 249 | 250 | ![](/assets/2-4-1.4.png) 251 | 252 | > Никто из людей не остров, кроме этого объекта (No man is an island, but this object is.. Отсылка к сериалу Девочки Гилмор http://www.imdb.com/title/tt0238784/) 253 | 254 | Если бы это было все что делает Self, пользоваться им было бы довольно сложно. Наследование в языках с классами, несмотря на свои недостатки, дает вам удобный механизм для полиморфного повторного использования кода и избегания дублирования. Для получения подобных результатов в Self есть _делегирование_. 255 | 256 | Чтобы получить доступ к полю или вызвать метод определенного объекта, мы сначала должны получить доступ к самому объекту. Если получилось - дальше все просто. Если нет - мы ищем _родителя_ объекта. Это просто ссылка на другой объект. Если не удалось найти свойство у самого объекта, мы попробуем его родителя, и родителя родителя и т.д. Другими словами, неудавшийся поиск _делегируется_ родителю объекта. 257 | 258 | > Здесь допущено небольшое упрощение. Self помимо всего прочего поддерживает еще и несколько родительских объектов. Родители - это всего лишь специальным образом помеченные поля, дающие вам возможность использовать штуки типа наследования родителей или изменять их во время работы. Такой подход называется _динамическим наследованием (dynamic inheritance)_. 259 | 260 | ![](/assets/2-4-1.5.png) 261 | 262 | Родительский объект дает нам возможность повторно использовать поведение (и состояние!) между несколькими объектами, так что мы уже перекрыли некоторую функциональность классов. Еще одна ключевая особенность классов заключается в том, что они позволяют нам создавать экземпляры классов. Когда вам нужен новый ThingamaBob, вы просто пишете `new Thingamabob()` ну или нечто подобное, если используете другой язык. Класс - это фабрика экземпляров самого себя. 263 | 264 | Как можно создать нечто без класса? А как мы на самом деле делаем обычно новые вещи? Также как и в рассмотренном нами шаблоне проектирования, Self делает это с помощью _клонирования_. 265 | 266 | В Self _каждый_ из объектов поддерживает шаблон проектирования Прототип автоматически. Любой объект можно клонировать. Чтобы наделать кучу одинаковых объектов нужно просто: 267 | 268 | 1. Привести один из объектов в нужное вас состояние. Можно просто взять за основу встроенный в систему базовый объект Object и дополнить его нужными полями и методами. 269 | 1. Клонировать его и получить столько... клонов, сколько вам нужно. 270 | 271 | Таким образом мы получаем элегантность шаблона Прототип, но без необходимости писать реализацию `clone()` для каждого класса самостоятельно. Он просто встроен в систему. 272 | 273 | Это настолько прекрасная, разумная и минималистская система, что как только я узнал об этой парадигме, я сразу принялся за написание языка на основе прототипов просто чтобы разобраться в парадигме получше. 274 | 275 | > Я пришел к выводу что написание языка с нуле - не лучший способ что либо выучить, но это одна из моих странностей. Если вам любопытно, язык называется Finch. 276 | 277 | 278 | ### И как оно? 279 | 280 | 281 | Играться с языком на базе прототипов было замечательно, но как только мой собственный язык заработал, я обнаружил один малоутешительный факт: программировать на нем было не особо весело. 282 | 283 | Конечно язык был простым для реализации, но только потому что я переложил всю сложность на плечи пользователя. Как только я начала пробовать им пользоваться, я обнаружил что мне очень не хватает структурированности, которую дают классы. Я закончил тем, что стал пытаться компенсировать их отсутствие в самом языке написанием специальной библиотеки. 284 | 285 | Возможно все дело в том что я слишком привык пользоваться языками с классами и мой мозг слишком привык к этой парадигме. Но у меня есть большое подозрение что многим людям такой "порядок вещей" нравится. 286 | 287 | И в продолжение истории ошеломительного успеха языков на основе классов. Посмотрите как много игр страдают от избытка классов персонажей, полного перечня различных типов врагов, предметов, навыков, каждый из которых старательно подписан. Не думаю что вы найдете много игр, где каждый монстр представляет собой уникальную снежинку в духе "нечто среднее между троллем и гоблином и небольшой примесью змея". 288 | 289 | > С тех пор я часто слышу что многие программисты на Self приходят к тому же выводу. Впрочем это не означает, что проект был совсем провальный. Self был настолько динамичен что для того чтобы работать с нормальной скоростью ему реально необходимы все современные инновации в области виртуализации. 290 | > 291 | > Изобретенные ими идеи относительно компиляции на ходу, сборщика мусора и оптимизации вызова методов - это именно те технологии, которые сделали (зачастую усилиями тех же самых людей) многие современные языки с динамическими типами достаточно быстрыми для того чтобы писать на них популярные приложения. 292 | 293 | Несмотря на то что прототипы - это действительно очень мощная парадигма, и я хочу чтобы об этом узнало как можно больше людей, я рад что большинство из нас все таки не использует ее в повседневной работе. Потому что тот код с реализаций прототипов что я видел, представлял из себя настолько ужасное месиво, что я так и не смог его понять. 294 | 295 | > Также это говорит о том, что на самом деле существует очень мало кода, написанного в стиле прототипирования. Я смотрел. 296 | 297 | ### А что насчет JavaScript? 298 | 299 | Ну хорошо, если языки на основе прототипов настолько недружественны, то как я могу объяснить существование Java Script? Ведь это язык с прототипами, которым ежедневно пользуются миллионы людей. Код JavaScript выполняет больше компьютеров чем код на любом другом языке в мире. 300 | 301 | Брендан Айк - создатель JavaScripts черпал вдохновение прямиком из Self и поэтому большая часть семантики JavaScripts основана на прототипах. Каждый объект может иметь произвольный набор свойств, которые в свою очередь могут быть как полями, так и "методами" (которые на самом деле просто функции, хранящиеся в виде полей). У каждого объекта может быть другой объект, называемый его "прототипом", к которому происходит делегирование если нужное поле не найдено. 302 | 303 | > Для разработчика языка привлекательной особенностью прототипов является то что реализовывать их легче чем классы. Эйх тоже этим пользовался: первая версия JavaScript была написана всего за десять дней. 304 | 305 | И все таки, несмотря на все это, я считаю что на практике у JavaScript гораздо больше общего именно с языками на основе классов, чем с основанными на прототипах. Это заметно уже хотя бы потому ,что в JavaScript предпринято большое отступление от Self - ключевой операции любого языка на основе прототипов - _клонирования_ - нигде не видно. В JavaScript не существует метода для клонирования объекта. 306 | 307 | Самая близкая по смыслу операция из существующих - это `Object.create`, позволяющая вам создать новый объект, делегирующий к уже существующему. И даже эта возможность появилась только в спецификации ECMAScript 5, через четырнадцать лет после выхода JavaScript. Давайте я покажу как обычно определяют типы и создают объекты в JavaScript вместо клонирования. Начинается все с _функции конструктора(constructor function)_: 308 | 309 | ```js 310 | function Weapon(range, damage) { 311 | this.range = range; 312 | this.damage = damage; 313 | } 314 | ``` 315 | 316 | С ее помощью создается новый объект и инициализируются его поля. Вызов выглядит следующим образом: 317 | 318 | ```js 319 | var sword = new Weapon(10, 16); 320 | ``` 321 | 322 | В этом коде new вызывает тело функции `Weapon`, внутри которой `this` связано с новым пустым объектом. Внутри тела функции к объекту добавляется куча полей, а потом новосозданный объект автоматически возвращается. 323 | 324 | `new` делает за вас еще одну вещь. Когда он создает чистый объект, он сразу делает его делегатом объекта-прототипа. Доступ к объекту прототипу можно получить через `Weapon.prototype`. 325 | 326 | Так как состояние добавляется в теле конструктора, для определения _поведения_ вы обычно добавляете методы к прототипу объекта. Примерно таким образом: 327 | 328 | ```js 329 | Weapon.prototype.attack = function(target) { 330 | if (distanceTo(target) > this.range) { 331 | console.log("Out of range!"); 332 | } else { 333 | target.health -= this.damage; 334 | } 335 | } 336 | ``` 337 | 338 | Здесь мы добавляем прототипу оружия свойство `attack`, значением которого будет функция. И так как каждый объект, возвращаемый `new Weapon()`, делегируется к `Weapon.prototype`, вы можете теперь сделать вызов `sword.attack()` и он вызовет нужную нам функцию. Выглядит это примерно так: 339 | 340 | ![](/assets/2-4-1.6.png) 341 | 342 | Давайте еще раз: 343 | 344 | * Новые объекты вы создаете с помощью операнда "new", который вы вызываете используя объект, представляющий собой тип - функцию-конструктор. 345 | * Состояние хранится в самом экземпляре. 346 | * Поведение задается через уровень косвенности - делегирование к прототипу и хранится в виде отдельного объекта, представляющего собой набор методов, разделяемый между всеми объектами данного типа. 347 | 348 | Вы можете назвать меня психом, но это крайне похоже на мое определение классов, которое я привел выше. Вы имеете возможность писать код в стиле прототипов в JavaScript (без клонирования), но синтаксис и идиоматика языка предполагают подход, основанный на классах. 349 | 350 | Я лично считаю что это хорошо. Как я уже сказал, я убедился на собственном опыте что прототипы усложняют работу с кодом, так что мне нравится то как JavaScript оборачивает свое ядро в более похожую на классы форму. 351 | 352 | ## Прототипы для моделирования данных 353 | 354 | Итак я продолжаю перечислять вещи за которые я не люблю прототипы. Депрессивная глава получается. Я задумывал эту книгу скорее как комедию, а не как трагедию, так что покончим с этим и перейдем к областям, где на мой взгляд прототипы или говоря конкретнее делегирование может быть полезным. 355 | 356 | Если вы посчитаете все байты в игре приходящиеся на код и сравните с объемом остальных данных, вы увидите что с момента появления игр доля данных постоянно увеличивается. Ранние игры практически все генерировали процедурно и как следствие могли поместиться на дискетку или картридж. В большинстве современных игр код - это всего лишь "движок", который позволяет игре работать, а сама игра полностью определена в данных. 357 | 358 | Это конечно здорово, но перемещение контента в файлы данных вовсе не означает, что мы избавляемся от организационных сложностей большого проекта. Скорее наоборот усложняем себе жизнь. Одной из причин почему мы используем языки программирования является то что они предоставляют нам инструменты по снижению сложности. 359 | 360 | Вместо того чтобы копировать и вставлять кусок кода в десяти местах, мы помещаем его в отдельную функцию и вызываем ее по имени. Вместо того чтобы копировать метод в кучу классов, мы просто помещаем его в отдельный класс , а остальные классы от него наследуем. 361 | 362 | Когда объем данных в игре достигает некоторого предела, вам сразу начинает хотеться обладать подобными возможностями. Моделирование данных - это слишком большая область чтобы обсуждать ее на поверхностном уровне, но я хочу показать вам одну из возможностей, которая пригодится вам в вашей игре: использование прототипов и делегирование для повторного использования данных. 363 | 364 | Давайте представим себе, что мы определяем модель данных для бессовестного клона Gauntlet, о котором я писал выше. Геймдизайнеру нужны какие-то файлы, в которые он сможет поместить описание атрибутов монстров и предметов. 365 | 366 | > Я имею в виду полностью оригинальную игру, никоим образом не напоминающую хорошо известную ранее многопользовательскую аркадную игру с видом сверху. Так что не подавайте на меня в суд пожалуйста. 367 | 368 | Можно использовать JSON: сущности данных будут представлены в виде _maps_ или _мешков со свойствами (property bags)_ или еще дюжиной терминов, потому что программисты просто обожают придумывать для одного и того же разные имена. 369 | 370 | > Мы так часто их переизобретаем что Стив Йегге решил назвать их [Универсальным шаблоном проектирования(“The Universal Design Pattern”.)](http://steve-yegge.blogspot.com/2008/10/universal-design-pattern.html). 371 | 372 | Итак гоблин в игре описан следующим образом: 373 | 374 | ``` 375 | { 376 | "name": "goblin grunt", 377 | "minHealth": 20, 378 | "maxHealth": 30, 379 | "resists": ["cold", "poison"], 380 | "weaknesses": ["fire", "light"] 381 | } 382 | ``` 383 | 384 | Довольно прямолинейный подход и даже не любящие писать дизайнеры могут справиться. Можно например добавить еще парочку сестринских описаний в славном семейном дереве зеленых гоблинов: 385 | 386 | ``` 387 | { 388 | "name": "goblin wizard", 389 | "minHealth": 20, 390 | "maxHealth": 30, 391 | "resists": ["cold", "poison"], 392 | "weaknesses": ["fire", "light"], 393 | "spells": ["fire ball", "lightning bolt"] 394 | } 395 | ``` 396 | 397 | ``` 398 | { 399 | "name": "goblin archer", 400 | "minHealth": 20, 401 | "maxHealth": 30, 402 | "resists": ["cold", "poison"], 403 | "weaknesses": ["fire", "light"], 404 | "attacks": ["short bow"] 405 | } 406 | ``` 407 | 408 | Если бы это был обычный код, наше чувство прекрасного уже заставило бы нас беспокоиться. У этих сущностей слишком много общей дублирующейся информации, а хорошо натренированные программисты это просто ненавидят. Данные занимают слишком много места и требуют слишком много времени на написание. Даже для того чтобы выяснить одинаковые ли это данные вам нужно тщательно их прочитать. Их поддержка - настоящая головная боль. Если мы захотим сделать всех гоблинов в игре сильнее, нам нужно будет не забыть обновить значение здоровья для них всех. Плохо, плохо, плохо. 409 | 410 | Если бы это был код, мы могли бы создать абстракцию "гоблин" и использовать ее между всему типами гоблинов. Но тупой JSON ничего об этом не знает. Давайте попробуем сделать его чуточку умнее. 411 | 412 | Определим для каждого объекта поле "`prototype`" и поместим туда имя объекта, к которому он делегирует. Любые свойства, отсутствующие у первого объекта нужно будет смотреть в прототипе. 413 | 414 | Это позволит нам упростить описание нашей оравы гоблинов: 415 | 416 | > Таким образом `prototype` переходит из разряда обычных данных в _метаданные_. У каждого гоблина есть бородавчатая кожа и желтые зубы. У него нет прототипа. Прототип - это свойство _объекта данных, описывающего гоблина_, а не самого гоблина. 417 | 418 | ``` 419 | { 420 | "name": "goblin grunt", 421 | "minHealth": 20, 422 | "maxHealth": 30, 423 | "resists": ["cold", "poison"], 424 | "weaknesses": ["fire", "light"] 425 | } 426 | 427 | { 428 | "name": "goblin wizard", 429 | "prototype": "goblin grunt", 430 | "spells": ["fire ball", "lightning bolt"] 431 | } 432 | 433 | { 434 | "name": "goblin archer", 435 | "prototype": "goblin grunt", 436 | "attacks": ["short bow"] 437 | } 438 | ``` 439 | 440 | Так как и лучник и чародей имеют в качестве прототипа пехотинца, нам не нужно указывать заново здоровье, сопротивляемости и уязвимости для каждого из них. Добавленная нами в данные логика предельно проста - мы просто добавили простейшее делегирование и сразу смогли избавиться от кучи повторов. 441 | 442 | Хочу обратить ваше внимание на то что мы не стали добавлять четвертого "базового гоблина" в качестве _абстрактного_ прототипа, к которому будут делегировать остальные три. Вместо этого мы просто взяли одного из гоблинов, который является простейшим и делегируем к нему. 443 | 444 | Такой подход является естественным для систем на основе прототипов, где каждый объект можно использовать для клонирования нового объекта с уточненными свойствами и смотрится натуральным и здесь. Применительно к игровым данным такой подход тоже удобен потому что здесь часто приходится создавать объекты, лишь немного отличающиеся от остальных. 445 | 446 | Подумайте о боссах и уникальных предметах. Очень часто они являются лишь немного измененной версией обыкновенных игровых объектов и прототипирование с делегированием очень хорошо подходит для их описания. Магический Меч-голова с плеч можно описать как длинный меч с определенными бонусами: 447 | 448 | ``` 449 | { 450 | "name": "Sword of Head-Detaching", 451 | "prototype": "longsword", 452 | "damageBonus": "20" 453 | } 454 | ``` 455 | 456 | Такие дополнительные возможности для описания данных могут облегчить жизнь вашим дизайнерам и добавить больше вариативности предметам и популяции монстров в игре, а это именно то, что может понравиться игрокам. -------------------------------------------------------------------------------- /shabloni-optimizatsii/pul-obektov-object-pool.md: -------------------------------------------------------------------------------- 1 | # Пул объектов \(Object Pool\) 2 | 3 | ## Задача 4 | 5 | _Улучшение производительности и эффективности использования памяти за счет повторного использования объектов из фиксированного пула, вместо их индивидуального выделения и освобождения._ 6 | 7 | ## Мотивация 8 | 9 | Мы работаем над визуальными эффектами в игре. Когда герой кастует заклинание, мы хотим чтобы мерцающие блестки рассыпались по всему экрану. Это будут вызовы системы частиц: движок, поражающий маленькие блестящие картинки и анимирующий их до тех пор пока они не исчезнут. 10 | 11 | Так как по мановению волшебной палочки мы можем породить сотни частиц, нашей системе нужно иметь возможность создавать их очень быстро. Что более важно, нам нужно удостовериться что создание и уничтожение частиц не приведет к _фрагментации памяти_. 12 | 13 | ## Проклятье фрагментации 14 | 15 | 16 | Программирование для игровых консолей типа XBox 360 больше похоже на программирование для встроенных систем, чем на программирование для PC. Как и в программировании для встроенных систем, консольные игры должны работать очень долгое время без падений и утечек памяти, при том что эффективные менеджеры памяти встречаются не так уж и часто. В такой рабочей среде фрагментация памяти смертельно опасна. 17 | 18 | Фрагментация означает что свободное место в нашей куче разбивается на мелкие кусочки памяти вместо больших открытых блоков. _Общее_ количество доступной памяти может быть большим, но наибольший _последовательный_ участок может быть ужасно маленьким. Предположим что у нас есть четырнадцать свободных байт, но они фрагментированы на два отдельных куска, разделенные участком занятой памяти посередине. Если мы попробуем разместить здесь объект длиной в двенадцать байт, мы получим ошибку. И больше никаких блесток на экране. 19 | 20 | > Очень похоже на параллельную парковку на заставленной улице, когда припаркованные авто распределены слишком далеко друг от друга. Если бы их можно было посдвигать чуть поближе, нашлось бы еще достаточно места. Но к сожалению это место _фрагментировано_ на промежутки между дюжинами машин. 21 | 22 | ![6-3-1.1](../assets/6-3-1.1.png) 23 | 24 | > Вот как будет фрагментирована наша куча и почему у нас будет ошибка выделения памяти, хотя теоретически памяти у нас достаточно. 25 | 26 | Даже если фрагментация встречается нечасто, она может постепенно привести кучу в состояние бесполезных пузырей из дырок и щелей, полностью лишив игру возможности с ней работать. 27 | 28 | > Большинство консольных платформодержателей требуют, чтобы игры проходили "тест на протечку (soak test)", когда игра оставляется работающей на несколько дней в демо-режиме. Если игра падает - ей не разрешают выйти. Тест на протечку иногда проваливается и из-за какого-нибудь редкого бага, но чаще всего игру обрушивает утечка памяти, вызванная фрагментацией. 29 | 30 | 31 | ### Лучшее из двух миров 32 | 33 | Из-за фрагментации и потому что выделение памяти может работать медленно, игры всегда очень осторожны насчет того где и как работать с памятью. Простейшее решение обычно и самое лучшее: Захватите кусок памяти побольше при старте игры и освободите его при выходе из игры. К сожалению такую стратегию сложно использовать когда нам нужно постоянно создавать и удалять объекты во время работы игры. 34 | 35 | Пул объектов дает нам лучшее из двух миров: с точки зрения менеджера памяти мы просто выделяем большой ломоть памяти и не освобождаем ее пока игра работает. Для пользователей пула мы можем создавать и удалять объекты сколько нашей душе будет угодно. 36 | 37 | 38 | ## Шаблон 39 | 40 | Определим класс **пула**, содержащего коллекцию **многоразовых объектов**. Каждый объект поддерживает **запрос "используется"**, означающий что он сейчас "жив". Когда пул инициализируется, он сразу создает всю коллекцию объектов (обычно выделяя один последовательный участок памяти) и инициализирует их всех состоянием "не используется". 41 | 42 | Когда вам понадобится новый объект, вы запрашиваете его у пула. Он ищет доступный объект, инициализирует его значением "используется" и возвращает. Когда объект больше не нужен, он снова возвращается в состояние "не используется". Таким образом, объекты можно свободно создавать и удалять без необходимости выделять память или другие ресурсы. 43 | 44 | 45 | ## Когда использовать 46 | 47 | Этот шаблон широко используется в играх не только для очевидных вещей типа игровых сущностей и визуальных эффектов, но и для менее заметных структур данных типа проигрываемых звуков. Пул объектов используется когда: 48 | 49 | - Вам нужно часто создавать и удалять объекты. 50 | 51 | - Объекты одного размера. 52 | 53 | - Выделение объектов из кучи работает медленно или может привести к фрагментации памяти. 54 | 55 | - Каждый объект инкапсулирует ресурс типа базы данных или сетевого соединения, который сложно получать и можно использовать повторно. 56 | 57 | 58 | ## Имейте в виду 59 | 60 | Обычно вы можете положиться на сборщик мусора или операторы `new` и `delete`, которые сделают всю работу за вас. Когда вы используете пул объектов, вы как будто говорите "Я лучше знаю как обращаться с этими байтами". А еще это значит, что на вас ложится бремя ограничений шаблона. 61 | 62 | ### Пул может тратить память на неиспользуемые объекты 63 | 64 | Размер пула нужно настраивать соразмерно с нуждами игры. При настройке обычно проще всего понять когда пул _недостаточного_ размера (уверен что падение игры наверняка привлечет ваше внимание). Но еще нужно следить и за тем чтобы пул не был слишком _большим_. Если уменьшить пул, освободившуюся память можно использовать для чего-либо более полезного. 65 | 66 | ### В каждый момент времени может быть активно только определенное количество объектов 67 | 68 | Иногда это хорошо. Разделение памяти на отдельные пулы для различных типов объектов означает с одной стороны то что последовательность взрывов не заставит вашу систему частиц отожрать _всю_ доступную память, не позволив вам создать что-либо более полезное, типа нового противника. 69 | 70 | Кроме того вы должны быть готовы к возможности что выделить объект из пула не удастся, потому что все объекты будут заняты. Есть несколько стратегий обработки такой ситуации: 71 | 72 | - _Прямое вмешательство_. Это самое очевидное "исправление": будем настраивать размер пула таким образом, чтобы он никогда не переполнялся независимо от действий пользователя. Для пулов с важными объектами, такими как противники или геймплейные предметы, это хороший выход. Не может быть "правильной" обработки недостатка свободных слотов для создания большого босса, когда игрок дошел до конца уровня. Так что лучше придумать что-то такое, что не позволит нам оказаться в подобной ситуации. 73 | 74 | Недостатком является то, что вам придется держать занятыми большие объемы памяти ради каких-то редких крайних случаев. Поэтому фиксированный размер пула не может считаться лучшим решением для всех состояний игры. Например, некоторые уровни могут больше налегать на визуальные эффекты, а другие на звуки. В таких случаях нам лучше иметь пулы объектов, настраиваемые отдельно для обеих сценариев. 75 | 76 | - _Просто не создаем объект_. Звучит грубо, но в случаях типа работы с системами частиц имеет смысл. Если все ваши частицы используются, экран и так кишит эффектами. Пользователь не обратит внимание если следующий взрыв будет менее впечатляющим, чем уже отображаемые. 77 | 78 | - _Принудительное убийство существующего объекта_. Представим себе пул проигрываемых сейчас звуков и предположим что вы хотите запустить новый звук, но наш пул заполнен. У вас нет желания просто проигнорировать новый звук: пользователь заметит что его магическая палочка _обычно_ драматически посвистывает, а иногда не издает ни звука. Лучше вместо этого найти самый тихий звук из тех, что уже играются и заменить его новым звуком. Новый звук заглушит слышимый обрыв предыдущего звука. 79 | 80 | В целом, если _исчезновение_ существующего объекта будет менее заметным чем _непоявление_ нового - это вполне хорошее решение. 81 | 82 | - _Увеличение размера пула_. Если ваша игра позволяет вам распоряжаться памятью более гибко, вы можете увеличивать размер пула во время выполнения или создавать дополнительные пулы переполнения. Если воспользовавшись одним из этих вариантов вы отхватите больше памяти, подумайте о том, имеет ли смысл в будущем вернуться к прежнему размеру, когда дополнительная вместимость вам уже будет не нужна. 83 | 84 | 85 | ### Размер памяти для каждого объекта фиксирован 86 | 87 | Большинство реализаций пула хранят объекты в массиве объектов на месте (in-place). Если все ваши объекты одного типа - это нормально. Однако если вы захотите хранить в пуле объекты нескольких типов или экземпляры подклассов с дополнительными полями, вам нужно быть уверенными что каждый слот пула обладает достаточным размером чтобы вместить _максимально_ возможный объект. В противном случае неожиданно большой объект вылезет за свои границы на соседний и разрушит память. 88 | 89 | В то же время, когда ваши объекты могут быть разного размера, вы впустую тратите память. Каждый слот должен быть достаточно большим чтобы вместить максимально возможный объект. Если такие большие объекты встречаются редко, вы будете попусту тратить память всякий раз, когда будете помещать в слот маленький объект. Это все равно, что проходить таможню в аэропорту с огромным чемоданом, внутри которого лежат только ключи и бумажник. 90 | 91 | Когда вы обнаружите что тратите на это слишком много памяти, вспомните о разделении пула на отдельные пулы для разных размеров объектов - большие отделения для чемоданов и маленькие для карманной мелочи. 92 | 93 | > Такой шаблон чаще всего используется в наиболее эффективных в плане скорости менеджерах памяти. У менеджера есть несколько пулов с блоками разного размера. Когда вы просите у него выделить вам блок, он ищет открытый слот в пуле подходящего размера и выделяет его из пула. 94 | 95 | 96 | ### Повторно используемые объекты не очищаются автоматически 97 | 98 | Большинство менеджеров памяти обладают отладочными функциями, которые очищают только что выделенную или освобожденную память магическими значениями типа `0xdeadbeef`. Это помогает обнаруживать болезненные баги, вызванные использованием неинициализированных значений или обращением к уже освобожденной памяти. 99 | 100 | Так как наш пул объектов не заходит в деле управления памяти дальше повторного использования объектов, он не имеет подобной страховочной сетки. Еще хуже то что память, используемая для "нового" объекта хранила раньше объект того же самого типа. Это делает весьма возможной ситуацию, когда вы забудете инициализировать что-то внутри нового созданного объекта, а память где он будет размещаться уже будет содержать почти корректные данные, оставшиеся с прошлой жизни. 101 | 102 | Поэтому нужно с особой тщательностью следить за тем, чтобы код инициализации нового объекта в пуле выполнял инициализацию объекта _полностью_. Возможно даже стоит потратить немного времени на написание отладочного функционала, очищающего память в слоте объекта при его повторном использовании. 103 | 104 | > Я буду гордится если вы выберете для очистки магическое число `0x1deadb0b`. 105 | 106 | 107 | ### Неиспользуемые объекты остаются в памяти 108 | 109 | Пулы объектов реже всего используются в системах со сборщиками мусора, потому что в таком случае менеджер памяти сам занимается проблемой фрагментации за вас. Но пулы все равно полезны тем, что помогают вам избегать выделения и освобождения памяти, особенно на мобильных устройствах с медленным процессором и простым сборщиком мусора. 110 | 111 | Если все таки будете использовать пул объектов, опасайтесь потенциальных конфликтов. Так как пул на самом деле не освобождает объекты когда они больше не используются, они остаются в памяти. Если они содержат ссылки на _другие_ объекты, они тем самым не дадут сборщику утилизировать и эти объекты тоже. Чтобы этого избежать, нам нужно очищать все ссылки на другие объекты, когда объект из пула нам больше ненужен. 112 | 113 | ## Пример кода 114 | 115 | Настоящие системы частиц обычно оперируют гравитацией, ветром, трением и другими физическими эффектами. Наш максимально простой пример будет просто перемещать частицы по прямой линии на протяжении некоторого количество кадров и потом будет убивать частицы. Не совсем киношная картинка, но для иллюстрации работы с пулом вполне достаточно. 116 | 117 | Начнем с самой наипростейшей реализации. Для начала наш класс частиц: 118 | 119 | ```cpp 120 | class Particle 121 | { 122 | public: 123 | Particle() : framesLeft_(0) {} 124 | 125 | void init(double x, double y, 126 | double xVel, double yVel, int lifetime) 127 | { 128 | x_ = x; y_ = y; 129 | xVel_ = xVel; yVel_ = yVel; 130 | framesLeft_ = lifetime; 131 | } 132 | 133 | void animate() 134 | { 135 | if (!inUse()) return; 136 | 137 | framesLeft_--; 138 | x_ += xVel_; 139 | y_ += yVel_; 140 | } 141 | 142 | bool inUse() const { return framesLeft_ > 0; } 143 | 144 | private: 145 | int framesLeft_; 146 | double x_, y_; 147 | double xVel_, yVel_; 148 | }; 149 | ``` 150 | 151 | Конструктор по умолчанию инициализирует частицу как "не используемую". Следующий вызов `init()` инициализирует частицу уже в живом состоянии. 152 | 153 | Частицы анимируются с помощью функции с именем `animate()`, которая должна вызываться на каждом кадре. 154 | 155 | Пулу нужно знать о том какая из частиц доступна для повторного использования. Он узнает это с помощью функции частицы `inUse()`. Учитывая, что жизнь частиц ограничена, она использует переменную `_framesLeft` для определения того, что частица используется без хранения отдельного флага. 156 | 157 | Класс пула также предельно прост: 158 | 159 | ```cpp 160 | class ParticlePool 161 | { 162 | public: 163 | void create(double x, double y, 164 | double xVel, double yVel, int lifetime); 165 | 166 | void animate() 167 | { 168 | for (int i = 0; i < POOL_SIZE; i++) 169 | { 170 | particles_[i].animate(); 171 | } 172 | } 173 | 174 | private: 175 | static const int POOL_SIZE = 100; 176 | Particle particles_[POOL_SIZE]; 177 | }; 178 | ``` 179 | 180 | Функция `create()` позволяет внешнему коду создавать новые частицы. Игра вызывает на каждом кадре `animate()`, анимируя тем самым все частицы в пуле. 181 | 182 | > Этот метод `animate()` представляет собой пример шаблона [Метод обновления (Update Method)](../posledovatelnie-shabloni-sequencing-patterns/metodi-obnovleniya-update-methods.md). 183 | 184 | Сами частицы просто сохраняются в массив фиксированного размера в классе. В этой простой реализации размер пула жестко закодирован в определении класса, но может быть определен и извне с помощью динамического массива определенного размера или с помощью значения для параметра шаблона. 185 | 186 | Создание новой частицы предельно простое: 187 | 188 | ```cpp 189 | void ParticlePool::create(double x, double y, 190 | double xVel, double yVel, 191 | int lifetime) 192 | { 193 | // Find an available particle. 194 | for (int i = 0; i < POOL_SIZE; i++) 195 | { 196 | if (!particles_[i].inUse()) 197 | { 198 | particles_[i].init(x, y, xVel, yVel, lifetime); 199 | return; 200 | } 201 | } 202 | } 203 | ``` 204 | 205 | Мы обходим пул и ищем первую доступную частицу. Когда мы ее находим, мы инициализируем ее и на этом все. Обратите внимание что в этой реализации, если у нас нет доступных частиц, мы вообще не создаем новую. 206 | 207 | Это и есть вся простая система частиц за исключением рендеринга частиц конечно. Теперь мы можем создать пул и несколько частиц с его помощью. Частицы будут деактивировать сами себя автоматически когда закончится их время жизни. 208 | 209 | Такая реализация вполне подходит для игры, но вы уже наверняка заметили, что создание новой частицы может потребовать (потенциально) обхода всей коллекции частиц до тех пор пока не найдем пустой слот. Если пул достаточно большой и практически заполнен, это может быть довольно медленно. Посмотрим как мы сможем с этим справиться. 210 | 211 | > Для тех из нас кто еще помнит теорию алгоритмов, создание частицы имеет сложность _O(n)_. 212 | 213 | 214 | ### Свободный список 215 | 216 | Если мы не хотим терять время на _поиск_ свободных частиц, логичным решением будет следить за ними. Мы можем хранить отдельный список указателей на каждую неиспользуемую частицу. И когда нам нужно будет создать новую частицы, мы просто удалим первый указатель из списка и повторно используем частицу, на которую он указывает. 217 | 218 | К сожалению для этого придется поддерживать еще один отдельный массив с количеством указателей, равным количеству объектов в пуле. В конце концов когда мы создаем пул, _все_ объекты в нем являются неиспользуемыми, так что изначально нам понадобятся указатели на все объекты. 219 | 220 | И все-таки, мне хотелось бы решить проблему с производительностью без дополнительных затрат памяти. К счастью у нас уже есть свободная память, которую мы можем позаимствовать - это сами неиспользуемые частицы. 221 | 222 | Когда частица не используется, большая часть ее состояния не имеет никакого значения. Ее позиция и скорость не используются. Единственное состояние которое для нас важно - это мертва ли частица. В нашем примере это член класса `_framesLeft`. Все остальные биты можно использовать. Вот пересмотренный вариант: 223 | 224 | ```cpp 225 | class Particle 226 | { 227 | public: 228 | // ... 229 | 230 | Particle* getNext() const { return state_.next; } 231 | void setNext(Particle* next) { state_.next = next; } 232 | 233 | private: 234 | int framesLeft_; 235 | 236 | union 237 | { 238 | // Состояние когда частица используется. 239 | struct 240 | { 241 | double x, y; 242 | double xVel, yVel; 243 | } live; 244 | 245 | // Состояние когда частица доступна. 246 | Particle* next; 247 | } state_; 248 | }; 249 | ``` 250 | 251 | Мы взяли все члены переменные за исключением `framesLeft_` и переместили их в структуру `live` внутри объединения `state_`. Эта структура хранит состояние частицы когда она анимируется. Когда частица не используется, в дело вступает другая часть объединения - член `next`. Он хранит указатель на следующую доступную частицу за данной. 252 | 253 | > В наши дни объединения используются не слишком часто, так что даже их синтаксис может выглядеть для вас незнакомым. Если вы работаете в команде, у вас наверняка есть "гуру по памяти", который вам помогает в случаях, когда у вас появляются проблемы с бюджетом памяти. Спросите его об объединениях. Такие люди много о них знают, включая довольно забавные трюки с упаковкой битов. 254 | 255 | Мы можем использовать эти указатели для создания связанного списка, связывающего воедино все неиспользуемые частицы в пуле. У нас есть нужный нам список доступных частиц и мы не использовали никакой дополнительной памяти. Вместо этого мы отобрали память для хранения списка у самих мертвых частиц. 256 | 257 | Такая хитрая техника называется [_свободный список (free list)_](http://en.wikipedia.org/wiki/Free_list). Чтобы она заработала, нам нужно удостовериться что указатели инициализируются корректно и поддерживать их когда частицы создаются и уничтожаются. И конечно нам нужно следить за головой списка: 258 | 259 | ```cpp 260 | class ParticlePool 261 | { 262 | // ... 263 | private: 264 | Particle* firstAvailable_; 265 | }; 266 | ``` 267 | 268 | Когда пул создается впервые, все частицы доступны, так что свободный список должен распространяться на весь пул. Конструктор пула делает это следующим образом: 269 | 270 | ```cpp 271 | ParticlePool::ParticlePool() 272 | { 273 | // Доступен первый. 274 | firstAvailable_ = &particles_[0]; 275 | 276 | // Каждая частица указывает на следующую. 277 | for (int i = 0; i < POOL_SIZE - 1; i++) 278 | { 279 | particles_[i].setNext(&particles_[i + 1]); 280 | } 281 | 282 | // Последняя завершает список. 283 | particles_[POOL_SIZE - 1].setNext(NULL); 284 | } 285 | ``` 286 | 287 | Теперь для создания новой частицы нам нужно перейти к первой доступной: 288 | 289 | > Сложность _O(1)_, детка! Вот чего мы добились! 290 | 291 | ```cpp 292 | void ParticlePool::create(double x, double y, 293 | double xVel, double yVel, 294 | int lifetime) 295 | { 296 | // Проверяем что пул не заполнен полностью. 297 | assert(firstAvailable_ != NULL); 298 | 299 | // Удаляем ее из списка доступных. 300 | Particle* newParticle = firstAvailable_; 301 | firstAvailable_ = newParticle->getNext(); 302 | 303 | newParticle->init(x, y, xVel, yVel, lifetime); 304 | } 305 | ``` 306 | 307 | Нам нужно знать когда частица умирает для того чтобы добавить ее в свободный список. Для этого мы сделаем так, чтобы `animate()` возвращала `true`, если предыдущая живая частица испустила дух в этом кадре: 308 | 309 | ```cpp 310 | bool Particle::animate() 311 | { 312 | if (!inUse()) return false; 313 | 314 | framesLeft_--; 315 | x_ += xVel_; 316 | y_ += yVel_; 317 | 318 | return framesLeft_ == 0; 319 | } 320 | ``` 321 | 322 | Когда это происходит, мы просто переносим ее обратно в список: 323 | 324 | ```cpp 325 | void ParticlePool::animate() 326 | { 327 | for (int i = 0; i < POOL_SIZE; i++) 328 | { 329 | if (particles_[i].animate()) 330 | { 331 | // Добавляем эту частицу в начало списка. 332 | particles_[i].setNext(firstAvailable_); 333 | firstAvailable_ = &particles_[i]; 334 | } 335 | } 336 | } 337 | ``` 338 | 339 | Вот и готово. Маленький и удобный пул объектов с константным временем создания и удаления. 340 | 341 | 342 | ## Архитектурные решения 343 | 344 | Как вы могли увидеть, простейшая реализация пула объектов практически тривиальна: Создается массив объектов и они переинициализируются по мере необходимости. Реальный код редко бывает настолько минималистичным. Существует несколько способов сделать пул более обобщенным, безопасным для использования и простым для поддержки. Прежде чем вы решите реализовать пул в собственной игре, попробуйте ответить на несколько вопросов: 345 | 346 | 347 | ### Привязаны ли объекты к пулу? 348 | 349 | Первый вопрос, с которым мы сталкивается когда хотим написать пул объектов - это должны ли объекты знать о том что находятся в пуле. Обычно это так, но у вас не будет такой роскоши если вы пишете класс обобщенного пула, в котором можно хранить произвольные объекты. 350 | 351 | - **Если объекты связаны с пулом:** 352 | 353 | - _Реализация будет проще_. Вы можете просто добавить в объект пула флаг "используется" или функцию и на этом остановиться. 354 | 355 | - _Вы можете быть уверены, что объекты создаются только в пуле_. В C++ это легко сделать, объявив класс пула дружественным классом класса объекта и сделать его конструктор приватным. 356 | 357 | ```cpp 358 | class Particle 359 | { 360 | friend class ParticlePool; 361 | 362 | private: 363 | Particle() : inUse_(false) {} 364 | 365 | bool inUse_; 366 | }; 367 | 368 | class ParticlePool 369 | { 370 | Particle pool_[100]; 371 | }; 372 | ``` 373 | 374 | Это отношение описывает предполагаемый способ использования класса и обеспечивает то что пользователь не сможет создать объект, не отслеживаемый пулом. 375 | 376 | - _Вы можете избежать необходимости использовать флаг "используется"_. У многих объектов уже есть какое-то состояние, которое можно использовать для определения живой объект или нет. Например, частица может быть доступна для повторного использования, если ее координаты находятся за экраном. Если класс объекта знает, что его можно использовать в пуле, он может предоставлять метод `inUse()` для проверки этого состояния. Таким образом мы оберегаем пул от необходимости тратить лишнюю память для хранения флагов "используется". 377 | 378 | - **Если объекты не связаны с пулом:** 379 | 380 | - _Можно помещать в пул объекты любых типов_. Это большое преимущество. Снижая связность объекта с пулом, вы имеете возможность реализовать обобщенный класс пула. 381 | 382 | - _Состояние "используется" можно отслеживать извне объекта_. Проще всего это сделать с помощью отдельного битового флага. 383 | 384 | ```cpp 385 | template 386 | class GenericPool 387 | { 388 | private: 389 | static const int POOL_SIZE = 100; 390 | 391 | TObject pool_[POOL_SIZE]; 392 | bool inUse_[POOL_SIZE]; 393 | }; 394 | ``` 395 | 396 | 397 | ### Кто отвечает за инициализацию повторно используемых объектов? 398 | 399 | Для того чтобы повторно использовать существующие объекты, их нужно повторно инициализировать новым состоянием. Ключевым вопросом здесь является где объект повторно инициализируется - внутри класса пула или снаружи. 400 | 401 | - **Если пул повторно инициализируется внутри:** 402 | 403 | - _Пул может полностью инкапсулировать свои объекты_. В зависимости от других возможностей, нужных вашим объектам, вы можете полностью хранить их внутри пула. В таком случае вы можете быть уверенными, что никакой другой код не будет хранить ссылку на объект пула и его можно будет свободно использовать повторно. 404 | 405 | - _Пул отвечает за то как объект будет инициализирован_. Объект пула может предлагать несколько функций для своей инициализации. Если инициализацией управляет пул, его интерфейсу нужно поддерживать их все и перенаправлять вызовы объекту. 406 | 407 | ```cpp 408 | class Particle 409 | { 410 | // Несколько способов инициализации. 411 | void init(double x, double y); 412 | void init(double x, double y, double angle); 413 | void init(double x, double y, double xVel, double yVel); 414 | }; 415 | 416 | class ParticlePool 417 | { 418 | public: 419 | void create(double x, double y) 420 | { 421 | // Переход к частице... 422 | } 423 | 424 | void create(double x, double y, double angle) 425 | { 426 | // Переход к частице... 427 | } 428 | 429 | void create(double x, double y, double xVel, double yVel) 430 | { 431 | // Переход к частице... 432 | } 433 | }; 434 | ``` 435 | 436 | - **Если объект инициализируется из внешнего кода:** 437 | 438 | - _Интерфейс пула может быть проще_. Вместо предоставления нескольких функций для сокрытия всех возможных способов инициализации объекта, пул может просто возвращать ссылку на новый объект. 439 | 440 | ```cpp 441 | class Particle 442 | { 443 | public: 444 | // Несколько способов инициализации. 445 | void init(double x, double y); 446 | void init(double x, double y, double angle); 447 | void init(double x, double y, double xVel, double yVel); 448 | }; 449 | 450 | class ParticlePool 451 | { 452 | public: 453 | Particle* create() 454 | { 455 | // Возвращаем ссылку на доступную частицу... 456 | } 457 | private: 458 | Particle pool_[100]; 459 | }; 460 | ``` 461 | 462 | После этого вызывающий код может инициализировать объект с помощью любого метода, демонстрируемого объектом. 463 | 464 | ```cpp 465 | ParticlePool pool; 466 | 467 | pool.create()->init(1, 2); 468 | pool.create()->init(1, 2, 0.3); 469 | pool.create()->init(1, 2, 3.3, 4.4); 470 | ``` 471 | 472 | - _Внешнему коду придется обрабатывать ошибку при создании нового объекта_. Предыдущий пример предполагает что `create()` всегда будет успешно заканчиваться возвращением указателя на объект. Если пул переполнен, он может возвращать `NULL`. Чтобы делать это безопасно, вам нужно добавить проверку перед попыткой инициализации объекта. 473 | 474 | ```cpp 475 | Particle* particle = pool.create(); 476 | if (particle != NULL) particle->init(1, 2); 477 | ``` 478 | 479 | 480 | ## Смотрите также 481 | 482 | - Довольно похоже на шаблон [Приспособленец (Flyweight)GoF](../obzor-shablonov-proektirovaniya/prisposoblenets-flyweight.md). Оба поддерживают коллекцию повторно используемых объектов. Разница в том что означает это "повторное использование". Объекты приспособленца используются повторно, разделяя один и тот же экземпляр между множеством обладателей _одновременно_. Таким образом мы избегаем _дублирования_ использования памяти, используя один и тот же объект в нескольких контекстах. 483 | 484 | Объекты в пуле тоже можно использовать повторно, но только через некоторое время. "Повторное использование" в контексте пула объектов означает повторное использование памяти объекта _после_ того как предыдущий владелец ее освободит. Когда мы используем пул объектов, мы не рассчитываем что объект будет разделяться между несколькими владельцами во время его жизни. 485 | 486 | - Упаковка множества объектов одного типа вместе в памяти позволяет вам держать кэш процессора заполненным, пока игра обходит все его объекты. Шаблон [Локальность данных (Data Locality)](lokalnost-dannih-data-locality.md) как раз об этом. -------------------------------------------------------------------------------- /posledovatelnie-shabloni-sequencing-patterns/metodi-obnovleniya-update-methods.md: -------------------------------------------------------------------------------- 1 | # Методы обновления \(Update Methods\) 2 | 3 | ## Задача 4 | 5 | _Симуляция коллекции независимых объектов с помощью указания каждому объекту обработки одного кадра поведения за раз._ 6 | 7 | ## Мотивация 8 | 9 | 10 | Могучая валькирия игрока выполняет квест по краже прекрасных украшений с трупа давно умершего короля-волшебника. Она приближается ко входу величественной усыпальницы и ее атакует... _ничего_. Никаких проклятых статуй, стреляющих молниями. Никаких воинов нежити, патрулирующих вход. Она просто заходит. Забирает лут. Игра окончена. Вы выиграли. 11 | 12 | Ну нет. Так не пойдет. 13 | 14 | Гробнице нужны стражи - противники, с которыми сможет побороться наша героиня. Для начала нам понадобятся ожившие скелеты воины, патрулирующие вход. Если вы проигнорируете все что знаете об игровом программировании, простейший код, перемещающий скелетов туда и сюда будет выглядеть так: 15 | 16 | > Если королю-волшебнику нужно более интеллектуальное поведение, у него должно остаться хоть что-то от мозгов. 17 | 18 | ```cpp 19 | while (true) 20 | { 21 | // Патрулируем вправо. 22 | for (double x = 0; x < 100; x++) 23 | { 24 | skeleton.setX(x); 25 | } 26 | 27 | // Патрулируем влево. 28 | for (double x = 100; x > 0; x--) 29 | { 30 | skeleton.setX(x); 31 | } 32 | } 33 | ``` 34 | 35 | Проблема здесь в том что хотя скелеты и двигаются туда-сюда, но игрок их не видит. Программа зациклена в бесконечном цикле и никакого игрового процесса тут нет. Чего мы на самом деле хотим добиться - так это того чтобы скелеты двигались на _каждом кадре_. 36 | 37 | Уберем эти циклы и переложим работу на уже существующий цикл. Это позволит игре реагировать на пользовательский ввод и рендерить врагов во время их перемещения. Вот так: 38 | 39 | > [Игровой цикл](igrovoi-tsikl-game-loop.md) - это еще один шаблон, описанный в книге. 40 | 41 | ```cpp 42 | Entity skeleton; 43 | bool patrollingLeft = false; 44 | double x = 0; 45 | 46 | // Главный игровой цикл: 47 | while (true) 48 | { 49 | if (patrollingLeft) 50 | { 51 | x--; 52 | if (x == 0) patrollingLeft = false; 53 | } 54 | else 55 | { 56 | x++; 57 | if (x == 100) patrollingLeft = true; 58 | } 59 | 60 | skeleton.setX(x); 61 | 62 | // Обработка игрового ввода и рендеринг игры... 63 | } 64 | ``` 65 | 66 | Я привожу здесь код до и после, чтобы показать вам насколько код усложнился. Патрулирование влево и вправо может быть простым циклом `for`. Они даже косвенно следят за направлением скелетов в зависимости от запущенного цикла. Но теперь нам придется на каждом кадре прерывать выполнение и переходить в игровой цикл, а потом продолжать с того места, на котором мы остановились. А чтобы определять направление движения нам придется использовать дополнительную переменную `patrollingLeft`. 67 | 68 | Этот вариант уже более-менее рабочий. Безмозглый мешок с костями не составит вашей Северной воительнице серьезную конкуренцию, так что следующее что мы добавим - это зачарованные статуи. Они будут стрелять молниями так часто что героине придется прокрадываться мимо них на цыпочках. 69 | 70 | Продолжая в нашем "максимально простом стиле ", получим вот что: 71 | 72 | ```cpp 73 | // Переменные скелетов... 74 | Entity leftStatue; 75 | Entity rightStatue; 76 | int leftStatueFrames = 0; 77 | int rightStatueFrames = 0; 78 | 79 | // Основной игровой цикл: 80 | while (true) 81 | { 82 | // Код скелетов... 83 | 84 | if (++leftStatueFrames == 90) 85 | { 86 | leftStatueFrames = 0; 87 | leftStatue.shootLightning(); 88 | } 89 | 90 | if (++rightStatueFrames == 80) 91 | { 92 | rightStatueFrames = 0; 93 | rightStatue.shootLightning(); 94 | } 95 | 96 | // Обработка игрового ввода и рендеринг игры... 97 | } 98 | ``` 99 | 100 | Не могу сказать что мы движемся в сторону кода, который легко и приятно поддерживать. У нас появилась куча новых переменных, а внутри игрового цикла образовалось месиво из кода, каждая часть которого обрабатывает отдельную сущность в игре. Чтобы они все работали одновременно, у нас получилось месиво относящегося к ним кода. 101 | 102 | > Каждый раз когда ваш код можно описать словом "месиво" - у вас явно есть проблема. 103 | 104 | 105 | 106 | Шаблон, который мы будем использовать для решения этой проблемы настолько прост, что вы наверное уже и сами до него додумались: _каждая сущность в игре инкапсулирует собственное поведение_. Таким образом наш игровой цикл становится лаконичным и мы получаем возможность обрабатывать любое количество сущностей. 107 | 108 | Чтобы это сделать, нам нужен уровень _абстракции_ и мы создаем его, определяя абстрактный метод `update()`. Игровой цикл поддерживает коллекцию объектов, но не знает их конкретный тип. Все что о них нужно знать - это то что их нужно обновлять. Таким образом мы отделяем поведение каждого объекта от игрового цикла и других объектов. 109 | 110 | На каждом кадре игровой цикл проходит по всей коллекции и вызывает `update()` для каждого объекта. Таким образом каждый объект может обновить свое поведение на один кадр. Вызывая его для объектов на каждом кадре, мы получаем их одновременное действие. 111 | 112 | > Так как мне обязательно кто-то это припомнит, сознаюсь сразу - да, они не работают в _полностью конкурентном режиме_. Пока один из объектов обновляется, все остальные этого не делают. Мы еще вернемся к этому позже. 113 | 114 | 115 | Игровой цикл содержит динамическую коллекцию объектов, так что добавлять и удалять объекты с уровня довольно просто - просто добавляем или удаляем их из коллекции. Больше никакого хардкодинга. Более того, уровень можно наполнить объектами из внешнего файла с данными, т.е. получаем как раз то что нужно геймдизайнерам. 116 | 117 | ## Шаблон 118 | 119 | **Игровой мир** содержит **коллекцию объектов**. Каждый объект реализует **метод обновления, симулирующий один кадр** поведения объекта. На каждом кадре игра обновляет каждый объект из коллекции. 120 | 121 | ## Когда использовать 122 | 123 | Если шаблон [Игровой цикл](igrovoi-tsikl-game-loop.md) можно сравнить с хлебом, то этот шаблон можно назвать маслом. В той или иной форме этот шаблон использует огромное число игр, в которых есть живые сущности. Если в игре есть космодесантники, драконы, марсиане, призраки или атлеты, скорее всего она использует этот шаблон. 124 | 125 | Однако, если игра более абстрактная и движущиеся части в ней не намного живее, чем шахматные фигуры, этот шаблон может и не понадобиться. В играх наподобие шахмат вам нет необходимости моделировать поведение всех игровых объектов одновременно и возможно вам не придется просить пешку обновить себя на каждом кадре. 126 | 127 | > Вам не нужно обновлять их _поведение_ на каждом кадре, но даже в настольной игре, вам возможно придется обновлять на каждом кадре _анимацию_. В этом вам шаблон тоже может помочь. 128 | 129 | Метод обновления хорошо работает когда: 130 | 131 | - В вашей игре есть некоторое количество объектов или систем, которые должны работать одновременно. 132 | 133 | - Поведение каждого объекта практически не зависит от остальных. 134 | 135 | - Объекты нужно обновлять постоянно. 136 | 137 | ## Имейте в виду 138 | 139 | Этот шаблон достаточно прост чтобы скрывать в себе какие-то неприятные сюрпризы. Тем не мене каждая строка кода имеет последствия. 140 | 141 | ### Разделение кода на срезы отдельных кадров делает его сложнее. 142 | 143 | Если вы сравните два первые примера кода, вы увидите что второй уже значительно сложнее. Оба они заставляют скелет просто ходить туда-сюда, но второй делает это с перерывами на передачу управления коду игрового цикла на каждом кадре. 144 | 145 | Такое изменение практически всегда необходимо для обработки пользовательского ввода, рендеринга и других вещей, о которых приходится заботиться игре, так что первый пример навряд ли можно назвать типичным. Зато он наглядно демонстрирует насколько усложняется поведенческий код, когда его приходится преобразовывать подобным образом. 146 | 147 | > Я говорю "практически" потому что иногда и такое возможно. У вас вполне может быть прямолинейный код, который никогда не отвлекается на поведение объектов и в то же время у вас может быть множество одновременно обновляющихся объектов, работающих в конкурентном режиме и управляемых игровым циклом. 148 | > 149 | > Все что вам нужно - это система, поддерживающая множество "потоков" выполнения, работающих одновременно. Если код объекта можно просто остановить во время выполнения и потом продолжить, вместо того чтобы полностью выходить из него, такой код можно писать в более императивной манере. 150 | > 151 | > Настоящие потоки обычно слишком тяжеловесны чтобы хорошо работать, но если ваш язык поддерживает легковесные конкурентные конструкции наподобие генераторов (generators), сопрограмм (coroutines) или нитей (fibers), вы можете использовать их. 152 | > 153 | > Еще одним способом создания потоков выполнения на уровне приложения является использование шаблона [Байткод(Bytecode)](../povedencheskie-shabloni-behavioral-patterns/baitkod-bytecode.md). 154 | 155 | 156 | ### Вам нужно сохранять состояние чтобы продолжать с того места где вы прервались 157 | 158 | 159 | В первом примере кода у нас не было никаких переменных, определяющих налево идет стражник или направо. Это явно следовало из выполняющегося кода. 160 | 161 | Когда мы поменяли этот код на код вида кадр за раз, нам пришлось добавить переменную `patrollingLeft`, чтобы за этим следить. Когда мы возвращаемся к нашему коду, предыдущая позиция теряется и нам нужно хранить достаточно информации чтобы восстановить состояние перед следующими кадром. 162 | 163 | Здесь нам может помочь шаблон [Состояние(State)](../obzor-shablonov-proektirovaniya/sostoyanie-state.md). Машины состояний (state machines) и их разновидности так часто встречаются в играх отчасти потому, что (что следует из их имени) они хранят состояние, которым вы можете воспользоваться после того как отвлечетесь на что-то. 164 | 165 | 166 | ### Объекты симулируются на каждом кадре, но не в настоящем конкурентном режиме 167 | 168 | 169 | В этом шаблоне игровой цикл перебирает всю коллекцию объектов и обновляет каждый из них. Внутри вызова `update()`, большинство объектов могут получить доступ ко всему остальному игровому миру, включая другие объекты, которые обновляются. Это значит что _порядок_, в котором объекты обновляются начинает иметь значение. 170 | 171 | Если обновление A происходит перед B в списке обновления, тогда во время обновления A, оно видит предыдущее состояние B. Но когда обновляется B, оно уже видит A в _новом_ состоянии, потому что A на этом кадре уже обновлялось. Даже если с точки зрения игрока все происходит одновременно, ядро игры все равно работает в пошаговом режиме. Просто один полный "шаг" соответствует по длительности одному кадру. 172 | 173 | > Если по какой либо причине, вы решили что ваша игра не должна работать в таком последовательном режиме, вам может помочь нечто наподобие шаблона [Двойная буферизация (Double Buffer)](dvoinaya-buferizatsiya-double-buffering.md). В таком случае порядок обновления A и B перестанет играть какую-либо роль, потому что и тот и другой объект будут видеть предыдущее состояние другого. 174 | 175 | 176 | Пока логика игры не предполагает тесного связывания это нормально. Обновление объектов в параллельном режиме таит в себе некоторые неприятные сюрпризы. Представьте себе шахматы, где белые и черные ходят одновременно. Обе стороны пытаются переставить фигуру в пустую клетку. Как же можно решить эту проблему? 177 | 178 | Последовательное обновление эту проблему решает: каждое обновление постепенно изменяет мир из одного состояния в другое без промежуточного времени, когда вещи находятся в двойственном состоянии и требуют уточнения. 179 | 180 | > Еще это помогает при разработке сетевых игр, потому что у вас есть сериализуемый набор шагов, которые можно передавать по сети. 181 | 182 | 183 | ### Будьте осторожны при изменении списка объектов во время обновления 184 | 185 | 186 | Когда вы используете этот шаблон, на метод обновления обязательно завязывается слишком большая часть поведения игры. Обычно сюда относится и код, добавляющий и удаляющий обновляемые объекты в игре. 187 | 188 | Например, наш скелет охранник будет дропать предмет после смерти. Новый объект вы можете без проблем просто добавить в конце списка обновляемых объектов. Вы идете по списку объектов дальше, доходите до только что добавленного нового объекта и обновляете и его тоже. 189 | 190 | Только это совсем не значит, что новый объект имеет право действовать в том же кадре, когда его добавили, до того как игрок смог его хотя бы увидеть. Если вы не хотите чтобы это произошло, можно применить одну хитрость и кешировать в начале цикла обновления количество объектов в списке и обновлять только такое их количество: 191 | 192 | ```cpp 193 | int numObjectsThisTurn = numObjects_; 194 | for (int i = 0; i < numObjectsThisTurn; i++) 195 | { 196 | objects_[i]->update(); 197 | } 198 | ``` 199 | 200 | Здесь `objects_` - это массив обновляемых объектов в игре, а `numObjects_` - его длина. Когда добавляется новый объект, длина увеличивается на единицу. Мы кэшируем длину в `numObjectsThisTurn` в начале цикла, так что итерация останавливается перед тем как мы доберемся до объектов, добавленных на текущем кадре. 201 | 202 | Проблема усложняется если мы выполняем _удаление_ во время итерации. Вы убиваете глупого монстра и теперь его нужно выкинуть из списка объектов. Если он находится в списке до объекта, который вы сейчас обновляете, вы можете случайно пропустить объект: 203 | 204 | ```cpp 205 | for (int i = 0; i < numObjects_; i++) 206 | { 207 | objects_[i]->update(); 208 | } 209 | ``` 210 | 211 | Этот простейший цикл инкрементирует индекс обновляемого объекта на каждой итерации. В левой части иллюстрации ниже показано как массив выглядит пока мы обновляем героиню. 212 | 213 | ![3-3-1.1](../assets/3-3-1.1.png) 214 | 215 | Так как мы обновляем героиню, i равняется 1. Она убивает монстра и он удаляется из массива. Героиня перемещается на позицию 0 и несчастный крестьянин перемещается на позицию 1. После обновления героини, i инкрементируется до 2. Как вы можете видеть справа, несчастного крестьянина пропустили и он никогда не будет обновлен. 216 | 217 | > Простейшее решение - это двигаться по списку объектов в обратную сторону. Таким образом удаление объекта сместит только уже обновленные объекты. 218 | 219 | 220 | 221 | Можно просто быть более аккуратным при удалении объектов и обновлять любые итерационные переменные, учитывая удаление. Или же можно подождать с удалением до тех пор, пока мы не обойдем весь список. Пометьте объект как "мертвый", но сразу не удаляйте. Во время обновления мы пропускаем мертвые объекты. А когда закончили - проходим список еще раз и удаляем все мертвые объекты. 222 | 223 | > Если вы обновляете объекты в цикле обновления в многопоточном режиме, вам тем более стоит повременить с любыми изменениями списка объектов, чтобы избежать расходов на синхронизацию во время обновления. 224 | 225 | 226 | ## Пример кода 227 | 228 | Шаблон настолько прямолинеен, что пример кода просто топчется на месте. Это совсем не значит что шаблон бесполезен. Он очень полезен _потому что_ он простой: это просто решение проблемы без всяких украшений. 229 | 230 | Но чтобы говорить более конкретно, давайте пройдемся по нескольким реализациям. Начнем с класса сущности, представляющей скелет или статую. 231 | 232 | ```cpp 233 | class Entity 234 | { 235 | public: 236 | Entity() : x_(0), y_(0) {} 237 | 238 | virtual ~Entity() {} 239 | virtual void update() = 0; 240 | 241 | double x() const { return x_; } 242 | double y() const { return y_; } 243 | 244 | void setX(double x) { x_ = x; } 245 | void setY(double y) { y_ = y; } 246 | 247 | private: 248 | double x_; 249 | double y_; 250 | }; 251 | ``` 252 | 253 | Я добавил в нее несколько вещей, но это самый минимум того, что понадобится нам в дальнейшем. Смею предположить что в реальном коде будет гораздо больше всяких связанных с графикой или физикой штук. Самое важное для нашего шаблона заключается в том, что у сущности есть абстрактный метод `update()`. 254 | 255 | Игра поддерживает коллекцию таких сущностей. В нашем примере мы поместим ее в класс, представляющий игровой мир: 256 | 257 | > В настоящей программе, вы скорее всего будете использовать специальный класс-коллекцию, но для упрощения я использую в своем примере обычный массив. 258 | 259 | ```cpp 260 | class World 261 | { 262 | public: 263 | World() : numEntities_(0) {} 264 | 265 | void gameLoop(); 266 | 267 | private: 268 | Entity* entities_[MAX_ENTITIES]; 269 | int numEntities_; 270 | }; 271 | ``` 272 | 273 | Теперь когда все готово, игра реализует шаблон, обновляя на каждом кадре все сущности: 274 | 275 | > Как следует из названия метода - это пример применения шаблона [Игровой цикл](igrovoi-tsikl-game-loop.md). 276 | 277 | ```cpp 278 | void World::gameLoop() 279 | { 280 | while (true) 281 | { 282 | // Обработка пользовательского ввода... 283 | 284 | // Обновление каждой из сущностей. 285 | for (int i = 0; i < numEntities_; i++) 286 | { 287 | entities_[i]->update(); 288 | } 289 | 290 | // Физика и рендеринг... 291 | } 292 | } 293 | ``` 294 | 295 | ### Сущности подклассы? 296 | 297 | Уверен что у некоторых читателей уже пошли мурашки по коже, когда они увидели как я наследую от главного класса сущности для определения специального поведения. Если вы не видите в этом проблемы, я введу вас в курс дела. 298 | 299 | Когда игровая индустрия вышла из первобытного супа ассемблерного кода 6502 и синхронизации с обратным ходом луча в мир объектно-ориентированных языков, разработчики попали в безумный мир архитектуры программного обеспечения. Одной из любимых возможностей было наследование. И была воздвигнута византийская башня наследования, такая высокая, что заслоняла собой солнце. 300 | 301 | Это была ужасная идея и никто не мог поддерживать гигантскую гору иерархии классов без того чтобы быть под ней погребенным. Даже банда четырех уже предупреждала об этом в еще 1994-м году: 302 | 303 | _Предпочитайте "композицию объектов" "наследованию классов"._ 304 | 305 | > Только между нами, мне кажется маятник слишком далеко качнулся от подклассов. Обычно я их избегаю, но не стоит возводить неиспользование наследования в догму, также как не стоит превращать в догму его использование. Используйте эту возможность осмотрительно и трезво оценивая последствия. 306 | 307 | Когда это понимание проникло в игровую индустрию, решение виделось в шаблоне [Компонент (Component)](../shabloni-snizheniya-svyaznosti-decoupling-patterns/komponent-component.md). Если мы его применим, наш `update()` станет _компонентом_ сущности, а не самой `Entity`. Таким образом нам не нужно будет организовывать сложную иерархию сущностей чтобы определять и повторно использовать поведение. Вместо этого мы можем смешивать и сопоставлять компоненты. 308 | 309 | > [Вот такие](../shabloni-snizheniya-svyaznosti-decoupling-patterns/komponent-component.md). 310 | 311 | Если бы мы писали настоящую игру, я бы тоже так поступил. Но эта глава не про компонент. Она о методе `update()` и проще всего увидеть как он работает с минимальным количеством дополнительных деталей - это поместить метод непосредственно в `Entity` и унаследовать от нее несколько подклассов. 312 | 313 | # Определение сущностей 314 | 315 | Хорошо, вернемся к нашей задаче. Изначально мы хотели реализовать патрулирование скелета-охранника и работу стреляющих молниями статуй. Начнем с нашего костлявого друга. Чтобы определить его патрульное поведение, создаем новую сущность с соответствующей реализацией `update()`: 316 | 317 | ```cpp 318 | class Skeleton : public Entity 319 | { 320 | public: 321 | Skeleton() : patrollingLeft_(false) {} 322 | 323 | virtual void update() 324 | { 325 | if (patrollingLeft_) 326 | { 327 | setX(x() - 1); 328 | if (x() == 0) patrollingLeft_ = false; 329 | } 330 | else 331 | { 332 | setX(x() + 1); 333 | if (x() == 100) patrollingLeft_ = true; 334 | } 335 | } 336 | 337 | private: 338 | bool patrollingLeft_; 339 | }; 340 | ``` 341 | 342 | Как вы видите мы практически выдрали этот кусок кода из игрового цикла, который мы писали раньше и поместили его в метод `update()` класса `Skeleton`. Единственное серьезное отличие только в том что `patrollingLeft_` превратилось в поле из обычной локальной переменной. Таким образом значение будет сохраняться между вызовами `update()`. 343 | 344 | Давайте теперь перейдем к статуям: 345 | 346 | ```cpp 347 | class Statue : public Entity 348 | { 349 | public: 350 | Statue(int delay) : frames_(0), delay_(delay) {} 351 | 352 | virtual void update() 353 | { 354 | if (++frames_ == delay_) 355 | { 356 | shootLightning(); 357 | 358 | // Сброс таймера. 359 | frames_ = 0; 360 | } 361 | } 362 | 363 | private: 364 | int frames_; 365 | int delay_; 366 | 367 | void shootLightning() 368 | { 369 | // Стрельба молнией... 370 | } 371 | }; 372 | ``` 373 | 374 | И опять мы практически просто перенесли сюда старый код из игрового цикла и кое-что переименовали. Таким образом мы просто упрощаем кодовую базу. В оригинальном скученном императивном коде у нас были отдельные локальные переменные и для счетчика кадров статуи и для частоты стрельбы. 375 | 376 | Теперь когда мы перенесли их в сам класс `Statue`, вы можете создать столько переменных сколько нужно и у каждого экземпляра будет собственный таймер. Вот она истинная цель шаблона: теперь нам гораздо проще добавлять новые сущности в игровой мир, потому что каждая из них располагает всем необходимым чтобы о себе позаботиться. 377 | 378 | Шаблон позволяет нам населять мир, а не _реализовывать_ его. А еще мы получаем дополнительную гибкость за счет наполнения мира через отдельный файл данных или редактор уровня. 379 | 380 | > Людям еще интересен UML? Если да, то ~~я сделяль~~ вот что я нарисовал. 381 | 382 | ![3-3-1.2](../assets/3-3-1.2.png) 383 | 384 | 385 | ## Ход времени 386 | 387 | Это была суть шаблона, но я хочу показать вам еще несколько вариаций. Пока что мы предполагали что каждый вызов `update()` продвигает состояние игрового мира на фиксированный отрезок времени. 388 | 389 | Мне такой подход нравится, однако многие игры используют _переменный временной шаг_. В этом случае игровой цикл симулирует более длинные или более короткие отрезки времени в зависимости от того сколько потребовалось времени на обработку и рендеринг предыдущего кадра. 390 | 391 | > В главе [Игровой цикл](igrovoi-tsikl-game-loop.md) как раз описаны достоинства и недостатки фиксированных и переменных временных шагов. 392 | 393 | Это значит что каждому вызову `update()` необходимо знать насколько продвинулись виртуальные часы. Поэтому внутрь передается количество прошедшего времени. Например наш скелет патрульный может обрабатывать переменный временной шаг следующим образом: 394 | 395 | ```cpp 396 | void Skeleton::update(double elapsed) 397 | { 398 | if (patrollingLeft_) 399 | { 400 | x -= elapsed; 401 | if (x <= 0) 402 | { 403 | patrollingLeft_ = false; 404 | x = -x; 405 | } 406 | } 407 | else 408 | { 409 | x += elapsed; 410 | if (x >= 100) { 411 | patrollingLeft_ = true; 412 | x = 100 - (x - 100); 413 | } 414 | } 415 | } 416 | ``` 417 | 418 | Теперь расстояние, проходимое скелетом увеличивается вместе с увеличением обрабатываемого времени. Как вы видите работа с переменным временным отрезком увеличивает сложность кода. Скелет должен следить за тем чтобы не выйти за рамки зоны патрулирования во время долгого временного отрезка и нам нужно корректно обрабатывать этот случай. 419 | 420 | ## Архитектурные решения 421 | 422 | В таком простом шаблоне не слишком много разнообразия, но все таки есть некоторые настройки. 423 | 424 | ### В каком классе будет жить метод обновления? 425 | 426 | Самое главное решение, которое вам нужно принять - это внутри какого класса разместить `update()`. 427 | 428 | 429 | - **Класс сущности:** 430 | 431 | Это простейшее решение, если у вас уже есть класс сущности, потому что вам не нужно будет вводить в игру никаких дополнительных классов. Это может сработать если у вас не слишком много видов сущностей, но в целом индустрия уходит от такого подхода. 432 | 433 | Создавать подкласс `Entity` каждый раз когда вам нужно новое поведение - довольно хрупкий и болезненный метод, если видов сущностей у вас достаточно много. Вскоре вы обнаружите что вам хочется повторно использовать куски кода, не слишком хорошо укладывающиеся в стройную иерархию и застрянете на этом. 434 | 435 | - **Класс компонент:** 436 | 437 | Если вы уже используете шаблон [Компонент](../shabloni-snizheniya-svyaznosti-decoupling-patterns/komponent-component.md), то тут и думать нечего. Он позволяет каждому компоненту обновляться независимо. Подобно тому как работает метод обновления для снижения связности в игре в целом, этот шаблон позволяет вам снизить связность _частей отдельной сущности_ друг от друга. Рендеринг, физика и AI могут позаботиться о себе самостоятельно. 438 | 439 | - **Класс делегат:** 440 | 441 | Есть еще один шаблон, который предполагает делегирование части поведения класса другому объекту. Шаблон [Cостояние](../obzor-shablonov-proektirovaniya/sostoyanie-state.md) именно этим и занимается, так что вы можете менять поведение объекта, подменяя объект к которому он делегирует. Шаблон [Объект тип (Type Object)](../povedencheskie-shabloni-behavioral-patterns/obekt-tip-type-object.md) делает это, позволяя вам разделять поведение между несколькими сущностями одного "вида". 442 | 443 | Если вы используете один из этих шаблонов, поместить `update()` в класс к которому мы делегируем будет вполне логичным решением. В этом случае у вас все еще может быть метод `update()` в главном классе, но он уже будет не виртуальным, а просто будет вызывать объект делегат. Примерно так: 444 | 445 | ```cpp 446 | void Entity::update() 447 | { 448 | // Forward to state object. 449 | state_->update(); 450 | } 451 | ``` 452 | 453 | Это позволяет вам определять новое поведение, изменяя объект делегат. Как при использовании компонентов, он дает вам гибкость в изменении поведения без необходимости определять полностью новый подкласс. 454 | 455 | 456 | ### Как обрабатываются спящие объекты? 457 | 458 | Очень часто у вас в игровом мире присутствуют объекты, которые по какой-либо причине не нужно обновлять. Они могут быть отключенными, находящимися за пределами экрана или еще не разблокированными. Если у вас в этом состоянии находится довольно большое количество объектов, может быть довольно расточительно в плане процессорного времени проходить их все во время обновления каждого кадра. 459 | 460 | Одним из решений является создание отдельной коллекции объектов только с "живыми" объектами, которые требуют обновления. Когда объект становится неактивным, он удаляется из этой коллекции. Когда включается снова - добавляется обратно. Таким образом мы будем проходить только по списку активных объектов. 461 | 462 | 463 | - **Если вы используете единственную коллекцию интерактивных объектов:** 464 | 465 | - _Вы впустую тратите время_. Для интерактивных объектов вам придется добавлять проверку флага в духе "активен ли я" или вызывать пустой метод. 466 | 467 | В дополнение к потерянным процессорным тактам на проверку активности объекта, бесполезный перебор объектов приводит к ошибкам обращения к кешу. Процессор спроектирован таким образом что быстрее читает данные из кеша, а не из памяти. Лучше всего это получается когда следующий читаемый объект находится сразу за предыдущим. 468 | 469 | Когда вы переходите к следующему объекту, вы можете пропустить оставшуюся в кеше порцию данных, заставляя процессор загружать следующую порцию данных из обычной памяти. 470 | 471 | - **Если вы используете отдельную коллекцию с активными объектами:** 472 | 473 | - _Вы используете дополнительную память для хранения второй коллекции_. В случае если у вас активны все объекты - это будет просто вторая копия первой коллекции. В таком случае она явно избыточна. Но когда скорость важнее памяти (а обычно так и есть) это допустимая потеря. 474 | 475 | Еще одно решение - это перейти к двум коллекциям, но теперь во второй будут храниться только _неактивные_ объекты. 476 | 477 | - _Вам нужно поддерживать синхронизацию коллекций_. Когда объект создается или полностью уничтожается (или временно отключается), вам нужно удалить или изменить его как в основной коллекции, так и в коллекции активных объектов. 478 | 479 | 480 | Выбрать что вам больше подходит можно проанализировав сколько неактивных объектов может у вас быть. Чем их больше, тем полезнее использовать для них отдельную коллекцию, чтобы не отделять их прямо внутри игрового цикла. 481 | 482 | ## Смотрите также 483 | 484 | 485 | - Этот шаблон вместе с [Игровым циклом](igrovoi-tsikl-game-loop.md) и [Компонентом](../shabloni-snizheniya-svyaznosti-decoupling-patterns/komponent-component.md) является частью троицы, обычно образующей сердце современного игрового движка. 486 | 487 | - Когда вы начинаете задумываться о производительности обновления кучи сущностей или компонентов на каждом цикле, вам может прийти на помощь шаблон [Локализация данных (Data Locality)](../shabloni-optimizatsii/lokalnost-dannih-data-locality.md). 488 | 489 | - Фреймворк [Unity](http://unity3d.com/) использует этот шаблон в нескольких классах, включая [MonoBehaviour](http://docs.unity3d.com/ScriptReference/MonoBehaviour.Update.html). 490 | 491 | - Платформа Microsoft [XNA](http://xbox.create.msdn.com/en-US/) использует этот шаблон в классах Game и [GameComponent](http://msdn.microsoft.com/ru-ru/en-us/library/microsoft.xna.framework.gamecomponent.update.aspx). 492 | 493 | - Игровой движок на JavaScript [Quintus](http://html5quintus.com/) использует этот шаблон в главном классе [Sprite](http://html5quintus.com/guide/sprites.md#.U5K0i_lLKTo). 494 | 495 | 496 | --------------------------------------------------------------------------------