Это может занять некоторое время, чтобы объяснить — иди перекуси, пока читаешь это.
Я разрабатываю 2D платформерную игру-головоломку для Gameboy Advance на C ++ (я довольно новый программист). Вплоть до прошлой ночи я создавал физический движок (только некоторые ограничивающие оси граничные рамки), который я тестировал, используя уровень, равный размеру экрана GBA. Тем не менее, финальная игра потребует наличия уровня, превышающего размер экрана, и поэтому я попытался реализовать систему, которая позволяет экрану GBA следовать за игроком, и в результате я должен нарисовать все на экране относительно смещений экрана.
Однако, у меня возникают проблемы, когда я показываю кубы, которые можно поднять и манипулировать на уровне. Всякий раз, когда игрок движется, местоположения кубов на экране, кажется, отклоняются от их фактических положений на уровне. Это как когда кубы нарисованы, это один кадр из синхронизации — когда я ставлю игру на паузу, когда игрок движется, ящики отображаются в правильном положении, но когда я делаю паузу, они сдвигаются, пока игрок не остановится двигаясь снова.
Краткое описание моих классов — есть базовый класс с именем Object, который определяет (x, y) позицию, ширину и высоту, есть класс Entity, который наследуется от Object и добавляет компоненты скорости, и класс Character, который наследуется от Entity. и добавляет функции движения. Мой игрок — это объект персонажа, а кубы, которые я хочу собрать, — это массив объектов сущности. И игрок, и массив кубов являются членами класса Level, который также наследуется от Object.
Я подозреваю, что проблема заключается в последнем примере кода, однако для полного понимания того, что я пытаюсь сделать, я выложил примеры в несколько более логичном порядке.
Вот усеченные заголовки уровня:
class Level : public Object
{
private:
//Data
int backgroundoffsetx;
int backgroundoffsety;
//Methods
void ApplyEntityOffsets();
void DetermineBackgroundOffsets();
public:
//Data
enum {MAXCUBES = 20};
Entity cube[MAXCUBES];
Character player;
int numofcubes;
//Methods
Level();
void Draw();
void DrawBackground(dimension);
void UpdateLevelObjects();
};
…и сущность:
class Entity : public Object
{
private:
//Methods
int GetScreenAxis(int &, int &, const int, int &, const int);
public:
//Data
int drawx; //Where the Entity's x position is relative to the screen
int drawy; //Where the Entity's y position is relative to the screen
//Methods
void SetScreenPosition(int &, int &);
};
Вот соответствующие части моего основного игрового цикла:
//Main loop
while (true)
{
...
level.MoveObjects(buttons);
level.Draw();
level.UpdateLevelObjects();
...
}
Из-за способа отображения спрайтов в правильных местах при приостановке, я почти уверен, что проблема не в MoveObjects()
, который определяет зелья игрока и кубики на уровне относительно уровня. Так что уходит Draw()
а также UpdateLevelObjects()
,
В порядке, Draw()
, Я предоставляю это в том случае, если неправильно отображаются не мои кубы, а уровень и платформы, на которых они расположены (я не думаю, что это проблема, но, возможно,). Draw()
вызывает только одну соответствующую функцию, DrawBackground()
:
/**
Draws the background of the level;
*/
void Level::DrawBackground(dimension curdimension)
{
...
//Platforms
for (int i = 0; i < numofplatforms; i++)
{
for (int y = platform[i].Gety() / 8 ; y < platform[i].GetBottom() / 8; y++)
{
for (int x = platform[i].Getx() / 8; x < platform[i].GetRight() / 8; x++)
{
if (x < 32)
{
if (y < 32)
{
SetTile(25, x, y, 103);
}
else
{
SetTile(27, x, y - 32, 103);
}
}
else
{
if (y < 32)
{
SetTile(26, x - 32, y, 103);
}
else
{
SetTile(28, x - 32, y - 32, 103);
}
}
}
}
}
}
Это неизбежно требует некоторого количества объяснения. Мои платформы измеряются в пикселях, но отображаются в виде плиток размером 8×8 пикселей, поэтому я должен разделить их размеры для этого цикла. SetTile()
во-первых, требуется номер экрана блокировки. Фоновый слой, который я использую для отображения платформ — это плитки размером 64×64, поэтому для их отображения требуются блоки экрана размером 2×2 по 32×32 каждая. Экранные блоки пронумерованы 25-28. 103 — это номер плитки в моей карте тайлов.
Вот UpdateLevelObjects()
:
/**
Updates all gba objects in Level
*/
void Level::UpdateLevelObjects()
{
DetermineBackgroundOffsets();
ApplyEntityOffsets();
REG_BG2HOFS = backgroundoffsetx;
REG_BG3HOFS = backgroundoffsetx / 2;
REG_BG2VOFS = backgroundoffsety;
REG_BG3VOFS = backgroundoffsety / 2;
...
//Code which sets player position (drawx, drawy);
//Draw cubes
for (int i = 0; i < numofcubes; i++)
{
//Code which sets cube[i] position to (drawx, drawy);
}
}
REG_BG
биты — это регистры GBA, которые позволяют фоновым слоям смещаться по вертикали и горизонтали на количество пикселей. Эти смещения сначала рассчитываются в DetermineBackgroundOffsets()
:
/**
Calculate the offsets of screen based on where the player is in the level
*/
void Level::DetermineBackgroundOffsets()
{
if (player.Getx() < SCREEN_WIDTH / 2) //If player is less than half the width of the screen away from the left wall of the level
{
backgroundoffsetx = 0;
}
else if (player.Getx() > width - (SCREEN_WIDTH / 2)) //If player is less than half the width of the screen away from the right wall of the level
{
backgroundoffsetx = width - SCREEN_WIDTH;
}
else //If the player is in the middle of the level
{
backgroundoffsetx = -((SCREEN_WIDTH / 2) - player.Getx());
}
if (player.Gety() < SCREEN_HEIGHT / 2)
{
backgroundoffsety = 0;
}
else if (player.Gety() > height - (SCREEN_HEIGHT / 2))
{
backgroundoffsety = height - SCREEN_HEIGHT;
}
else
{
backgroundoffsety = -((SCREEN_HEIGHT / 2) - player.Gety());
}
}
Просто быть чистым, width
относится к ширине уровня в пикселях, в то время как SCREEN_WIDTH
относится к постоянному значению ширины экрана GBA. Также извините за ленивое повторение.
Вот ApplyEntityOffsets
:
/**
Determines the offsets that keep the player in the middle of the screen
*/
void Level::ApplyEntityOffsets()
{
//Player offsets
player.drawx = player.Getx() - backgroundoffsetx;
player.drawy = player.Gety() - backgroundoffsety;
//Cube offsets
for (int i = 0; i < numofcubes; i++)
{
cube[i].SetScreenPosition(backgroundoffsetx, backgroundoffsety);
}
}
В основном это центрирует игрока на экране, когда он находится в середине уровня, и позволяет ему двигаться к краям, когда экран сталкивается с краем уровня. Что касается кубов:
/**
Determines the x and y positions of an entity relative to the screen
*/
void Entity::SetScreenPosition(int &backgroundoffsetx, int &backgroundoffsety)
{
drawx = GetScreenAxis(x, width, 512, backgroundoffsetx, SCREEN_WIDTH);
drawy = GetScreenAxis(y, height, 256, backgroundoffsety, SCREEN_HEIGHT);
}
Терпите меня — я объясню 512 и 256 в данный момент. Вот GetScreenAxis()
:
/**
Sets the position along an axis of an entity relative to the screen's position
*/
int Entity::GetScreenAxis(int &axis, int &dimensioninaxis, const int OBJECT_OFFSET,
int &backgroundoffsetaxis, const int SCREEN_DIMENSION)
{
int newposition;
bool onawkwardedgeofscreen = false;
//If position of entity is partially off screen in -ve direction
if (axis - backgroundoffsetaxis < dimensioninaxis)
{
newposition = axis - backgroundoffsetaxis + OBJECT_OFFSET;
onawkwardedgeofscreen = true;
}
else
{
newposition = axis - backgroundoffsetaxis;
}
if ((newposition > SCREEN_DIMENSION) && !onawkwardedgeofscreen)
{
newposition = SCREEN_DIMENSION; //Gets rid of glitchy squares appearing on screen
}
return newposition;
}
OBJECT_OFFSET
(512 и 256) — это особенность GBA — установка отрицательного числа для позиции x или y объекта не будет выполнять то, что вы обычно намерены — она испортит спрайт, используемый для его отображения. Но есть хитрость: если вы хотите установить отрицательную позицию X, вы можете добавить 512 к отрицательному числу, и спрайт появится в правильном месте (например, если вы собирались установить его в -1, то установите его в 512 + -1 = 511). Точно так же добавление 256 работает для отрицательных позиций Y (это все относительно экрана, а не уровня). В последнем операторе if кубы отображаются частично за пределами экрана, если они обычно отображаются дальше, так как попытка отобразить их слишком далеко приводит к появлению глючных квадратов, опять же, специфических для GBA.
Вы абсолютный святой, если вы зашли так далеко, прочитав все. Если вы сможете найти то, что может вызывать дрейфующие кубики, я буду ОЧЕНЬ благодарен. Кроме того, любые советы, как вообще улучшить мой код, будут оценены.
Изменить: способ обновления объектов GBA для настройки игрока и позиции кубов выглядит следующим образом:
for (int i = 0; i < numofcubes; i++)
{
SetObject(cube[i].GetObjNum(),
ATTR0_SHAPE(0) | ATTR0_8BPP | ATTR0_REG | ATTR0_Y(cube[i].drawy),
ATTR1_SIZE(0) | ATTR1_X(cube[i].drawx),
ATTR2_ID8(0) | ATTR2_PRIO(2));
}
Я объясню в этом ответе, как работают побитовые операторы и как одно число позволяет, скажем, байту с возможным значением от 0 до 255 (256 комбинаций) хранить все нажатия управляющих сигналов GBA. Что похоже на вашу проблему положения X / Y.
Элементы управления
Up - Down - Left - Right - A - B - Select - Start
Это элементы управления GameBoy Color. Я думаю, что в GameBoy Advanced есть больше элементов управления.
Всего 8 элементов управления.
Каждый элемент управления можно либо нажать (удерживать), либо не нажимать.
Это означает, что каждый элемент управления должен использовать только номер 1
или же 0
,
поскольку 1
или же 0
занимает всего 1 бит информации. В одном байте вы можете хранить до 8 разных битов, что соответствует всем элементам управления.
Теперь вы можете подумать, как я могу объединить их, добавив или что-то? да, вы можете сделать это, но это усложняет понимание и создает проблему.
Скажем, у вас есть стакан воды, который наполовину пуст, и вы добавляете в него больше воды, и вы хотите отделить вновь добавленную воду от старой воды … вы просто не можете этого сделать, потому что вода стала одной водой без возможности отмените это (если мы не помечаем каждую молекула воды, и мы еще не инопланетяне … смеется).
Но с побитовыми операциями он использует математику, чтобы выяснить, какой именно бит 1
или же 0
во всем потоке (списке) битов.
Итак, первое, что вы делаете, вы отдаете каждый бит контролю.
Каждый бит в двоичном виде кратен 2, поэтому вы просто удваиваете значение.
Up - Down - Left - Right - A - B - Select - Start
1 - 2 - 4 - 8 - 16 - 32 - 64 - 128
Также побитовые операции используются не только для определения, какой бит 1
или же 0
Вы также можете использовать их для объединения определенных вещей. Элементы управления делают это хорошо, так как вы можете нажать и удерживать несколько кнопок одновременно.
Вот код, который я использую, чтобы выяснить, нажата или нет нажата.
Я не использую C / C ++, так что это javascript
Я использовал это для моего веб-сайта эмулятора gameboy, строковая часть может быть неправильной, но фактический побитовый код универсален почти на всех языках программирования, единственное различие, которое я видел, это Visual Basic &
будет называться AND
там.
function WhatControlsPressed(controlsByte) {
var controlsPressed = " ";
if (controlsByte & 1) {
controlsPressed = controlsPressed + "up "}
if (controlsByte & 2) {
controlsPressed = controlsPressed + "down "}
if (controlsByte & 4) {
controlsPressed = controlsPressed + "left "}
if (controlsByte & 8) {
controlsPressed = controlsPressed + "right "}
if (controlsByte & 16) {
controlsPressed = controlsPressed + "a "}
if (controlsByte & 32) {
controlsPressed = controlsPressed + "b "}
if (controlsByte & 64) {
controlsPressed = controlsPressed + "select "}
if (controlsByte & 128) {
controlsPressed = controlsPressed + "start "}
return controlsPressed;
}
Как установить индивидуальный элемент управления для нажатия? ну, вы должны помнить, какой битовый номер вы использовали для какого контроля я бы сделал что-то вроде этого
#DEFINE UP 1
#DEFINE DOWN 2
#DFFINE LEFT 4
#DEFINE RIGHT 8
Так скажем, вы нажимаете Up
а также A
сразу так ты нажал 1
а также 16
Вы делаете 1 байт, который содержит все элементы управления, скажем,
unsigned char ControlsPressed = 0;
Так что ничего не нажимается сейчас, потому что это 0.
ControlsPressed |= 1; //Pressed Up
ControlsPressed |= 16; //Pressed A
Так что да ControlsPressed
теперь будет держать номер 17
Вы можете думать только 1+16
это именно то, что он делает, лол, но да, вода, которую вы не можете вернуть к ее базовым ценностям, которые в первую очередь составили ее с использованием базовой математики.
Но да, вы можете изменить это 17
в 16
и бац вы отпустите стрелку вверх и просто удерживая A
кнопка.
Но когда вы удерживаете много кнопок, значение становится таким большим, скажем так.
1+4+16+128
знак равно 149
Таким образом, вы не помните, что вы сложили, но вы знаете, значение 149
как ты вернешь ключи сейчас? ну, это довольно легко, да, просто начните вычитать наибольшее число, которое вы можете найти использование элементов управления ниже, чем 149
и вы, если он больше, когда вычитаете его, он не прижимается.
Да, в этот момент вы думаете, что да, я мог бы сделать несколько циклов и сделать это, но этого не нужно делать, есть встроенные команды, которые делают это на лету.
Вот как вы нажимаете любую из кнопок.
ControlsPressed = ControlsPressed AND NOT (NEGATE) Number
В C / C ++ / Javascript вы можете использовать что-то вроде этого
ControlsPressed &= ~1; //Let go of Up key.
ControlsPressed &= ~16; //Let go of A key.
Что еще сказать, это обо всем, что вам нужно знать о побитовых вещах.
РЕДАКТИРОВАТЬ:
Я не объяснил операторы побитового сдвига <<
или же >>
Я действительно не знаю, как объяснить это на базовом уровне.
Но когда вы видите что-то подобное
int SomeInteger = 123;
print SomeInteger >> 3;
Что там есть оператор сдвига вправо, привыкший там, и он сдвигает 3 бита вправо.
Что он на самом деле делает, так это делит на 2 до степени 3.
Так что в базовой математике это действительно делает это
SomeInteger = 123 / 8;
Итак, теперь вы знаете, что сдвиг вправо >>
это то же самое, что деление значения на степени 2.
Теперь смещается влево <<
логично будет означать, что вы умножаете значение на степени 2.
Сдвиг битов в основном используется для объединения двух разных типов данных и их последующего извлечения. (Я думаю, что это наиболее распространенное использование сдвига битов).
Допустим, в вашей игре есть координаты X / Y, каждая координата может иметь только ограниченное значение.
(Это всего лишь пример)
X: (0 to 63)
Y: (0 to 63)
И вы также знаете, что X, Y должны быть сохранены в некотором маленьком типе данных. Я предполагаю, что очень плотно упакован (без пробелов).
(это может потребовать некоторого реверс-инжиниринга, чтобы точно понять или просто читать руководства).
Там могут быть пропуски, используемые для зарезервированных битов или некоторой неизвестной информации.
Но, двигаясь здесь, можно получить 64 разных комбинации.
Так что оба X
а также Y
каждый занимает 6 битов, всего 12 бит для обоих.
Таким образом, всего 2 бита сохраняются для каждого байта. (Всего сохранено 4 бита).
[1 2 4 8 16 32] | [1 2 4 8 16 32] [1 2 4 8 16 32 64] [128 1 2 4 8 [16 32 64 128]X | Y
Таким образом, вам нужно использовать сдвиг битов для правильного хранения информации.
Вот как вы их храните
int X = 33;
int Y = 11;
Поскольку каждая координата занимает 6 битов, это означает, что вам нужно сдвинуть влево на 6 для каждого числа.
int packedValue1 = X << 6; //2112
int packedValue2 = Y << 6; //704
int finalPackedValue = packedValue1 + packedValue2; //2816
Так что да, окончательное значение будет 2816
Теперь вы получаете значения обратно из 2816
делать то же смещение в противоположном направлении.
2816 >> 6 //Gives you back 44. lol.
Так что да, проблема с водой снова возникла, у вас 44 (33 + 11), и у вас нет возможности вернуть ее, и на этот раз вы не можете рассчитывать на силу 2, чтобы помочь вам.
Я использовал очень грязный код, чтобы показать вам, почему вы должны специально усложнять его, чтобы избежать ошибок в будущем.
В любом случае, вернитесь к его 6 битам на координату, и вы должны взять 6 и добавить его туда.
так что теперь у вас есть 6
а также 6+6=12
,
int packedValue1 = X << 6; //2112
int packedValue2 = Y << 12; //45056
int finalPackedValue = packedValue1 + packedValue2; //47168
Так что да, окончательное значение теперь больше 47168. Но, по крайней мере, теперь у вас не будет проблем с возвратом значений. Единственное, что нужно помнить, это то, что вы должны сделать это в противоположном направлении.
47168 >> 12; //11
Теперь вам нужно выяснить, из чего состоит большое число 11, чтобы сдвинуть его назад влево 12 раз.
11 << 12; //45056
Вычесть из первоначальной суммы
//47168 - 45056 = 2112
Теперь вы можете закончить сдвиг вправо на 6.
2112 >> 6; //33
Теперь вы вернули оба значения ..
Вы можете сделать упаковочную часть намного проще с помощью битовой команды выше для добавления элементов управления вместе.
int finalPackedValue = (X << 6) | (Y << 12);