Kotlin и стоимость разработки игры (+ немного оффтопика)

Добрый день. Сегодня я хочу рассказать о разработке игры с использованием языка программирования Kotlin. Также приведу небольшой пример работы с RxJava в конце статьи.

Первый commit для этой игры случился 4 июня сего года, то есть до открытой беты я дошёл примерно за 3 с половиной 4 недели. Нельзя сказать что это первая игра или программа под Android которую я разрабатываю, но и опытным разработчикам под Андроид я также не являюсь. Мой предыдущий опыт в основном составляла Enterprise разработка.

Я хочу обозначить несколько тем в этой статье и пробежаться по ним коротенечко. Тему Kotlin’a постараюсь раскрыть подробно, по остальным возможны дополнительные статьи если будет такой запрос от вас (ну и плюс уточняющие вопросы в комментариях помогут улучшить подачу материала). Итак к списку: это сравнение стоимости разработка на Kotlin vs Java, где брать графику для вашей игры. Немного про деньги (пока про затраты, т.к. статистики по доходам пока нет). Также я считаю очень важно коснуться мотивировочной части. Начнем пожалуй с конца.

Мотивация
Почему я начинаю эту статью с такой казалось бы мелочи как мотивация? Ведь если я хочу создать приложение, значит я уже мотивирован. И всё хорошо, и меня ждет успех. Проблема в том что часто приступая к работе мы не осознаем свою истинную мотивацию: ждем ли мы денег, есть ли личная потребность, хотим ли мы создать пример для своего портфолио. Вариантов может быть много. Казалось бы, какая разница если я все равно мотивирован? Но взять к примеру портфолио — вроде бы хорошая инвестиция. Ты сможешь показывать своим потенциальным работодателям готовый продукт и они конечно же впечатлятся и тут же назначат огромную зарплату. Так вот, по мере разработки, по мере того как вы вкладываете свои силы и время, ценность портфолио начинает отходить на второй план (особенно если визуальной привлекательности добиться не удается). Уже хочется получить каких-то денег и если изначально планов монетизации приложения не было, то каждый следующий день требует все большей силы воли. Так в чём же проблема? Казалось бы — начинал работать с одной мотивацией, а в середине процесса разработки добавил монетизацию, выложил в Google Play и заработал 100500 руб. К сожалению так не работает. Пользователи не хотят тратить время на глючное, сырое или неинтересное приложение. То есть вы не можете выложить проект в середине разработки. Если вы так сделаете, скорее всего это просто приведет к падению рейтинга и отсутствию скачивания. То есть вся ваша работа по сути уйдёт в мусорную корзину.

Если вы изначально рассматриваете монетизацию как один из своих интересов, еще до самого старта работ вы должны понимать на чём вы будете зарабатывать деньги. Самое простое и самое неэффективное это рекламные баннеры. Чуть сложнее или примерно так же просто — показ рекламы на весь экран. Далее идет магазин. Но для того чтобы магазин заработал, вам необходимо зацепить пользователя чем-то. Чем раньше вы получите просмотр рекламы, тем больше это будет подпитывать вашу мотивацию продолжать работать дальше.

Графика
Вопрос графики для программиста является пожалуй что основной проблемой. Я имею в виду игры конечно. Когда я только начинал разработку, я считал что главное написать код, а уж найти графику не такая большая проблема. Может быть даже и заказать.

Существует значительное количество стоков ресурсов где графика продается, существуют ресурсы с бесплатными картинками. И в принципе да, действительно, так оно и есть. Есть только одна проблема — эти ресурсы выполнены в разных художественном стиле и комбинация из них даёт крайне странные результаты. Поэтому если вы разрабатываете свою первую игру, мой вам настоятельный совет — не ищите графику под свои задумки. Ищите готовые наборы графики под игру, и исходя из графики решайте что вы можете реализовать. Такие наборы есть, их не так чтобы много, но по крайней мере для первого второго раза вам хватит.

