Pull to refresh

LibGDX + Scene2d (программируем на Kotlin). Часть 0

Reading time9 min
Views18K
И снова всем привет! Спешу поделиться, у меня были отличные выходные! Полтора дня я обдумывал вариант подачи материала, пилил макет и вообще всячески старался сделать хорошо. Что такое хорошо в контексте обучающего материала? На мой взгляд это «интересность», краткость, корректность и наглядность. Для меня лично написать такую статью — это подвиг. А вот серию статей — просто емкая и ответственная задача. Изучать Scene2d мы будем в процессе написания игры с нуля! Процесс нашего творчества растянется на долгие десять-двенадцать дней. Мне хочется верить что периодичность материалов будет примерно раз в день. Для меня лично это очень амбициозная задача, ведь требуется не столько запрограммировать, но и описать в статьях с детальным разбором. Я не сторонник бросаться в бушующий океан, в надежде научиться плавать. Мы прыгнем у лужу и будем последовательно ее углублять и расширять. Итак начинаем.

Разработку любой программы я настоятельно советую начинать с составления карточки продукта. Из обязательного — цели. Я составляю карточку продукта в Google Docs и вот как карточка выглядит в нашем случае.

Средневековый магнат


Цели проекта


  1. Демонстрация процесса разработки игры для сайта habrahabr.ru (общественная, информационная, краткосрочная)
  2. Создание материалов, которые впоследствие могут быть использованы как обучающие (личная, репутация, долгосрочная; общественная, информационная, долгосрочная)
  3. Создание основы для коммерческой игры (личная, краткосрочная)
  4. Привлечение скачиваний из Google Play (личная, финансовая, долгосрочная)

Первым делом стараемся максимально честно разобраться со своей мотивацией. Зачем мы вообще влезаем в это дело. Что будет служить нам путеводной звездой когда все хорошо и пинком под зад когда все плохо. Пожалуйста постарайтесь избегать альтруизма и “мира во всем мире”. Человеческая психика устроена таким образом, что личные побудительные мотивы значат больше чем общественные (визионерскую психологию тут не рассматриваю).

Игровой мир


Средние века / фэнтези
Цель игры — много денег
Сбор ресурсов
Продажа

Описание процесса игры


Так как первая и основная цель данной разработки является демонстрация работы, и игровой мир, и описание игрового процесса будут упрощены в угоду краткости и ясности подачи материала.

Игрок собирает ресурсы и продает их на местном рынке. На поиск и сбор ресурсов тратится еда/энергия. Когда энергия кончилась, ее необходимо покупать в городе.

Прототип интерфейса


пример


Несмотря на то, что в предыдущей статье я рекомендовал тетрадку и карандаш в качестве инструментов для прототипирования, этот макет сделан в Adobe Experience Design CC (Beta). На момент публикации статьи, его можно скачать бесплатно. На работу с ним я угрохал полтора дня но считаю это оправданным. Дело в том, что публикация на Хабре является групповой работой, даже если я все делаю один. Чем более качественные опорные материалы я предоставлю, тем легче будет воспринимать информацию. Вот проектный файл Adobe Experience Design. Его можно скачать, запустить в режиме презентации и даже немного потыкать по кнопкам. Технически можно запилить отдельную статейку, но не знаю нужно ли это. Комментарии рассудят.

Ну и какая разработка без публичного репозитория? Вот ссылка.

Для работы нам понадобится Android Studio 3.0 (на данный момент доступна версия Canary 5), Android SKD и LibGDX. Установку всех этих тряхомудрий я пропущу, тут все большие мальчики и девочки. На крайний случай есть комментарии.

Запуск мастера конфигурирования LibGDX происходит из командной строки:

java -jar gdx-setup.jar

Параметры проекта


Кто был не в курсе, LibGDX это кроссплатформенный фреймворк, позволяющий писать одновременно под PC, Android, iOS и даже HTML (для последнего используется GWT, а у нас Kotlin, так что HTML нам точно не грозит). Из расширений я выбрал два:

Freetype — позволяет генерировать растровые шрифты из ttf/otf
Tools — среди прочего позволяет генерировать атласы текстур


Коммит с получившимся проектом доступен в репозитории. Я старался крошить и именовать коммиты таким образом, чтобы было просто понять какой фрагмент за что отвечает. Так как LibGDX кроссплатформенный, я предпочитаю большую часть разработки проводить на PC и тестировать/исправлять ошибки под Android непосредственно перед релизом. Как правило на эту работу уходит не больше 2-3 часов времени.

Дальше в этой статье


  • Запуск проекта через DesktopLauncher
  • Перевод проекта на Kotlin
  • Ошибка Kotlin not configured
  • Конфигурация gradle для kotlin-desktop версии
  • Конфигурация портретной ориентации для desktop версии
  • Первое использование Scene2D. Загрузочный экран, загрузочная сцена

Запуск проекта через DesktopLauncher


Конфигурация


Обратите внимание, что рабочая папка для DesktopLauncher расположена в android/assets. Запуск DesktopLauncher на коммите:

initial commit after libgdx wizard


