SGDK. Создание платформера для Sega Genesis.

Предисловие.

Скачайте готовый файл проекта с MEGA.

В этом уроке мы создадим платформер на SGDK. Да, так быстро, и сразу в пекло, приступим.

Что потребуется для платформера.

А потребуется следующее:

  • Поместить спрайт игрока в центр экрана.
  • Скроллить карту мира в зависимости от координат игрока.
  • Остановить эту карту, если игрок столкнулся с тайловой сеткой (добавить коллизию).
  • Дать игроку гравитацию (просто, прибавлять +1 к y, каждый кадр).
  • Позволить игроку прыгать (если на земле, отнять 10 по y).

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

  • Остановить эту карту, если игрок столкнулся с тайловой сеткой (добавить коллизию).

Остальные, мы либо изучали, либо до них не трудно догадаться. А теперь вопрос, каким образом, мы поймем тайл на карте является твердым?

Разбираемся в массиве коллизий.

На помощь приходит массив коллизий.

Данную картинку я создавал для статьи SGDK. Перемещаемся по карте. Но она не подошла по смыслу, поэтому я её переработал.

Массив коллизий — это 2-х мерный массив хранящий информацию о твердости тайла. Например:

  • 0твердый тайл (нельзя пройти)
  • 1прозрачный тайл (можно пройти)

Используя координаты игрока, можно узнать тайл, на котором стоит игрок. Делается это так.

u16 x_tile = x_pos / 8; //так
u16 x_tile = x_pos >> 3; //или так

1-я и вторая строка идентичны, но 2-я предпочтительней, т.к. работает быстрее. Позиция игрока делится на 8, потому-что тайл 8×8 px.

Зная координаты тайла, можно подставить их в массив коллизий (level), и узнать, твердый ли, тайл на котором стоит игрок, или нет. Делается это по такому шаблону.

level[y_tile][x_tile]

x_tile, y_tile — тайл на котором стоит игрок.

Т.е. если level[y_tile][x_tile] вернет 0, то нельзя пройти, если 1, то можно.

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

Генерируем массив коллизий.

Сперва, скачайте скрипт с MEGA.

И перетащите картинку с картой на col_generator.exe, скрипт сам сгенерирует нужные файлы.

Альтернативный способ. Запустите col_generator.exe через консоль, со следующими аргументами.

col_generator.exe изображение [цвет_фона_hex]
  • изображение — имя изображения, с расширением.
  • цвет_фона_hex — цвет фона в hex формате. Его можно не указывать, тогда будет использоваться первый цвет в палитре. Если тайл содержит ничего, кроме цвет_фона, тогда скрипт понимает, что тайл прозрачный. В противном случае — твердый.

Поместите скрипт в папку с изображением (за пределами проекта), затем, запустите скрипт со следующими аргументами.

col_generator.exe bg.png #005500

Изображение bg.png, находится в папке res проекта.

После запуска, скрипт сгенерирует 2 картинки и текстовый файл.

На картинке bg_WithCollision.png, твердые тайлы отмечены красным.

В файле bg_array.txt хранится массив коллизий, и размер карты в пикселях, который, в будущем, понадобится для ограничения камеры.

Массив коллизий получили, теперь разберем код платформера.

Ресурсы платформера.

В папке res, находится спрайт игрока, и карта мира.

Спрайт игрока(кружки) я сделал максимально квадратным, что-бы проще было увидеть область столкновений.

  • resources.res — мы разбирали в предыдущих статьях.
  • resources.h — создается автоматически, во время компиляции.

Код платформера.

Откройте main.c

Нас приветствует огромный массив коллизий, сгенерированный ранее.

Далее, идут структуры, в которых хранятся переменные игрока.

typedef struct {
	f32 x;
	f32 y;
} Point;

typedef struct {
	bool moving;
	bool is_on_floor;
	Point pos;
	Point spd;
} Player;

Т.е хранится:

  • двигается ли игрок
  • на земле ли игрок
  • позиция игрока
  • его скорость

Структуры разбирались в SGDK. Двигаем много спрайтов.

В функции main, заполняем переменные игрока — значениями.