В моем же случае получилось что я потратил 2 недели на разработку, купил какие-то ресурсы в маркетах, но естественно их не хватило для того чтобы реализовать мою задумку. Скажу честно — купленная графика составляла менее чем 30% от необходимого. Но ведь нет нерешаемых вопросов, можно найти исполнителей которые нарисуют то, что вам нужно. И здесь мы с удивлением узнаём, что это не так просто. Особенно если нас интересует качественный результат. Что делал я — я просматривал стоки, находил интересные в плане графики экземпляры, переходил на описание автора и пробовал связаться.

Уже на этом этапе у вас должен быть перечень элементов которые вы хотите заказать и уже на этом этапе скорее всего ваши розовые слоники приопустят хоботы. Но пока это только предчувствие. Вы отправляете письмо с вопросом “можно ли обратиться для заказа графики” и в зависимости от профессионализма исполнителя (а те люди с которыми я обращался были профессионалами за исключением одного) вы получите сразу ответ что “Да можно, часовая ставка 25$/час”, ваш запрос будет выполнен за 30-40 часов не включая доработки по замечаниям (свободное время для заказа будет через полтора месяца). Или “Могу сделать за месяц-полтора, будет стоить 50 тысяч рублей”. Либо “Нет, у меня я слишком много работы и я дополнительные заказ не беру”. Это всё профессиональный варианты ответа. А самое неудачное что со мной случилось это следующее:

— А сколько вы готовы заплатить? (То есть человек не оценивает ресурсоемкость или сложность поставленной задачи, а пытается адаптироваться под платежеспособность заказчика)
— от 10000 до 20000 руб (Это был первый художник с которым я связался и в тот момент я считал что данная сумма Вполне себе подходит для небольшой, как я считал, работы)
Человек большого интереса не проявил, на вроде как согласился. Сам назначил срок когда даст описание что он за эту сумму может мне предоставить… и пропал. Это даже не звоночек это колокол что дальнейшая работа с подобным человеком для вас закончится неудачей. Тем не менее через 3 дня после озвученного им же срока, он связался со мной и сказал что сделает всё что мне надо за 15000 рублей. Я очень настороженно отношусь к обобщениям, поэтому попросил перечислить что же будет входить в это “всё” по пунктам. После чего человек пропадает с концами и я считаю что это счастливый конец истории.

Kotlin


Разработка личного проекта существенно отличается от рабочего. Различий несколько, но я остановлюсь на самом (для меня) существенном. Это отношение к стоимости разработки. В подавляющем большинстве случаев работодатель определяет требования, часто инструмент. Он же несет финансовую ответственность. Есть тестирование или нет, какие библиотеки и какой стек технологий. В личном проекте все права и все обязанности — на вас лично. Хотите использовать новый фреймворк — ради бога. Но время которое вы потратите на его освоение вы будете “оплачивать” из своего кармана. И да, не стоит думать что вы работаете бесплатно. Выкиньте из головы эту мысль, напишите на стикере вашу ставку в час и крайне желательно чтобы она соответствовала хотя бы средней стоимости специалиста вашей квалификации по городу. Не надо занижать себе цену.

Таким образом вопрос эффективности вашей работы встает на первый план. А сделал ли я что-то полезное за эти 8 часов, за что в ином случае я получил бы (к примеру) 4000 рублей? То есть вопрос Kotlin vs Java я предлагаю решать исключительно с финансовой точки зрения. Оба языка тьюринг-полные и значит любую программу написанную на Kotlin можно реализовать на Java и наоборот. За счет чего же мы можем получить разницу в стоимости разработки/стоимости владения продуктом?

Стоимость разработки — это количество денег для реализации функциональности. Стоимость владения = стоимость разработки + стоимость поддержки. В некоторых случая стоимость поддержки несоизмеримо выше стоимости разработки. Это особенно характерно для write-only языков (пример RegExp. Гораздо проще написать новое выражение чем понять где ошибка в существующем).

В идеале язык должен быть дешевым и в разработке, и в поддержке. Сокращение boilerplate code однозначно удешевляет разработку. Синтаксический сахар удешевляет разработку, но может (подчеркну может, но не обязан) приводить к увеличению стоимости владения. Синтаксическая соль удорожает разработку но удешевляет стоимость владения. Ниже я приведу примеры кода на Kotlin и Java и опишу какой вариант на мой взгляд дешевле и почему. Часть примеров будет из моего проекта, часть нет.