Обратите внимание, что даже только что сконфигурированный проект не запускается под android. Мы поправим это на следующем шаге.

Перевод проекта на Kotlin


LibGDX проекты сконфигурированы как мультимодульный gradle. Есть проектный build.gradle и модульные build.gradle для core, android и desktop. Почти весь код мы будем писать в core. В проекте android позже у нас будет сидеть AdMob + конфигурация immersive mode + покупки в Google Play маркете.

Для перевода проекта из java в kotlin мы меняем все apply plugin: «java» на apply plugin: «kotlin». В android/build.gradle добавляем apply plugin: 'kotlin-android'. Самые большие изменения произошли в проектном build.gradle

build.gradle
         mavenCentral()
         maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
         jcenter()
+
+        maven { url 'https://maven.google.com' }
     }
+
+    ext.kotlin_version = '1.1.3'
+
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.2.0'
-        
-
+        // uncomment for desktop version
+        // classpath 'com.android.tools.build:gradle:2.3.2'
+        classpath 'com.android.tools.build:gradle:3.0.0-alpha5'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     }
 }
 
@@ -37,7 +43,7 @@
 }
 
 project(":desktop") {
-    apply plugin: "java"
+    apply plugin: "kotlin"
 
 
     dependencies {
@@ -74,13 +80,13 @@
 }
 
 project(":core") {
-    apply plugin: "java"
+    apply plugin: "kotlin"
 
 
     dependencies {
         compile "com.badlogicgames.gdx:gdx:$gdxVersion"
         compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
-        
+        compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
     }
 }


Добавился гугловый репозиторий, в buildscript.dependencies добавлен kotlin-gradle-plugin и в core проект добавлена compile-зависимость kotlin-stdlib (в нашем случае kotlin-stdlib-jre8).

Данная версия работает на android, но не работает в desktop варианте из-за ошибки Android Studio 3.0 Canary 5. Почему я считаю что это причина — запуск gradle цели desktop-run таки запускает приложение (правда требует запущенное Android device/emulator для запуска android:run). А вот запуск из Android Studio выкидывает Exception in thread «main» java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics. Если кто сможет победить запуск DesktopLauncher'a со свежей версией gradle — дайте знать пожалуйста!

Перевод java файлов в kt элементарна — выделяете файл/папку и жмете Ctrl+Alt+Shitf+K. Единственная ошибка которая возникнет у вас после данной операции заключается в требовании Kotlin'a инициализировать свойство в момент определения:

java
public class MedievalTycoonGame extends ApplicationAdapter {
	SpriteBatch batch;
	Texture img;
	
	@Override
	public void create () {
		batch = new SpriteBatch();
		img = new Texture("badlogic.jpg");
	}

	@Override
	public void render () {
		Gdx.gl.glClearColor(1, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
		batch.begin();
		batch.draw(img, 0, 0);
		batch.end();
	}
	
	@Override
	public void dispose () {
		batch.dispose();
		img.dispose();
	}
}


kotlin
class MedievalTycoonGame : ApplicationAdapter() {
    internal var batch: SpriteBatch // ошибка тут <- следует заменить на private lateinit var batch: SpriteBatch
    internal var img: Texture // ошибка тут <- следует заменить на private lateinit var img: Texture
    override fun create() {
        batch = SpriteBatch()
        img = Texture("badlogic.jpg")
    }

    override fun render() {
        Gdx.gl.glClearColor(1f, 0f, 0f, 1f)
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
        batch.begin()
        batch.draw(img, 0f, 0f)
        batch.end()
    }

    override fun dispose() {
        batch.dispose()
        img.dispose()
    }
}


internal = package видимость в java. Нам пакетная видимость не нужна (и вообще через пару коммитов мы удалим эти поля). Не во всех случаях мы можем проинициализировать поле сразу, а делать его nullable это вообще глупость (нам котлин интересен как раз из-за null-safety). Для этого в kotlin есть модификатор lateinit, который говорит компилятору, что программист зуб дает, на момент использования этого поля оно не будет равно null. Это так называемая синтаксическая соль. Вообще, если смотреть на этот код не как результат автоматической конверсии, то уместнее бы смотрелось:

private val batch = SpriteBatch()
private val img = Texture("badlogic.jpg")

Ошибка Kotlin not configured


Эту ошибку вы будете видеть каждый раз при запуске Android Studio. Просто щелкните синхронизировать gradle:



Конфигурация gradle для kotlin-desktop версии


Как я уже говорил, я предпочитаю разрабатывать desktop версию приложения, и изменением пары строчек мы реанимируем этот режим. Все что нужно — указать в проектном build.gradle
classpath 'com.android.tools.build:gradle:2.3.2', а в gradle-wrapper.properties версию gradle-3.3-all.zip

Конфигурация портретной ориентации для desktop версии


В DesktopLauncher добавляем горсть параметров конфигурации. Три относятся к размеру окна и возможности изменения размеров. Четвертый параметр vSync отключен т.к. есть глюк, на некоторых видеокартах в desktop и только на config.foregroundFPS=60 (по умолчанию), грузит одно ядро процессора в 100%.

        config.width = 576
        config.height = 1024
        config.resizable = false
        config.vSyncEnabled = false

Первое использование Scene2D. Загрузочный экран, загрузочная сцена


Вот мы и добрались до первого использования Scene2D. Коротко пару слов для чего он предназначен и что можно от него хотеть.

Scene2D является графом (деревом) элементов и предназначен в первую очередь для создания UI. Прямо «из коробки», вы получаете возможность верстки, трансформации элементов (поворот, масштаб, сдвиг и т.д.). Огромным плюсом идет обработка касаний. Ну и вишенка на торте система действий. С непонятным определением закончили, теперь то же самое человеческим языком.

Есть сцена, она занимает весь экран. На сцене вы можете разместить таблицу, в таблице картинку, панель прокрутки, десяток кнопок и даже черта лысого (главное чтобы в душе он был Actor'ом). При помощи волшебных слов top/center/left/width и т.д. вы реализуете верстку. Пример сложнее чем hello world будет только завтра, и так статья большая получилась. Дальше на любой произвольный элемент вы вешаете обработчик касания и он работает. Вам не нужно вручную ловить координаты клика, проверять что же находится там, какой у объектов z-index и т.п. Но еще раз, про это завтра. А сегодня просто несколько фрагментов кода напоследок:

class MedievalTycoonGame : Game() {

    val viewport: FitViewport = FitViewport(AppConstants.APP_WIDTH, AppConstants.APP_HEIGHT)

    override fun create() {
        screen = LoadingScreen(viewport)
    }
}

Наш класс MedievalTycoonGame теперь наследуется от Game, вся задача которого свалить работу на Screen. Первый экран, который мы сейчас покажем пользователю будет называться LoadingScreen и будет содержать одну сцену — LoadingStage. Т.к. не предполагается разрастания этих классов, я размещу их в одном файле LoadingScreen.kt

LoadingScreen.kt
class LoadingScreen(val viewport: Viewport) : ScreenAdapter() {

    private val loadingStage = LoadingStage(viewport)

    override fun render(delta: Float) {
        Gdx.gl.glClearColor(0f, 0f, 0f, 0f)
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)

        loadingStage.act()
        loadingStage.draw()
    }

    override fun resize(width: Int, height: Int) {
        viewport.update(width, height)
    }
}

class LoadingStage(viewport: Viewport) : Stage(viewport) {

    init {
        val backgroundImage = Image(Texture("backgrounds/loading-logo.png"))
        addActor(backgroundImage.apply {
            setFillParent(true)
            setScaling(Scaling.fill)
        })
    }
}


Все что делает LoadingScreen — затирает экран черным цветом и вызывает методы act() и draw() у LoadingStage. На act() очень удобно вешать логику программы, работу с данными. Draw() это просто отрисовка всех элементов сцены.

Единственный момент, хочу заакцентировать как выглядит инициализация сцены java vs kotlin

    init {
        val backgroundImage = Image(Texture("backgrounds/loading-logo.png"))
        addActor(backgroundImage.apply {
            setFillParent(true)
            setScaling(Scaling.fill)
        })
    }

    public LoadingStage() {
        Image backgroundImage = new Image(new Texture("backgrounds/loading-logo.png"));
        backgroundImage.setFillParent(true);
        backgroundImage.setScaling(Scaling.fill);
        addActor(backgroundImage);
    }

В случае с kotlin у нас всегда инициализация элемента и его размещение на соседних строчках. Это достигается за счет функции расширения apply. Иерархия в kotlin автоматически создает отступы и визуально очень легко читается. В java вся верстка идет без отступов. Инициализация элемента и его размещение часто невозможно рядом. Если иерархия состоит из 3+ уровней глубины, упорядочить элементы красиво (и дешево в поддержке) в java невозможно.

На сегодня это все. Рассматривайте эту статью как вводную, собственно раскрытие Scene2D и реализация игры будет завтра и далее. Спасибо что были с нами ;) И не рекламы ради (в этом приложении надо приложить усилия чтобы увидеть рекламу), мой первый проект «Пятнашки» на Scene2D когда я еще только-только осваивал. Из достоинств — удобство управления. Существуют сотни если не тысячи версий приложения и в 90% что я видел передвижение фишек возможно только тычком в соседнюю с пустой клеткой. Попробуйте собрать котика.

скрин



В следующих статьях


  • Базовые элементы Scene2D
  • Два базовых контейнера для верстки
  • Атлас текстур
  • Шкурки
  • Интернационализация

P.S. В загрузочном экране использована работа художника Виталия Самарина aka Vitaly S Alexius.

Upd:
ссылка на исходники Пятнашек
ужасы нашего городка

Сразу предупреждаю, код страшный. Так мы с вами писать не будем. Картинку разбирает на лету, т.е. можно поправить код чтобы изображение засасывало по URL. Проект закоммичен для запуска под Android. Что нужно сделать для запуска десктоп версии вы уже должны знать после прочтения статьи.
Tags:
Hubs:
+15
Comments9

Articles