26 августа 2016

Как я провёл милых два дня в погоне за random()'ом.

Вчера выложил свою игру «Time Hotel» на Kongregate. И тамошняя публика быстро нашла интересный баг. Суть в том, что курсор-призрак иногда не делает тех действий, которые делал игрок ранее. Не держит кнопку, не убивает зомби и так далее. Об этом мне говорил один из игроков на DevGAMM:Moscow, но мне тогда показалось, что в спешке он мог просто не понять как что работает. Ну и просто иногда тяжело уследить за всеми своими воплощениями. Но сейчас, когда на Kong'е народ валом сообщает о том же, я хочу сказать тому тестеру: «Прости, что не воспринял тебя всерьез сразу! Буду доверчивей в следующий раз».
Давно я рассказывал об эффекте перемотки. Сегодня буду о баге, который вывел меня из себя.
Вчера вечером и сегодня полдня я пытался пофиксить этот баг. Если коротко, то дело было в random()'ном стартовом кадре анимации зомби. В разные заходы его голова могла быть немного в разных положениях и из-за этого курсор-призрак промахивается. Казалось бы проблема легко-находящаяся, почему я долго не мог ее отследить?



По пунктам:

  • Код настолько ужасен, что найти логическую цепочку действий иногда безумно сложно :) Цена быстрой разработки — ужасная читабельность кода и архитектура проекта. Такие дела...
  • Ошибка была наслоенной — во-первых стартовый random(), во вторых анимация зомби обновлялась и на стартовом экране с названием, в-третьих стартовый заход и последующие отличались по инициации объектов. Вероятно, это следствие первого пункта.
  • Воспроизвести ошибку было крайне сложно — нужно выстрелить в голову зомби в уникальные 5 кадров, когда сама голова наиболее сильно наклонена. Я несколько часов потратил только, чтобы понять, что ошибка в принципе связана с зомби.
Хотелось сделать так:






Но перфекционизм взял верх, и я принялся за дело.
Как я искал баг:

  • Сначала долго пытался воспроизвести. Много играл, пробовал разное.
  • Затем понял, что проблема с зомби. Иногда предыдущий успешный выстрел заканчивается промахом. Какого черта?
  • Я убил очень много времени, думая, что ошибка в округлении координат, в переходе из экранных координат в мировые, и так далее.
  • Пришла идея о рандоме. Но починить так просто не получалось — в первый день я пофиксил только несколько наслоений ошибки, оставив без внимания последний. Он давал разницу в два кадра анимации. Воспроизвести ошибку стало крайне сложно.
  • Еще была проблема с тем, что Math.random() я заменил на детерминированный, но зерно из-за разной инициации было разным при первом и последующих заходах. Как бы я вернул одно из наслоений бага, но только уже новым, детерминированным рандомом.
  • Чтобы не пытаться воспроизвести баг каждый раз, пришлось разобрать игру, убрать произвольное распределение объектов на уровнях, а сделать их положения определенными и сохранять положения мышки в SharedObject.
  • Как только баг воспроизвелся, я разобрал игру еще сильнее, загружая курсор из SharedObject, и убрав геймплей с отматыванием времени. Я просто смотрел, как ведет себя курсор и все объекты.
  • Методичность дала свое — сдвиг анимации дал о себе знать. Оставалось только найти, почему кадры зомби инициируются по-разному для стартового курсора и его дальнейшего призрака.


Где-то с мая я делал новую idle-defense-игру, основанную на заходах. То есть зашли, поиграли, добыли кристаллов, погибли и по новой. Так как там очень много всего завязано на произвольном спавне, вероятности критического выстрела и так далее, то создавалось неприятное впечатление из-за того, что в разные заходы, с одинаковой экипировкой, игрок зарабатывал разное количество кристаллов. Поэтому я практически на старте разработки отказался от любых Math.random(), влияющих на геймплей. Перешел на детерминированный рандом, с набором из сотни «красивых» сидов. Немного арта новой игры:







Это помогло мне отловить текущий баг. Сложность была в том, что базовый рандом в Time Hotel у меня использовался везде — как для геймплейных элементов, так и для эффектов и прочего. Это дополнительно затруднило правильное понимание и замену вызова этой функции в нужных местах. Мораль, которую я постараюсь сохранить и в последующих проектах — не использовать Math.random(), и прокидывать такие значения через собственные функции, и, при необходимости, быстро подменить их на детерминированный или нет, рандом.

Это было сложно, но баг я починил. Как-то так…

Играем здесь:
Time Hotel на Конге.
Буду рад пятеркам :)

Всем только хороших случайностей!

Сообщения, схожие по тематике:

0 коммент.:

Отправить комментарий