Не так давно вышла моя первая личная игра для мобильных. Суть заключается в том, что врагов надо пережевывать пальцами. Алгоритм не уникальный, но встречается редко. На первый взгляд, нужно только прослушать зум-движение двух пальцев, вроде ничего сложного, однако в процессе разработки игры выявляются различные подводные камни.

  • Вычисление времени на сжатие челюстей;
  • Сочетание жевания с управлением персонажем;
  • Изменение параметров по ходу тестов.

Весь код написан на языке с# для движка Unity3D, для 2Д игры. Перейдем непосредственно к коду. В методе Update вычисляем кол-во тачей, и производим соответствующие действия. Двигаем персонажа в случае одного прикосновения:

//Если одно прикосновение
if (Input.touchCount == 1) {
//Если челюсти не сжимаются или не разжимаются, персонаж двигается к месту прикосновения
	if (!compressing && !decompressing) { 
		Touch singleTouch = Input.GetTouch(0);
		Vector3 targetPoint = Camera.main.ScreenToWorldPoint (singleTouch.position);
		targetPoint = new Vector3 (targetPoint.x, targetPoint.y, 0);
		transform.position = Vector3.MoveTowards (transform.position, targetPoint, movementSpeed * Time.deltaTime);
	}
}

Тут ничего сложного, можно двигаться дальше. Код обработки двух касаний. Если нет сжимания/разжимания челюстей, то персонаж перемещается между двух пальцев.

if (Input.touchCount > 1) {
        //Работа с двумя первыми касаниями.
        Touch touch1 = Input.GetTouch(0);
	Touch touch2 = Input.GetTouch(1);
        //Если челюсти не работают, передвигаем персонажа между пальцами
	if (!compressing && !decompressing) {
		Vector3 targetPoint = Camera.main.ScreenToWorldPoint ((touch1.position + touch2.position) / 2);
		targetPoint = new Vector3 (targetPoint.x, targetPoint.y, 0);
		transform.position = Vector3.MoveTowards (transform.position, targetPoint, movementSpeed * Time.deltaTime);
	}

        float currentDistance = Vector2.Distance(touch1.position, touch2.position);
        if(pastFingersDistance == 0) {
                //Обнуление прошлого расстояния, если первый раз засечено два тача
		pastFingersDistance = currentDistance; 
	}else if(currentDistance < pastFingersDistance - fingersMunchDetectionMin) { 
                //Метод включения сжатия челюстей. Управление графикой, у каждого индивидуально.
		SetCompression(); 
	}else if(currentDistance > pastFingersDistance + fingersMunchDetectionMin) {
                //Метод включения разжатия челюстей. Управление графикой, у каждого индивидуально.
		SetDecompression(); 
	}
}
//Обнуление переменной, для того чтоб вычислять необходимость жевания относительно новой позиции пальцев.
if(Input.touchCount < 2) pastFingersDistance = 0;
//Если касаний стало меньше двух, а челюсти сжаты - они автоматически разжимаются.
if(Input.touchCount < 2 && isCompressed)  SetDecompression(); 

fingersMunchDetectionMin — переменная, определяющая какое расстояние достаточно для того, чтобы начать жевание. Достаточно долго настраивал с помощью нескольких друзей. У каждого оказалось разное восприятие, вывел нечто среднее. В ходе тестов также выяснилось, что постоянно жевать пальцами пользователю попросту неудобно. Возникла необходимость сделать сжимание челюстей по простому тапу и метод, изложенный выше, приобрел следующий вид:

if (Input.touchCount > 1) {
        //Работа с двумя первыми касаниями.
	Touch touch1 = Input.GetTouch(0);
	Touch touch2 = Input.GetTouch(1);
        //Проверка если челюсти не работают
	if (!compressing && !decompressing) { 
		float touch1Time = 0;
		float touch2Time = 0;
                //Вычисляется сколько времени активен тач 1
		if (tapsHash.Contains (touch1.fingerId)) {
			float startTouch1Time = (float) tapsHash [touch1.fingerId];
			touch1Time = Time.time - startTouch1Time;
		}
                //Вычисляется сколько времени активен тач 2
		if (tapsHash.Contains (touch2.fingerId)) {
			float startTouch2Time = (float) tapsHash [touch2.fingerId];
			touch2Time = Time.time - startTouch2Time;
		}
                //Если время отведенное на тап уже превышено для двух пальцев, персонаж передвигается между пальцами.
		if (touch1Time > SECONDS_FOR_TAP && touch2Time > SECONDS_FOR_TAP) {
			Vector3 targetPoint = Camera.main.ScreenToWorldPoint ((touch1.position + touch2.position) / 2);
			targetPoint = new Vector3 (targetPoint.x, targetPoint.y, 0);
			transform.position = Vector3.MoveTowards (transform.position, targetPoint, movementSpeed * Time.deltaTime);
		}
	}
	float currentDistance = Vector2.Distance(touch1.position, touch2.position);
        if(pastFingersDistance == 0) {
                //Обнуление прошлого расстояния, если первый раз засечено два тача
		pastFingersDistance = currentDistance; 
	}else if(currentDistance < pastFingersDistance - fingersMunchDetectionMin) {
                 //Метод включения сжатия челюстей. Управление графикой, у каждого индивидуально.
		SetCompression(); 
	}else if(currentDistance > pastFingersDistance + fingersMunchDetectionMin) {
                 //Метод включения разжатия челюстей. Управление графикой, у каждого индивидуально.
		SetDecompression();
	}
}
//Обнуление переменной, для того чтоб вычислять необходимость жевания относительно новой позиции пальцев.
if(Input.touchCount < 2) pastFingersDistance = 0;
//Если касаний стало меньше двух, а челюсти сжаты - они автоматически разжимаются.
if(Input.touchCount < 2 && isCompressed)  SetDecompression(); 
//Метод который отвечает за осуществление жевания по тапу.
SetTapAttackListener ();