plr.moving = FALSE;
plr.is_on_floor = FALSE;
plr.pos.x = FIX32(160);
plr.pos.y = FIX32(950);
plr.spd.x = 0;
plr.spd.y = 0;

Далее

SPR_init();
VDP_setPalette(PAL3, spr_cup.palette->data);
setCameraPosition(fix32ToInt(plr.pos.x),fix32ToInt(plr.pos.y));
setCameraPosition(fix32ToInt(plr.pos.x)-1,fix32ToInt(plr.pos.y)-1);

cup_obj = SPR_addSprite(&spr_cup, fix32ToInt(plr.pos.x), fix32ToInt(plr.pos.y), TILE_ATTR(PAL3, 0, FALSE, FALSE));

SPR_update();
  • Иницализируем спрайтовый движок SPR_init
  • Дважды вызываем setCameraPosition, что-бы получить картинку в первый кадр.
  • Добавляем спрайт на экран SPR_addSprite.
  • Делаем его видимым SPR_update

Установка позиции спрайта и скролл карты, осуществляется в setCameraPosition.

void setCameraPosition(s16 x, s16 y)
{
    if ((x-160 != camPosX) || (y-120 != camPosY))
    {
        camPosX = x-160;
        camPosY = y-120;
		
	if (camPosX < 0){
		camPosX = 0;
	} else if (camPosX > MAX_X-320) {
		camPosX = MAX_X-320;
	}		
	if (camPosY < 0){
		camPosY = 0;
	} else if (camPosY > MAX_Y-120) {
		camPosY = MAX_Y-120;
	}	
	SPR_setPosition(cup_obj, x-camPosX, y-camPosY);
        MAP_scrollTo(bga, camPosX, camPosY);
    }
}

Разберем данную функцию

camPosX = x-160;
camPosY = y-120;

Позиция камеры смещена в минус относительно координат игрока, таким образом, игрок находится в центре.

if (camPosX < 0){
	camPosX = 0;
} else if (camPosX > MAX_X-320) {
	camPosX = MAX_X-320;
}		
if (camPosY < 0){
	camPosY = 0;
} else if (camPosY > MAX_Y-120) {
	camPosY = MAX_Y-120;
}	

Ограничиваем камеру в рамках карты.

SPR_setPosition(cup_obj, x-camPosX, y-camPosY);
MAP_scrollTo(bga, camPosX, camPosY);

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

Перейдем к проверке коллизии.

Разбираем код коллизии.

bool checkCollision(s16 x, s16 y)
{
	plr.is_on_floor = FALSE;
	
	s16 y_tile = y >> 3;
	s16 x_tile = x >> 3;
	
	u8 player_width = 4;
	u8 player_height = 4;
	
	s16 leftTile = x_tile;
	s16 rightTile = x_tile+player_width;
	s16 topTile = y_tile;
	s16 bottomTile = y_tile+player_height;
	
	for(s16 i=leftTile; i<=rightTile; i++)
	{
		for(s16 j=topTile; j<=bottomTile; j++)
		{
			if(level[j][i] == 0) {
				if(j == bottomTile){
					plr.is_on_floor = TRUE;
				}
				return FALSE;
			}
		}
	}
	return TRUE;
}
  • Здесь, мы соотносим тайл, на котором стоит игрок (y_tile, x_tile) и т.к. игрок занимает не и 1 тайл, а 4.
  • Мы проверяем каждый тайл, который занимает игрок, в цикле, и если тайл твердый (значение level[y][x] == 0), то возвращаем FALSE (пройти нельзя).
  • Если тайл, с которым сталкиваемся (level[j][i] == 0), находится под игроком (j == bottomTile), значит, игрок находится на земле (plr.is_on_floor = TRUE).
  • В остальный случаях, игрок может пройти.

Теперь, разберем moveEntity, именно эта функция отвечает за движение игрока.

Если бы, игрок перемещался со скростью, кратной размерам тайла (1, 2, 4, 8). И находился бы, строго на координатах тайла. Тогда, можно было-бы сократить код moveEntity, до:

