Добрый день. Я тут в качестве хобби разрабатываю 2д движок для создания игр. Сегодня хочу с вами поделиться как на нем сделать классическую игру "змейка". Прошу внимательно прочитать, покритиковать, похвалить, посоветовать, и прочее.
Пишу я его на с++ с использованием графической библиотеке SDL, в качестве скриптового языка использую Питон.
Прошу прощения за скриншоты, но не понял как вставлять "правильный код", а вставлять цитатами было очень страшно и не красиво.
Для лиги лени сразу прикрепляю ссылку с готовым архивом:
Заранее спасибо. И так начнем.
Создаем пустой проект
Структура каталогов пустого проекта:
Редактируем config.xml
config.xml - это файл который читается в движке первым.
Основная строка которая нас интересует
<AppMain value = "..\Assets\ScriptsP\AppMain.py" />
Application.Script.AppMain - это путь к скрипту с которого стартует приложение
Все конфиг нам больше не нужен.
Создаем Тайлы
В каталог /Assets/Tiles/ - добавляем следующие картинки (все имеют формат 120 на 120)
2. snakeBody.png - часть тела змеи (кружочек зеленый)
3. snakeGrid.png - квадратик с полупрозрачными черными линиями(из него мы будем делать сетку)
4. snakeHeadDown.png - голова которая смотри вниз
5. snakeHeadLeft.png - голова которая смотри влево
6. snakeHeadRight.png - голова которая смотри вправо
7. snakeHeadUp.png - голова которая смотри вверх
Создаем UI
Элементы ГУИ можно создавать как програмно так и задавать расположение в xml файлах.
Получается что то типа "Окна" - к которому прикреплены все элементы как дочерние.
К основному объекту типа xdCanvas прикрепляем
Объект типа xdText с именем tFPS который будет счетчиком фпс
Объект типа xdText с именем tObjects который будет счетчиком количества объектов на экране
Объект типа xdText с именем tScore который будет отображать текущий счет.
Ниже полный текст файла UISnake.xml
<?xml version="1.0"?>
<object type="xdCanvas">
<position x="0" y="0" />
<scale value="1" />
<order value="999998" />
<name value="UISnake" />
<relative value="XD_UI" />
<alignment value="XD_UI_ALI_NONE" />
<anchorVertical value="XD_UI_ANC_V_TOP" />
<anchorHorizontal value="XD_UI_ANC_H_LEFT" />
<childs>
<object type="xdText">
<position x="-100" y="-20" />
<text value="FPS:" />
<font name="..\Assets\Fonts\Comfortaa.ttf" size = "10" style = "1" colorR = "93" colorG = "86" colorB = "79" />
<name value="tFPS" />
<anchorVertical value="XD_UI_ANC_V_DOWN" />
<anchorHorizontal value="XD_UI_ANC_H_RIGHT" />
</object>
<object type="xdText">
<position x="-100" y="-10" />
<text value="Objects:" />
<font name="..\Assets\Fonts\Comfortaa.ttf" size = "10" style = "1" colorR = "93" colorG = "86" colorB = "79" />
<name value="tObjects" />
<anchorVertical value="XD_UI_ANC_V_DOWN" />
<anchorHorizontal value="XD_UI_ANC_H_RIGHT" />
</object>
<object type="xdText">
<position x="20" y="20" />
<text value="Счет:" />
<font name="..\Assets\Fonts\Comfortaa.ttf" size = "30" style = "1" colorR = "100" colorG = "50" colorB = "50" />
<name value="tScore" />
<anchorVertical value="XD_UI_ANC_V_TOP" />
<anchorHorizontal value="XD_UI_ANC_H_LEFT" />
</object>
</childs>
</object>
Создаем файл UISnakeMenu.xml
Который будет отвечать за меню начала игры и отображения результатов
Иерархия нашей меню будет следующая
UISnakeMenu(xdCanvas) - основной канвас окна
uiPanelCenterMenu(xdPanel) - панелька на которой все нарисуем
tCaption(xdText) - Заголовок "Пиратская Змейка"
tGameOver(xdText) - Надпись "Начните новую игру", или "Game over" (значение будем задавать скриптом)
tScoreEnd(xdText) - Счет, если был проигрыш (значение будем задавать скриптом)
btnNewGame(xdButton) - Кнопка новая игра (при нажатии которой игра начинается сначала)
btnExit(xdButton) - Кнопка выход из игры
Из непонятного у нас только xdPanel и xdButton
это по сути те-же элементы UI, что и xdText - описанные выше, только с небольшими отличиями
xdPanel - элемент интерфейса отражает физическую панельку, на которой мы можем что то рисовать.
xdButton - это кнопка с текстом(есть разные варианты можно картинками задать статусы, но у нас кнопка простая по умолчанию)
Принципиально не отличается от текста вся логика будет задаваться на стороне скрипта.
Ниже полный текст файла UISnakeMenu.xml
<?xml version="1.0"?>
<object type="xdCanvas">
<position x="0" y="0" />
<scale value="1" />
<order value="999998" />
<name value="UISnakeMenu" />
<relative value="XD_UI" />
<alignment value="XD_UI_ALI_NONE" />
<anchorVertical value="XD_UI_ANC_V_TOP" />
<anchorHorizontal value="XD_UI_ANC_H_LEFT" />
<childs>
<object type="xdPanel">
<position x="0" y="0" />
<sizePrecent x="20" y="50" />
<name value="uiPanelCenterMenu" />
<anchorVertical value="XD_UI_ANC_V_CENTER" />
<anchorHorizontal value="XD_UI_ANC_H_CENTER" />
<childs>
<object type="xdText">
<position x="0" y="+30" />
<text value="Пиратская Змейка" />
<font name="..\\Assets\\Fonts\\Comfortaa.ttf" size = "20" style = "1" colorR = "93" colorG = "86" colorB = "79" />
<name value="tCaption" />
<anchorVertical value="XD_UI_ANC_V_TOP" />
<anchorHorizontal value="XD_UI_ANC_H_CENTER" />
</object>
<object type="xdText">
<position x="0" y="80" />
<text value="Начните новую игру" />
<font name="..\\Assets\\Fonts\\Comfortaa.ttf" size = "18" style = "1" colorR = "190" colorG = "86" colorB = "79" />
<name value="tGameOver" />
<anchorVertical value="XD_UI_ANC_V_TOP" />
<anchorHorizontal value="XD_UI_ANC_H_CENTER" />
</object>
<object type="xdText">
<position x="0" y="100" />
<text value="Счет:" />
<font name="..\\Assets\\Fonts\\Comfortaa.ttf" size = "18" style = "1" colorR = "93" colorG = "86" colorB = "79" />
<name value="tScoreEnd" />
<anchorVertical value="XD_UI_ANC_V_TOP" />
<anchorHorizontal value="XD_UI_ANC_H_CENTER" />
</object>
<object type="xdButton">
<position x="0" y="0" />
<size x="130" y="50" />
<name value="btnNewGame" />
<text value="Новая Игра" />
<anchorVertical value="XD_UI_ANC_V_CENTER" />
<anchorHorizontal value="XD_UI_ANC_H_CENTER" />
</object>
<object type="xdButton">
<position x="0" y="70" />
<size x="130" y="50" />
<name value="btnExit" />
<text value="Выход" />
<anchorVertical value="XD_UI_ANC_V_CENTER" />
<anchorHorizontal value="XD_UI_ANC_H_CENTER" />
</object>
</childs>
</object>
</childs>
</object>
```
Теперь переходим к самому интересному к логике.
Файл globals.py - вспомогательный скрипт который в данной игре не имеет смысла большого, можно обойтись без него.
но по замыслу тут находятся какие-то глобальные константы и методы
например есть метод Debug который позволяет отлаживать скрипты в режиме реального времени в Visual Studio Code ооооочень удобно, но об этом в другой раз.
Импортируем пространство имен xd
import xd # Импорт в питон методов движка. почти все находится в пространстве имен xd
добавляем самый первый метод OnStart
После чего делаем обертку в виде класса Application это не обязательно , но автору это кажется красивым
Application.OnStart - будет вызываться при старте сцены.
application - экземпляр класса приложение.
в глобальный метод **OnStart** добавляем вызов:
Минимальный скрипт готов.
Далее создаем пустую сцену, а именно файл SceneGame.py
Сцена это объект порождённый от объекта xd.GameScene со следующими методами:
OnLoad - вызывается после загрузки сцены
OnStart - вызывается при старте сцены
OnFrame - вызывается каждый кадр.
Unload - метод где будем удалять все, что создали.
Текст файла SceneGame.py с минимальной сценой.
Далее в файле AppMain.py добавляем следующие строки
xd.GameScene.LoadScene("SceneGame.SceneGame", self.OnLoadScene)
Вызываем метод движка xd.GameScene.LoadScene для загрузки сцены.
1. "SceneGame.SceneGame" - строка с указанием класса сцены в питон скрипте. В формате [Имя файла скрипта].[Имя класса в файле]
2. self.OnLoadScene - обработчик вызываемый после загрузки сцены.
Реализация метода SceneGame.OnLoadScene
На данный момент полный текст файла AppMain.py:
```python
import xd # type: ignore
import globals
globals.Debug(False)
class Application:
""" Глобальный класс сцена"""
def OnStart(self):
xd.GameScene.LoadScene("SceneGame.SceneGame", self.OnLoadScene)
def OnLoadScene(self, scene):
self.scene = scene
xd.App.SetGame(scene)
pass
#Экземпляр класса сцены
application = Application()
def OnStart():
"""Обработчик при старте сцены (вызывается движком)"""
application.OnStart()
return 1
После запуска должно быть пустое окно с синеватым фоном. Если это так, от значит мы все делаем правильно.
Файл AppMain.py - мы больше не меняем. Он принял конечный вид.
В методе SceneGame.OnStart добавляем следующий код
На любой сцене есть корневой объект с именем rootObject куда можно прикреплять объекты. Не прикрепленные объекты не отображаются на сцене (игнорируются циклом рендера)
Метод xd.BaseObject.FindByNameS это метод FindByNameS - ищет игровой объект на сцене с по имени. и возвращает ссылку на объект.
Метод xd.Panel.to - преобразовывает ссылку в Объект типа xdPanel есть аналогичные методы для любых предопределенных объектов xd.Text.to , xd.Button.to и др.
После запуска мы увидим Окно игровое с отображаемой меню.
Далее добавляем на сцену окно из файла UISnake.xml для отображения текущих игровых данных а запоминаем ссылки на элементы интерфейса см которыми после будем работать
Первый обработчик событий. Начнем с простого, сделаем обработчик, который выходит из игры.
У нас есть кнопка для выходи из игры с именем btnExit
Первая сцена и обработчик готовы
При текущем варианте запуска приложения должно быть окно с главным меню и рабочей кнопкой выхода из игры.
Далее добавим в проект вспомогательный класс GameData
Основная особенность которого хранить данные между сценами, аля глобальный объект. В текущей реализации это не обязательно но предлагается его использовать, потому, что при развитии может быть у нас появятся новые сцены, и как между ними передавать данные?
С высоты птичьего полета класс GameData выглядит так:
GameData.Unload - метод где описываются удаления элементов аля деструктор
GameData.OnStart - обработчик который вызываем при старте приложения
GameData.OnTick - обработчик таймера (обсудим позже)
GameData.OnKeyPressed - обработчик нажатия клавиши
GameData.GameOver - Метод проигрыша
GameData.NewGame - метод новая игра
GameData.IsPause - проверить на паузе ли игра
GameData.NewApple - добавить новое яблоко на сцену
GameData.DrawGrid - нарисовать сетку
Реализация этих методов будет позже и постепенно. Сейчас сосредоточимся на следующем.
Часть первая закончена (лимит картинок в посте закончился)
Спасибо, кто дочитал, и прокомментировал.