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

    И снова всем привет! Спешу поделиться, у меня были отличные выходные! Полтора дня я обдумывал вариант подачи материала, пилил макет и вообще всячески старался сделать хорошо. Что такое хорошо в контексте обучающего материала? На мой взгляд это «интересность», краткость, корректность и наглядность. Для меня лично написать такую статью — это подвиг. А вот серию статей — просто емкая и ответственная задача. Изучать 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. Что нужно сделать для запуска десктоп версии вы уже должны знать после прочтения статьи.
    • +15
    • 6,4k
    • 8
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 8
    • 0
      Автор, не подведите — будем ждать продолжения серии!:)
      Плюсик я уже поставил.
      • 0
        Спасибо за вводную.
        > мой первый проект «Пятнашки»
        Тут должна быть ссылка?
        • 0
          Ох, там было предложение про котика и в нем была ссылка. И похоже мы его потеряли :( Спасибо за подсказку, сейчас верну.
          • 0
            > Попробуйте собрать котика.
            Очень хотелось бы ещё ссылку на репозиторий. Если вам не сложно :)
            • 0
              Не сложно конечно, просто там треш (напомню это первая попытка). Времени причесать нет. Если не пугает, то сегодня в течение дня выложу в репозиторий, ссылку добавлю в конец статьи.
        • +1
          В закладки кинул, буду изучать=) Спасибо!
          • 0
            Спасибо за материал, понравилось! Как автор правильно заметил для HTML дистрибуции сейчас официально используется GWT (Google Web Toolkit). У GWT есть минус, который заключается в том, что писать можно только на Java. Но любителям Kotlin не стоит отчаиваться, ведь команда LibGDX пишет о разработке Kotlin дистрибуции HTML:
            (https://github.com/libgdx/libgdx/wiki/Using-libGDX-with-Kotlin: «This could be fixed in the future by using Kotlin’s JavaScript back-end»).
            Также для Kotlin на текущий день уже работает дистрибуция с помощью TeaVM (https://github.com/konsoletyper/teavm). TeaVM работает c JVM байткодом и ему сгодятся как Kotlin, так и Java или Scala и т.д… Но для того чтобы это дело заработало нужно локально сделать mvn clean install и полностью соберётся TeaVM у вас локально. А вот демо проект: https://github.com/konsoletyper/teavm-libgdx/tree/master/demos/invaders. В него уже можно добавлять Kotlin и будет работать.
            К сожалению с отладчиком кода в TeaVM у меня так и не получилось разобраться.
            В любом случае на текущий момент LibGDX имеет ограничения при дистрибуции в HTML: нельзя использовать рефлексию (например оператор instanceOf). Частично это решается с помощью кодогенерации на стадии перед компиляцией для чего всю рефексию нужно оборачивать через специальные классы (https://github.com/libgdx/libgdx/wiki/Reflection).
            Мораль такова — что на текущий момент LibGDX + Kotlin сложно настраиваются для HTML дистрибуции. Но нет ничего невозможного! Команда LibGDX также нацелена на полноценную поддержку Kotlin.
            • 0
              Сегодня смог осилить только половину статьи, хотя и потратил только на текст около 3 часов. Решил не выкладывать огрызок, завтра добью до логического завершения и выложу. Коммит делаю, если кому-то не терпится можете смотреть.

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