void moveEntity(){
	s16 posX = fix32ToInt(plr.pos.x);
	s16 posY = fix32ToInt(plr.pos.y);

	s16 spdX = fix32ToInt(plr.spd.x);
	s16 spdY = fix32ToInt(plr.spd.y)+2; //Ранее здесь не было +2, и игрок мог застрять на уступе, как ни странно, костыль подобранный на угад, спас ситуацию.

	if(checkCollision(posX+spdX, posY+spdY)) {
		posX += spdX;
		posY += spdY;
	}
	plr.pos.x = FIX32(posX);
	plr.pos.y = FIX32(posY);
}

Т.е. если на будующей позиции игрока (posX+spdX) нет коллизии, тогда перемещаемся на будующую позицию, в противном случае — стоим на месте.

Но, такой вариант, не подходит для платформеров.

На помощь приходит огроменный блок else. Который, в основном, состоит из копипасты.

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

Проверяем путь игрока на столкновение.

Разберем код else.

if (spdX) {
  testPosX = posX;
  if (spdX > 0) {
    for(u8 i=1;i<spdX;i++){
      testPosX++;
      if(checkCollision(testPosX, posY)) {
      posX = testPosX;
          } else {
            break;
          }  
        }
      } else {
        for(u8 i=spdX;i>0;i++){
          testPosX--;
          if(checkCollision(testPosX, posY)) {
            posX = testPosX;
          } else {
            break;
          }  
        }
      }
    }

Если игрок двигается влево или вправо if (spdX), то проверяем позиции игрока, начиная от posX, заканчивая spdX.

for(u8 i=1;i<spdX;i++){
      testPosX++;
      if(checkCollision(testPosX, posY)) {
      posX = testPosX;
          } else {
            break;
          }  
        }

Ровно до того момента, пока координата игрока не столкнется с коллизией. Финальная координата игрока, хранится в testPosX, testPosY. Она приравнивается к позиции игрока.

plr.pos.x = FIX32(posX);
plr.pos.y = FIX32(posY);

В конце, меняем координаты игрока.

Разбираем управление.

Управление задается в handleInput:

void handleInput()
{
    u16 value = JOY_readJoypad(JOY_1);
    if (!paused && !plr.moving)
    {
		
		plr.spd.x = 0;
		
		if(!plr.is_on_floor){
			plr.spd.y += FIX32(1);
		} else {
			plr.spd.y = 0;
		}
		
        if (value & BUTTON_RIGHT)
        {
			plr.spd.x = FIX32(5);
		}
        else if (value & BUTTON_LEFT)
        {
			plr.spd.x = FIX32(-5);
		}
        if (value & BUTTON_UP)
        {
			if(plr.is_on_floor){
				plr.spd.y = FIX32(-10);
			}
        }
    }
}
  • Если игрок не на полу, то применяем гравитацию, иначе — отключаем.
  • Меняем скорость игрока, при нажатии на кнопок вправо/влево
  • Прыгаем, если нажата кнопка вверх и игрок находится на земле.

Последнее что осталось, это вызвать все эти фукнции в главном цикле.

while(1)
{
	SPR_update();
	handleInput();
	moveEntity();
		
	VDP_showFPS(FALSE);
	setCameraPosition(fix32ToInt(plr.pos.x),fix32ToInt(plr.pos.y));
        
	SYS_doVBlankProcess();
}

Вроде обо всем рассказал.

Заключение.

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

(Бонус) Правильный код коллизии.

Вместо того, чтобы проверять весь путь от posX до posX+spdX. Намного проще, узнать позицию тайла (в пикселях) с которым ты сталкиваешься, и вычесть из неё координату игрока.

pos_x += (coll_tile_x*8) - posX; //прожорливый вариант
pos_x += (coll_tile_x<<3) - posX; //эффективный вариант

Пока, мне не удлось реализовать такую коллизию. Если разберусь, обновлю статью.

Отдельная благодарность.

Передаю благодарность:

  • Sharpnull — из форума emu-land, за объясение работы коллизии.
  • Diamond Softhouse — тоже помог, в изучении данного вопроса.

Итоговый результат.