class Car(val id: String) {
    var speed: Double = 0.0
}

public class Car() {

    public final String id;
    public Double speed;

    public Car(String id) {
        this.id = id;
        this.speed = 0.0;
    }
}

* Для DTO классов нет необходимости в геттерах/сеттерах
** Геттеры/сеттеры имеют смысл только и только в том случае, если они меняют поведение при работе с полями

В данном сравнении мы видим что код на Kotlin более читаемый и самое главное он защищен от “ошибки на миллиард” — NPE. И id, и speed в Kotlin не могут быть null.

var car: Car = Car(null) // compile error
car.speed = null // compile error

Второй, не менее важный момент в приведенном выше примере — это мутабельность.

fun toRoman(value: Int): String {
   val singles = arrayOf("", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX")
   val tens = arrayOf("", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC")
   val hundreds = arrayOf("", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM")
   val thousands = arrayOf("", "M", "MM", "MMM")

   val roman = thousands[value / 1000] + hundreds[value % 1000 / 100] + tens[value % 100 / 10] + singles[value % 10]

   return roman
}

public String toRoman(int value) {
   final String[] singles = new String[] { "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX" };
   final String[] tens = new String[] { "", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC" };
   final String[] hundreds = new String[] { "", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM" };
   final String[] thousands = new String[] { "", "M", "MM", "MMM" };

   final String roman = thousands[value / 1000] + hundreds[value % 1000 / 100] + tens[value % 100 / 10] + singles[value % 10];
   return roman;
}

Не так много разницы, не правда ли? Тем не менее модификаторы final смотрятся крайне неуместно и визуально засоряют программу. Это приводит к тому, что final используется в Java гораздо реже чем следовало бы. А ведь иммьютбл гораздо дешевле и в разработке и во владении.

А вот пример трансформации данных:

val data:List<Int> = ArrayList()
val sum = data.filter { it > 0 }.sum()

List<Integer> data = new ArrayList<>();
final Integer[] sum = { 0 }; //Variable used in lambda expression should be final or effectively final
data.stream().filter(value -> value > 0).forEach(value -> sum[0] += value);

Тут два момента. Kotlin выполняется на jvm 6, Java требует jvm 8 для стримов. А для андроида, это на минуточку 24 API. На 5 июня это всего 9,5% устройств. И второй момент — final or effectively final переменные в лямбдах Java.


Инициализация объекта
private val buttonGroup = ButtonGroup<Button>().apply {
   setMinCheckCount(0)
}

private ButtonGroup<Button> buttonGroup = new ButtonGroup<>();
…
public constructor(...) {
    …
    buttonGroup.setMinCheckCount(0);
    …
}

Инициализация объекта в Kotlin’e возможна в одном месте что сокращает контекст для программиста и снижает стоимость вложения

В принципе, из того что я попробовал в Kotlin это самые серьезные вещи, влияющие на стоимость владения продуктом. Я сейчас не хочу говорить про вкусовщину типа data classes, перегрузку операторов, string template, lazy properties и т.п. Все это вещи интересные но они могут как сокращать, так и увеличивать стоимость владения.

В заключение небольшой пример Kotlin + RxJava + Jackson. Я хочу иметь dto класс, который позволит не просто хранить данные, но и уведомлять об их изменениях. Пример упрощенный для более наглядной демонстрации.

interface Property<T> {
   var value: T
   val rx: Observable<T>
}

open class BaseProperty<T>(defaultValue: T) : Property<T> {

   override var value: T = defaultValue
       set(value) {
           field = value
           _rx.onNext(field)
       }

   private val _rx = PublishSubject.create<T>()
   override val rx: Observable<T> = _rx
       get() {
           return field.startWith(value)
       }
}

Здесь хочу обратить внимание на перегрузку val rx. При подписке на Observable сразу же приходит текущее значение. Это важно т.к. десериализация из json'a случается раньше чем верстка экрана и подвязывание графических элементов к свойству. А при startWith мы сразу инициализируем графический элемент текущим значением и меняем по ходу пьесы.

class Car {
   private val _speed = BaseProperty(0.0)

   var speed: Double
       get() = _speed.value
       set(value) {
           _speed.value = value
       }
  
   @JsonIgnore
   val rxSpeed: Observable<Double> = _speed.rx
}

class Police {
   val cars: List<Car> = listOf(Car("1st"), Car("2nd"), Car("3rd"))

   init {
       cars.forEach {
           car -> car.rxSpeed
                   .map { speed -> speed > 60 } // преобразует double скорость в boolean скоростьПревышена
                   .distinctUntilChanged()
                   .filter { aboveLimit -> aboveLimit == true }
                   .subscribe { writeTicket(car) }
       }
   }

   private fun writeTicket(car: Car) {
       // do some stuff
   }
}

Класс Car прекрасно сериализуется/десериализуется Jackson'ом, может быть использован как классический dto класс, но в то же время позволяет обрабатывать изменения свойств в реактив стиле.

Ниже пример подвязки Label к свойству объекта:

Label("", assets.skin, "progress-bar-time-indicator").apply {
                        setAlignment(Align.center)
                        craft.rx(DURATION).subscribe {
                            setText(TimeFormat.format(it))
                        }
                    })

Заключение:

К сожалению я не могу представить объективных цифр насколько стоимость владения продуктом на Kotlin'e дешевле Java. Да и само это утверждения я уверен будет не раз оспорено в комментариях. Могу сказать только по своему субъективному суждению, цифра в 1.5 — 2 раза для меня реальна. Причем сокращение стоимости владения в полтора раза характерно для начала перехода с Java на Kotlin, примерно через неделю-две я думаю на двойную эффективность вышел. В основном за счет NPE-proof, immutable, lambda & функции высшего порядка.

Upd:
Статья по графике
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 21
  • +1
    Похоже я промахнулся с формой подачи материала :) Завтра постараюсь сделать статью полностью по графике. С перечнем ресурсов откуда что можно позаимствовать, ссылки на интересных художников/дизайнеров. Кстати подавляющее большинство интересных и плодотворных дизайнеров — наши с вами соотечественники. Я наткнулся только на одного интересного француза, но к этому времени его уже нанял близзард для дизайна карт в Heartstone.
    • +2
      BehaviorSubject даст то же поведение за бесплатно, только перегрузкой сеттера
      • +1
        Спасибо огромное за подсказку, не знал. Это вообще отличная иллюстрация к тому, что мы только думаем что программируем на языке/фреймворке, а на деле используем только часть (порой малую) а на остальное городим велосипеды. Честно обещаю исправиться по крайней мере с BehaviorSubject :)
      • 0
        Спасибо за статью, а не думали использовать unity для разработки игры? Я думаю, что так можно еще раза в 2 снизить цену разработки
        • +1
          Рассматривал этот вариант, но у Unity есть несколько недостатков которые делают эту платформу мне не интересной.
          • Узкая специализация. Навыки, полученные при разработке в Unity можно продать только фирмам, занимающимся разработкой игр на Unity. В случае Java/Kotlin + LibGDX я успешно использовал полученные знания для собеседований. В данный момент у меня оффер на релокацию в Прагу и в Лимассол (Кипр).
          • Unity дает очень большой оверхед в плане потребления ресурсов. К примеру запуск подавляющего большинства приложений на юнити это 5-10 секунд. Мое приложение запускается вместе с прогрузкой ресурсов около 1.5-2 секунд. Ну и каждая вторая игра на Unity превращает телефон в обогреватель. Может из-за плохой оптимизации. Не знаю.
        • +1
          Очень странная статья и вопрос стоимости разработки почти не раскрыли, и на Kotlin написали «Hello world», еще и не в лучшем виде.
          • 0
            Да, я понимаю что статья странная. На то она и первая чтобы делать ошибки и учиться. Могли бы вы сформулировать вопрос чтобы я его раскрыл подробно?
          • +1
            «Kotlin выполняется на jvm 6» — уже можно и на 8, а за статью спасибо, как раз начинаю свой проект, некоторые мысли из «мотивации» были кстати.
            • +1
              Обычно требование JVM 6 возникает не от хорошей жизни. Это могут быть старые веб сервера или поддержка старых версий Android. К примеру Java 8 появилась только в IBM WebSphere 8.5.5.9. А многим беднягам приходится до сих пор работать с WebSphere 7, это та самая JVM 6 и есть. И получить вкусняшки Kotlin'a на мой взгляд огромный плюс.

              В зависимости от Target JVM (а котлин умеет и в 6 и как вы заметили в 8) меняется байткод. Что как я понимаю сказывается в первую очередь на производительности.
              • 0
                Мой комментарий — это просто небольшое уточнение, чтобы не было лишних нападок в сторону языка. Уже не раз видел как Kotlin в минус ставят jvm 6)
            • 0
              Что-то последнее время стали много везде говорить про Kotlin, даже подозрительно как-то…
              • +1
                Действительно, с чего бы это. Вроде ничего не произошло, кроме признания языка гуглом и оффициальной поддержке как first-class citizen-а, начиная с Android Studio 3.0.
              • 0
                .filter { aboveLimit -> aboveLimit == true }
                

                это какой-то ад, разве нельзя написать

                .filter{it}
                

                ?
                • 0

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

                  • 0
                    я больше не буду комментировать с телефона :)
                    cars.forEach { 
                            val car = it
                            car.rxSpeed
                                       .map { it > 60 } // преобразует double скорость в boolean скоростьПревышена
                                       .distinctUntilChanged()
                                       .filter { it }
                                       .subscribe { writeTicket(car) }
                           }
                    
                • 0
                  Тем не менее модификаторы final смотрятся крайне неуместно и визуально засоряют программу. Это приводит к тому, что final используется в Java гораздо реже чем следовало бы.

                  Не большой специалист по Java, но кажется где-то встречал понятие effectively final. Разве современный компилятор не в состоянии сам определить, что переменная не меняется и является по сути константой со ваеми вытекающими оптимизациями?
                  • +1
                    https://stackoverflow.com/questions/20938095/difference-between-final-and-effectively-final
                    В приведенном случае переменная никак не может быть effective final, т.к. она постоянно пересчитывается. В начале операции sum = 0, но для каждого элемента больше нуля происходит инкремент на величину элемента.

                    Плюс еще один момент: если вы пишете final String — вы можете определить значение или немедленно, или в конструкторе. Любая попытка переопределить будет кидать compile error. В случае effective final ну станет переменная не-финальной. Никаких предупреждений и тем более ошибок не возникнет. А если это проект да с индусами в команде, вы никогда не можете знать откуда ваше поле изменят. Да и наши порой жгут. Привет, Юра!
                    • 0
                      Пример с sum, честно говоря, пропустил (чукча не читатель). Сейчас посмотрел, но ведь он не корректен. Корректный эквивалент на java как-то так:
                      int sum = data.stream().filter(value -> value > 0).mapToInt(Integer::intValue).sum();
                      

                      И тут sum ничто не мешает быть final.
                      • 0
                        Да, вы правы. Для меня было не очевидно что Integer нужно мапить в intValue. Замечание принято. Правда не могу устоять против приведения аналога на котлине :) Но это уже конкретная вкусовщина и на стоимость разработки/поддержки почти не влияет.
                                val data: List<Int> = ArrayList()
                                var sum = 0
                                data.filter { it > 0 }.forEach { sum += it }
                        
                        • 0
                          вот так суммировать будет лаконичнее и эффективнее:

                          val data = ArrayList<Int>()
                          val sum = data.filter { it > 0 }.sum()
                          

                          Изменяемая переменная захваченная в функцию в котлине оборачивается во враппер – объект, например, IntRef. И так пока что даже для заинлайненных анонимок (это вроде оптимизируют со временем).
                  • 0
                    del

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