Существует немало трюков, которые могут облегчить жизнь геймдевелоперу. Многие начинающие разработчики даже и не подозревают о том как можно использовать примитивные вычисления, с легкостью решая повседневные задачи.
Для примера приведенного выше, если вызывать метод с параметром 5, модуль по числу 16 будет браться из диапазона значений (6; 20).
Предположим, что у нас есть карта, хранящаяся в виде двумерной сетки из множества ячеек. Тогда каждая ячейка будет иметь X и Y координату в этой сетке.
Таким образом, карту можно хранить в виде двухмерного массива, а обращаться к каждому элементу по двум индексам. Иногда такой подход оправдан, но с ростом сложности проекта, два значения начинают создавать проблемы. А если нужно хранить больше значений? (в одном проекте мне нужно было использовать адрес «между двумя клетками» – 4 значения)
Трюк заключается в записи двух чисел подряд в одно, с помощью операции битового смещения.
если нашему приложению хватает 65536 возможных вариантов для каждого значения, то два значения можно записать в переменную типа Integer.
Integer – хранит в себе 4 байта информации или 32 бита. В двоичной системе это вот столько:
1111 1111 1111 1111 1111 1111 1111 1111
65536 – это собственно половина Integer – 2 байта.
Для того чтобы сохранить числа X:43 (0010 1011) и Y:6435 (0001 1001 0010 0011), необходимо совершить следующие действия:
При всем при этом, существует проблема связанная с тем, что Integer – тип знаковый. И старший бит предназначен для хранения знака (0 – число положительное, 1 – отрицательное). В связи с этим, при декодировании происходит ошибка. Исправить этот эффект можно вычитая при кодировании из первой части кода половину максимально возможного кодируемого значения (для приведенного выше примера – это 65536 / 2 = 32768), а при декодировании, прибавления его к результату.
Пример кода:
1. «Прокрутка» по диапазону значений
Очень распространенная задача, когда нужно инкрементировать значение, а по достижению максимального значения возвращаться к нулю. Например, переключение колесиком мыши выбранного оружия из 8 возможных.
Как это можно было сделать и как это делают чаще всего:
public int selectedWeapon; public const int MAX_WEAPONS = 8; public void Increment() { selectedWeapon++; if (selectedWeapon >= MAX_WEAPONS) { selectedWeapon = 0; } } public void Decrement() { selectedWeapon--; if (selectedWeapon < 0) { selectedWeapon = MAX_WEAPONS; } }
Намного элегантнее это можно сделать с помощью операции вычисления остатка от деления:
public int selectedWeapon; public const int MAX_WEAPONS = 8; public void Increment() { selectedWeapon = (selectedWeapon + 1) % MAX_WEAPONS; } public void Decrement() { selectedWeapon = (MAX_WEAPONS + selectedWeapon - 1) % MAX_WEAPONS; }
Что же здесь произошло?! Дело в том, что мы всякий раз получаем лишь остаток от деления на на наше максимальное значение, в данном случае 8. Таким образом, при достижении максимального значения, остаток будет равен 0.
С вычитанием операция несколько сложнее, так как мы уходим в отрицательную сторону:
(-1) % 8 = -1
Это решается простым добавлением максимального значения, так что остаток берется от:
8 + 0 - 1 = 7
7 % 8 = 7
2. Случайное значение из диапазона, за исключением заданного
С этой задачей можно столкнуться, например, когда из ряда персонажей нужно выбрать случайного противника, отличного от выбранного игроком. Эта задача очень похожа на предыдущую и так же решается с помощью нахождения остатка от деления.
Предположим, что персонажи хранятся в массиве. Тогда выбор случайного будет выглядеть так:
GameCharacter[] characters = new GameCharacter[16]; Random r = new Random(); public GameCharacter GetRandomCharacter(int playerCharIndex) { int enemyIndex = (playerCharIndex + 1 + r.Next(characters.Length - 1)) % characters.Length; return characters[enemyIndex]; }Таким образом, мы совершаем выборку случайного значения, из диапазона всех значений после выбранного и до следующего его появления.
Для примера приведенного выше, если вызывать метод с параметром 5, модуль по числу 16 будет браться из диапазона значений (6; 20).
3. Быстрые вычисления
Несмотря на то, что вычислительные мощности компьютеров и даже мобильных устройств сегодня позволяют не задумываться о повышении производительности на низком уровне (хотя все еще остаются области, где нужно экономить самые крохи процессорного времени и памяти, например при написании шейдеров), следует быть знакомым с простейшими приемами, связанными с принципами работы процессора, к примеру:- Деление с помощью умножения. Операция деления значительно затратнее, нежели умножение. Будет полезно взять в привычку, вместо деления, к примеру на 10, умножать на 0.1. По скромному мнению автора, это как минимум повышает читабельность сложных формул в коде.
- Так же деление/умножение на степени двойки можно совершать с помощью операции битового смещения:
37 << 4 = 37 * 2 * 2 * 2 * 2 = 592
136 >> 3 = (((136 / 2) / 2) /2 = 17
Двоичные операции значительно быстрее десятичных! - Операция побитового умножения «И» (&) может быть использована для проверки числа, является ли оно степенью двойки:
(x & (x - 1)) == 0
Конечно, хороший компилятор может проанализировать и заменить многие вычисления на их быстрые аналоги, поэтому всегда нужно знать меру и не портить читабельность кода, в угоду сомнительной предварительной оптимизации.
4. Кодирование нескольких значений в одно поле
Этот прием крайне полезен для экономии памяти и упрощении обозначения каких либо связанных значений, например координат.Предположим, что у нас есть карта, хранящаяся в виде двумерной сетки из множества ячеек. Тогда каждая ячейка будет иметь X и Y координату в этой сетке.
Таким образом, карту можно хранить в виде двухмерного массива, а обращаться к каждому элементу по двум индексам. Иногда такой подход оправдан, но с ростом сложности проекта, два значения начинают создавать проблемы. А если нужно хранить больше значений? (в одном проекте мне нужно было использовать адрес «между двумя клетками» – 4 значения)
Трюк заключается в записи двух чисел подряд в одно, с помощью операции битового смещения.
если нашему приложению хватает 65536 возможных вариантов для каждого значения, то два значения можно записать в переменную типа Integer.
Integer – хранит в себе 4 байта информации или 32 бита. В двоичной системе это вот столько:
1111 1111 1111 1111 1111 1111 1111 1111
65536 – это собственно половина Integer – 2 байта.
Для того чтобы сохранить числа X:43 (0010 1011) и Y:6435 (0001 1001 0010 0011), необходимо совершить следующие действия:
- Сместить 43 на 16 бит влево: получаем 0000 0000 0010 1011 0000 0000 0000 0000 или 2818048 в десятичной системе
- Добавить к полученному числу вторую координату: 2818048 + 6435 = 2824483. Однако, так как мы добавляем предыдущее значение в разряды заполненные нулями, можно воспользоваться операцией побитового «ИЛИ» ( | )
- Готово! Мы получили число 0000 0000 0010 1011 0001 1001 0010 0011. Можете проверить этот процесс в калькуляторе в режиме «Программист».
Для того чтобы раскодировать полученное значение, необходимо совершить следующие действия:
- Сместить код на 16 бит вправо. Так младшие два байта исчезнут, останутся только старшие два, которые будут соответствовать значению X:43
- Значение Y можно найти двумя способами:
- Самый примитивный, приходящий в голову людям, незнакомым с двоичной арифметикой – сместить значение X обратно, на 16 бит влево и вычесть полученное число из кода. Этот вариант дает возможность вычислить лишь последнюю часть кода.
- Более быстрым и "правильным" будет использование маски – побитового умножения «И» (&).
Если совершить это действие над кодом с аргументом в 1111 1111 1111 1111 (65535), то мы получим именно последние два байта, соответствующие координате Y: 2824483 & 65535 = 6435.
Для упрощения подобной операции рекомендуется использовать шестнадцатеричную запись числа 65535 – 0xFFFF. Это легко запомнить, ибо один байт заполненный единицами будет записан как 0xFF, три байта – 0xFFFFFF и т.д.
При всем при этом, существует проблема связанная с тем, что Integer – тип знаковый. И старший бит предназначен для хранения знака (0 – число положительное, 1 – отрицательное). В связи с этим, при декодировании происходит ошибка. Исправить этот эффект можно вычитая при кодировании из первой части кода половину максимально возможного кодируемого значения (для приведенного выше примера – это 65536 / 2 = 32768), а при декодировании, прибавления его к результату.
Пример кода:
public int Code (int x, int y) { return ((x - 32768) << 16) | y; } public int DecodeX (int code) { return (code >> 16) + 32768; } public int DecodeY (int code) { return code & 0xFFFF; }Следует учитывать, что полученный код может иметь отрицательное значение и потому не может быть использован, как индекс в массиве. Возможные решения:
- Хранение данных в коллекции ключ-значение
- Отказ от использования значений, приводящих к отрицательному коду (сокращение возможных значений вдвое)
- Использование типов данных бОльшего размера, для хранения кода, например long