Первый commit для этой игры случился 4 июня сего года, то есть до открытой беты я дошёл примерно за 3 с половиной 4 недели. Нельзя сказать что это первая игра или программа под Android которую я разрабатываю, но и опытным разработчикам под Андроид я также не являюсь. Мой предыдущий опыт в основном составляла Enterprise разработка.
Я хочу обозначить несколько тем в этой статье и пробежаться по ним коротенечко. Тему Kotlin’a постараюсь раскрыть подробно, по остальным возможны дополнительные статьи если будет такой запрос от вас (ну и плюс уточняющие вопросы в комментариях помогут улучшить подачу материала). Итак к списку: это сравнение стоимости разработка на Kotlin vs Java, где брать графику для вашей игры. Немного про деньги (пока про затраты, т.к. статистики по доходам пока нет). Также я считаю очень важно коснуться мотивировочной части. Начнем пожалуй с конца.
Если вы изначально рассматриваете монетизацию как один из своих интересов, еще до самого старта работ вы должны понимать на чём вы будете зарабатывать деньги. Самое простое и самое неэффективное это рекламные баннеры. Чуть сложнее или примерно так же просто — показ рекламы на весь экран. Далее идет магазин. Но для того чтобы магазин заработал, вам необходимо зацепить пользователя чем-то. Чем раньше вы получите просмотр рекламы, тем больше это будет подпитывать вашу мотивацию продолжать работать дальше.
Существует значительное количество стоков ресурсов где графика продается, существуют ресурсы с бесплатными картинками. И в принципе да, действительно, так оно и есть. Есть только одна проблема — эти ресурсы выполнены в разных художественном стиле и комбинация из них даёт крайне странные результаты. Поэтому если вы разрабатываете свою первую игру, мой вам настоятельный совет — не ищите графику под свои задумки. Ищите готовые наборы графики под игру, и исходя из графики решайте что вы можете реализовать. Такие наборы есть, их не так чтобы много, но по крайней мере для первого второго раза вам хватит.
В моем же случае получилось что я потратил 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)
ijsgaus
29.06.2017 21:45+2BehaviorSubject даст то же поведение за бесплатно, только перегрузкой сеттера
TerraV
29.06.2017 22:01+1Спасибо огромное за подсказку, не знал. Это вообще отличная иллюстрация к тому, что мы только думаем что программируем на языке/фреймворке, а на деле используем только часть (порой малую) а на остальное городим велосипеды. Честно обещаю исправиться по крайней мере с BehaviorSubject :)
Legion21
29.06.2017 22:28Спасибо за статью, а не думали использовать unity для разработки игры? Я думаю, что так можно еще раза в 2 снизить цену разработки
TerraV
29.06.2017 22:36+1Рассматривал этот вариант, но у Unity есть несколько недостатков которые делают эту платформу мне не интересной.
- Узкая специализация. Навыки, полученные при разработке в Unity можно продать только фирмам, занимающимся разработкой игр на Unity. В случае Java/Kotlin + LibGDX я успешно использовал полученные знания для собеседований. В данный момент у меня оффер на релокацию в Прагу и в Лимассол (Кипр).
- Unity дает очень большой оверхед в плане потребления ресурсов. К примеру запуск подавляющего большинства приложений на юнити это 5-10 секунд. Мое приложение запускается вместе с прогрузкой ресурсов около 1.5-2 секунд. Ну и каждая вторая игра на Unity превращает телефон в обогреватель. Может из-за плохой оптимизации. Не знаю.
sargeras55
29.06.2017 22:30+1Очень странная статья и вопрос стоимости разработки почти не раскрыли, и на Kotlin написали «Hello world», еще и не в лучшем виде.
TerraV
29.06.2017 22:45Да, я понимаю что статья странная. На то она и первая чтобы делать ошибки и учиться. Могли бы вы сформулировать вопрос чтобы я его раскрыл подробно?
jericho_code
30.06.2017 16:40+1«Kotlin выполняется на jvm 6» — уже можно и на 8, а за статью спасибо, как раз начинаю свой проект, некоторые мысли из «мотивации» были кстати.
TerraV
30.06.2017 16:46+1Обычно требование JVM 6 возникает не от хорошей жизни. Это могут быть старые веб сервера или поддержка старых версий Android. К примеру Java 8 появилась только в IBM WebSphere 8.5.5.9. А многим беднягам приходится до сих пор работать с WebSphere 7, это та самая JVM 6 и есть. И получить вкусняшки Kotlin'a на мой взгляд огромный плюс.
В зависимости от Target JVM (а котлин умеет и в 6 и как вы заметили в 8) меняется байткод. Что как я понимаю сказывается в первую очередь на производительности.jericho_code
30.06.2017 16:53Мой комментарий — это просто небольшое уточнение, чтобы не было лишних нападок в сторону языка. Уже не раз видел как Kotlin в минус ставят jvm 6)
mr-garrick
30.06.2017 16:40Что-то последнее время стали много везде говорить про Kotlin, даже подозрительно как-то…
vvgladoun
02.07.2017 10:37+1Действительно, с чего бы это. Вроде ничего не произошло, кроме признания языка гуглом и оффициальной поддержке как first-class citizen-а, начиная с Android Studio 3.0.
mezastel
30.06.2017 19:50.filter { aboveLimit -> aboveLimit == true }
это какой-то ад, разве нельзя написать
.filter{it}
?TerraV
30.06.2017 20:24Можно, конечно можно. Просто статья писалась для людей не владеющими kotlin. И использование явных параметров обусловлено исключительно лучшей читаемость. Просто представьте этот же фрагмент кода но с it. Там три раза меняется контекст этого ключевого слова.
TerraV
30.06.2017 20:31я больше не буду комментировать с телефона :)
cars.forEach { val car = it car.rxSpeed .map { it > 60 } // преобразует double скорость в boolean скоростьПревышена .distinctUntilChanged() .filter { it } .subscribe { writeTicket(car) } }
vagran
30.06.2017 20:25Тем не менее модификаторы final смотрятся крайне неуместно и визуально засоряют программу. Это приводит к тому, что final используется в Java гораздо реже чем следовало бы.
Не большой специалист по Java, но кажется где-то встречал понятие effectively final. Разве современный компилятор не в состоянии сам определить, что переменная не меняется и является по сути константой со ваеми вытекающими оптимизациями?TerraV
30.06.2017 20:33+1https://stackoverflow.com/questions/20938095/difference-between-final-and-effectively-final
В приведенном случае переменная никак не может быть effective final, т.к. она постоянно пересчитывается. В начале операции sum = 0, но для каждого элемента больше нуля происходит инкремент на величину элемента.
Плюс еще один момент: если вы пишете final String — вы можете определить значение или немедленно, или в конструкторе. Любая попытка переопределить будет кидать compile error. В случае effective final ну станет переменная не-финальной. Никаких предупреждений и тем более ошибок не возникнет. А если это проект да с индусами в команде, вы никогда не можете знать откуда ваше поле изменят. Да и наши порой жгут. Привет, Юра!vagran
30.06.2017 21:03Пример с sum, честно говоря, пропустил (чукча не читатель). Сейчас посмотрел, но ведь он не корректен. Корректный эквивалент на java как-то так:
int sum = data.stream().filter(value -> value > 0).mapToInt(Integer::intValue).sum();
И тут sum ничто не мешает быть final.TerraV
30.06.2017 21:25Да, вы правы. Для меня было не очевидно что Integer нужно мапить в intValue. Замечание принято. Правда не могу устоять против приведения аналога на котлине :) Но это уже конкретная вкусовщина и на стоимость разработки/поддержки почти не влияет.
val data: List<Int> = ArrayList() var sum = 0 data.filter { it > 0 }.forEach { sum += it }
samally
02.07.2017 20:30вот так суммировать будет лаконичнее и эффективнее:
val data = ArrayList<Int>() val sum = data.filter { it > 0 }.sum()
Изменяемая переменная захваченная в функцию в котлине оборачивается во враппер – объект, например, IntRef. И так пока что даже для заинлайненных анонимок (это вроде оптимизируют со временем).
TerraV
Похоже я промахнулся с формой подачи материала :) Завтра постараюсь сделать статью полностью по графике. С перечнем ресурсов откуда что можно позаимствовать, ссылки на интересных художников/дизайнеров. Кстати подавляющее большинство интересных и плодотворных дизайнеров — наши с вами соотечественники. Я наткнулся только на одного интересного француза, но к этому времени его уже нанял близзард для дизайна карт в Heartstone.