Константа SECONDS_FOR_TAP — время, отведенное на тап, как и расстояние на жевание, достаточно долго тестировалась и настраивалась. Ну и собственно последние методы, которые осуществляют жевание по простому тапу:

void SetTapAttackListener() {
	if (Input.touchCount > 0) {
		foreach (Touch touch in Input.touches) {
                        //Обработка активного тача
			DetectOneTouchTap (touch); 
		}
	}
}

void DetectOneTouchTap(Touch touch) {
	if (touch.phase == TouchPhase.Began) {
                //В случае если тач только начался, он записывается в хэш-таблицу для обработки.
                //Ключ - ид тача, значение - начало прикосновения.
		tapsHash.Add (touch.fingerId, Time.time); 
	} else if(touch.phase == TouchPhase.Ended) {
		float startTouchTime = (float) tapsHash [touch.fingerId];
		float timeOfTouch = Time.time - startTouchTime;
                //Осуществление сжатия и разжатия челюстей, если тач был тапом
		if (timeOfTouch <= SECONDS_FOR_TAP) {
			SetCompression();
			SetDecompression();
		}
		tapsHash.Remove (touch.fingerId);
	}
}

В начале пытался найти сей алгоритм на просторах интернета, не для копипаста, а для проверки своего хода мыслей. Однако ничего не нашел и решил выложить его в помощь коллегам. Сейчас очень хорошо вижу, что код несколько хаотичен, ну а в остальном — жду комментариев.

Update 1:
Демонстрация работы алгоритма:


Update 2:
Статья о разработке игры

Комментарии (11)


  1. gturk
    29.04.2016 12:20
    +5

    Немного не по существу, но этот код можно прямо сейчас в парижскую палату мер и весов, как образец бесполезных комментариев в коде


    1. romeoq
      29.04.2016 12:28
      +3

      Ну честно говоря я комментарии для хабра написал. Песочница же, первая публикация. Я как-то забыл что коллеги будут смотреть.


      1. HeaTTheatR
        29.04.2016 16:25
        +6

        Не обращайте внимания на:

        > этот код можно прямо сейчас в парижскую палату мер и весов, как образец бесполезных комментариев в коде

        Уверен, если бы вы предоставили в коде на десяток комментариев меньше, последовало:

        > этот код можно прямо сейчас в парижскую палату мер и весов, как образец отсутствия комментариев в коде

        Злоупотребления комментприями в вашем коде нет. Предложите тому человеку, который написал вам первый комментарий прокомментировать, что происходит в вашем коде. Только перед этим удалите ВСЕ свои комментарии из кода в статье.

        Помните, что комментировать код нужно так, как если бы знали, что его будет читать маньяк, знающий ваш адресс проживания. Это, одноко, не говорит о том, что нужно снабжать комментариями КАЖДУЮ строчку. В вашем случае — первый комментарий к вашей статье — просто позерство!

        Спасибо за код и статью.


        1. romeoq
          29.04.2016 16:38
          +3

          Спасибо за поддержку. В песочнице это важно!


        1. Vilyx
          29.04.2016 16:41
          +1

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


      1. dimas6000
        29.04.2016 19:14

        Не слушайте gturk и спасибо за комментарии. Я только учусь программировать и такие комментарии очень помогают ускорить понимание кода.


  1. samodum
    29.04.2016 12:29

    Было бы очень кстати приложить видео как всё это выглядит в реальности


    1. romeoq
      29.04.2016 12:34

      Опять же песочница, а видео с ссылкой на игру. Даже не знаю если корректно так делать.


      1. TimsTims
        29.04.2016 12:48

        Корректно. Ведь в статье даже ни одной картинки (их бы тоже не мешало бы добавить, т.к. здесь все-таки обсуждается видео-игра, а не просто код, который решает научную задачу). А лучше все-же GIF-ку, потому-что не у всех youtube на работе корректно работает )


        1. romeoq
          29.04.2016 13:52
          +3

          GIF-ку добавил.


  1. DyadichenkoGA
    01.05.2016 18:05

    По стилистике кода. Я бы советовал писать по-разному локальные переменные, поля класса и т. п. Просто видимо тут поля класса в стиле простых переменных, не критично, но читать удобнее когда с ходу понятно что есть что. (Вот пример одного из моих классов pastebin.com/ezvhTYG1, с помощью которых обрабатывается свайп в одном проекте, он большой, но тут по стилю по сути использовано всё)