Предыдущие части: Лыко-мочало, Время жизни, Графическая прокладка, Дженерики, Композиция, Модули, Начальное проектирование, Итоги про память, Что там с памятью, Колхозим интерфейсы, Где у него классы, Поддержка SDL2, Полируем ржавчину
По итогам предыдущей части я немного пересмотрел архитектуру приложения.
Сцена (Stage) будет отвечать не только за графику, но и за все игровые объекты – так как игровой объект может вообще не иметь отображения на экране, но участвовать в игре.
Игровые объекты будут называться GameObject, а для отображения будут иметь ссылку на объект с трейтом Renderer. С него и начнём.
Данный трейт (или может кому-то удобнее считать его интерфейсом) содержит один метод render(), которому нужны: ссылка на self, мутабельная ссылка на холст, где происходит рисование – структуру WindowCanvas, и ссылка на GameObject, который нужно нарисовать.
Подразумевается, что у разных GameObject могут быть разные реализации Renderer, например, один рисуется как цветной квадратик, а другой как картинка.
Я пока сделаю цветной квадратик. Для этого напишу реализацию RendererRect:
Пока что всё хорошо. Ссылки используются, но указывать для них время жизни не требуется, так как они нигде не сохраняются и не возвращаются.
Теперь надо описать GameObject:
В нём используется ссылка на Renderer, и поэтому сразу требуется уточнить времена жизни. На слово dyn не обращайте внимания, оно просто нужно для ссылок на типы-трейты, сейчас это неважно.
Важно то, что указано время жизни под названием 'gmo. Необходимо уточнить информацию, описанную ранее здесь:
Если просто исправлять ошибки компилятора его же подсказками, мы можем получить работающую программу, но работать она может скорее вопреки, а не благодаря. Я всё ещё плаваю в вопросе задания времени жизни, поэтому буду уточнять эти моменты в следующие разы, по мере освоения.
Итак, что такое время 'gmo, указанное в описании struct GameObject?
Оно не существует нигде в программе и ни с чем не связано. Его можно назвать как угодно. Всё, что мы делаем с его помощью, это говорим следующее:
"Есть структура GameObject, которая где-то в программе будет жить какое-то условное время 'gmo..."
Когда объект GameObject создаётся в программе, это может произойти где угодно: внутри функции, в цикле, в одном месте кода или позже / раньше него. Это всё неважно. Важно, что этот конкретный объект проживёт какое-то время. У одного объекта оно будет одно, у другого другое. И вот любое из них мы называем условно 'gmo.
При создании объекта GameObject требуется передать ему ссылку на Renderer. Объект Renderer порождается отдельно от GameObject. Значит, он имеет время жизни, независимое от GameObject. Но так как GameObject содержит ссылку на Renderer, эта ссылка должна быть валидна, пока жив GameObject.
Значит, объект Renderer должен жить не меньше, чем GameObject. Поэтому мы и указываем для ссылки на него время жизни 'gmo, и всё вместе получается так:
"Есть структура GameObject, которая где-то в программе будет жить какое-то условное время 'gmo, и она содержит ссылку на другой объект, и у этого объекта время жизни должно быть не меньше, чем это же условное время 'gmo"
За всем остальным Rust проследит уже сам. Мы задали не время жизни структуры как таковое, а только лишь правило хранения ссылки в этой структуре для любого возможного времени жизни.
Эти правила на начальном этапе полезно проговаривать для понимания, поэтому я добавляю комментарий:
Теперь имплементация метода new() для GameObject:
Я использовал здесь время 'rend специально для того, чтобы подчеркнуть, что оно никак не связано с 'gmo (иначе мы интуитивно захотели бы написать 'gmo и стали бы думать, что названия времён для GameObject должны быть одинаковыми – нет, не должны).
Рассмотрим подробнее, что происходит здесь.
impl<'rend> - в данном случае 'rend используется в дженерике, чтобы просто объявить имя. Без него получится ошибка о неизвестном имени.
Дальше мы делаем всё по тому же принципу:
Структура GameObject, которая живёт любое время 'rend, принимает и сохраняет у себя ссылку на тип-трейт Renderer, которая должна жить не меньше чем 'rend.
Перейдём к Stage:
Здесь уже должно быть чуть полегче: структура Stage содержит вектор obj_list, состоящий из объектов GameObject, и если время жизни Stage равно 'st, то у объектов в векторе оно должно быть не меньше чем 'st.
Я отказался от хранения в векторе ссылок на объекты, там хранятся сами объекты. При этом внутренняя реализация объекта класса Vec хранит его элементы в куче, в том время как сам объект (его служебная информация) хранится на стеке.
Реализация метода add_child() для добавления объекта в список:
Объект child передаётся в метод извне, и тот, кто его передал, теряет над ним владение. Теперь им владеет переменная child внутри метода. Когда child попадает в вектор (с помощью push()), то владение переходит к вектору, а переменная child становится невалидной. Исправить это можно путём копирования, но по сути оно здесь и так происходит: объект создаётся в стеке, передаётся через стек, а при добавлении в вектор должен быть перекопирован в кучу. Тем не менее, владение всё равно переходит. В данном случае это не волнует, пусть переходит.
Реализация метода вывода на экран всех объектов:
В этом методе не используются времена жизни. Мы проходим циклом по вектору и получаем ссылки на каждый его элемент. Важно, что они не мутабельные, поэтому можно получать их в любом количестве.
Далее у объекта GameObject, который находится по ссылке, берётся присвоенный ему Renderer (тоже в виде немутабельной ссылки) и у него вызывается метод render(), в который передаётся мутабельная ссылка на canvas и немутабельная ссылка на текущий GameObject. Все полученные ссылки нигде не сохраняются и живут только во время вызова метода, поэтому проблем с временами жизни нет.
Напоследок рассмотрим отдельностоящую функцию, которая инциализирует сцену, добавляя в неё несколько разноцветных квадратиков:
Как видим, здесь тоже используется время жизни. Функция принимает два параметра: мутабельную ссылку на Stage, чтобы через неё добавлять объекты на сцену, и ссылку на тип-трейт Renderer, чтобы использовать его для создания GameObject.
Ранее мы видели, что в описании Stage используется время жизни для GameObject, а в описании GameObject в свою очередь используется время жизни для ссылки на Renderer. Поэтому время жизни Stage через GameObject связано с временем жизни Renderer, которое должно быть не меньше чем y Stage. В этой функции мы должны пояснить, что параметры, которые ей передаются, соответствуют требованиям, а именно, мы передаём ссылку на Stage с временем 'st, и ссылку на Renderer также с временем 'st. (И ещё раз напомню, что название 'st может быть любым другим.)
Единственная проблема, которую я сейчас вижу, это неоднозначное указание времени ссылок. То есть:
- stage: & mut Stage<'st> – здесь время указано для структуры Stage, но не для ссылки
- renderer: &'st dyn Renderer – а здесь наоборот, время указано для ссылки
Понять этот момент я пока не в силах. Но на пути к просветлению.
Теперь можно проверить рассуждения на практике. Как происходит вызов функции init_stage()?
Здесь созданы сцена и рендерер, и затем ссылки на них переданы как параметры в init_stage(). Они имеют одинаковое время жизни, так как созданы в одном и том же программном блоке, так что всё хорошо.
Попробуем передать в функцию ссылки с разным временем жизни:
В этом случае компилятор видит, что рендерер живёт меньше сцены:
Но у нас было требование, чтобы рендерер жил не меньше сцены, значит, сцена может жить меньше рендерера и это должно компилироваться:
И да, это компилируется.
Что ж, у меня есть первый результат. На сцену добавляются квадратики разного размера и цвета:
А полный код можно посмотреть здесь:
Читайте дальше: