Autotiling: автоматичні переходи тайлів

Буквально щойно натрапив на статтю з пісочниці про grid-tiling'е і вирішив написати свій аналог.
Мій метод розподілу переходів дещо відрізняється від згаданої в тій статті.
Початок даної системи покладено в відомої грі WarCraft III .
 
 image
 
На цьому скріншоті редактора карт WC3 можна помітити шви, вони позначені червоними стрілками. Судячи з усього, логіка тайлів в цій грі дещо інша, ніж в більшості ігор. Один тайл тут не займає цілої клітини . Він перебуває ніби в точці, навколо якої вже малюються його кути.
 
Особливо добре це спостерігається з включеною сіткою.
 
 image
 
Зазвичай у такій ситуації пропонується розділити тайл на 4 маленьких. Але є одне але: що робити в подібному випадку?
 
 image
 
Коли всі 4 навколишні один квад тайли різні? Тут явно видно, що більшу частину займає самий нижній тайл.
Зваживши всі за і проти, я прийшов до своєї, досить специфічною, системі. Додамо нову сітку, сітку переходів. У ній ми можемо зберігати, наприклад, тип int. У такому разі у нас буде можливо записати для кожного квада тайлів 16 ID оточуючих 4 тайлів з 16 варіантами переходу. Цього більш ніж достатньо. Ні, якщо комусь потрібно більше ID — будь ласка, використовуйте long. Я вирішив, що мені вистачить по 16 автотайлов на ігрову локацію, інші будуть без авто-переходів.
 
Далі, нам потрібен сет тайлів. Можна, звичайно, використовувати маску, але з сетом тайлів, погодьтеся, при хорошому навичці (не в мене, немає), можна домогтися дуже і дуже непоганий картинки.
 
 image
 
Собі я зробив ось такий тестовий набір тайлів. На один тайл припадає 12 варіантів переходу, можна додати ще свої 4. Ще я зарезервував слоти для майбутньої варіації тайлів, як в WC3, але ця частина досить легка і описувати тут я її не буду.
 
Переходимо до частини програмування. Для початку, опишемо функції, які визначатимуть потрібну бітову маску для вибору коректного індексу текстури. Відразу обмовлюся, я властивий вибирати досить нестандартні рішення. Тут буде використовуватися Java + LWJGL.
 
Ця функція буде створювати маску бітів для даного квада. Біт 1 означає, що в даному розі тайла є суміжний йому тайл (таким чином, можна комбінувати різні тайлсети однієї висоти). Ах, так. Висота, про неї-то я і забув. Звичайно, нам треба буде визначати для кожного тайла його висоту, щоб знати що малювати поверх, а що внизу. Це вирішується просто додаванням очевидною змінної.
 
 
public int getTransitionCornerFor(World world, int x, int y, int height) {
		int corner = 0;
		if (world.getTile(x-1, y).zOrder == height)
			corner |= 0b0001;
		if (world.getTile(x, y).zOrder == height)
			corner |= 0b0010;
		if (world.getTile(x, y-1).zOrder == height)
			corner |= 0b0100;
		if (world.getTile(x-1, y-1).zOrder == height)
			corner |= 0b1000;
		return corner;
	}

 
Кожен біт означає свій куток. 1 біт — лівий верхній кут, 2 — нижній лівий, 3 — нижній правий, ну і 4 — верхній правий.
 
Тепер, касаемо, самого методу визначення потрібних індексів текстури для переходів. Метод у мене вийшов громіздкий і некрасивий, ну, все в силу моїх навичок. Хоча спеціально для статті я розбив його на кілька методів, щоб не створювати величезну кількість відступів.
 
 
public void updateTransitionMap(World world, int x, int y) {
		int w = 16, h = 16;
		int[] temp = new int[4];    //создаем массив, который будет хранить нам 4 угла с 4 битами под ID и 4 битами под переход (т.е. 32 бита в целом для всего тайла)
		for (int i = 0; i < 4; i++)    //на самом деле мне просто было лень нормально разбираться с побитовыми операциями
			temp[i] = 0; 
		
		if (tileID > 0) {
			for (int i = 1; i <= tilesNum; i++) {
				int corner = getTransitionCornerFor(world, x, y, i);
				int c = 0;
				if (corner > 0) {
					c = setPointTransition(world, temp, x, y, corner, c, i);  //сначала задаем маску для всех углов
					if (c == 3) 
						c = setCornerTransition(world, temp, x, y, corner, c, i);  //потом, если есть 3 смежных(!) угла, соединяем их в один большой
					if (c == 2) 
						c = setEdgeTransition(world, temp, x, y, corner, c, i); //если есть 2 смежных(!) угла, соединяем их в сторону
				}	
			}
		}
	}

 
А ось і самі методи:
 
 
public int setPointTransition(World world, int[] temp, int x, int y, int corner, int c, int i) {
		for (int k = 0; k < 4; k++)
			if ((corner >> k & 1) == 1) {
				int idx = 8+k;
				int storage = 0;
				storage = (idx & 0xF) << 4 | (i & 0xF);
				temp[k] = storage;
				int t = 0;
				for (int l = 0; l < 4; l++) {
					t = (t << 8) | temp[l] & 0xFF;
				}
				world.setTransition(x, y, t);
				c++;
			}
		
		return c;
	}

 
Тут все просто. Пробігаю по кожному куті, перевіряємо біт. Якщо він один — ставимо індекс 8 + k , тобто кут (вище я описував номер для кожної сторони (NE, SE, SW, SE)). Далі костильного методом через цикл оновлюємо нашу карту переходів.
 
Не забуваємо наприкінці віддавати оновлене число с. Спасибі Java , що в ній немає ні out , ні передачі найпростіших типів за посиланням.
 
Методи, що з'єднують точки в кути і сторони:
 
public int setEdgeTransition(World world, int[] temp, int x, int y, int corner, int c, int i) {
		for (int offset = 0; offset < 4; offset++) {
			boolean isSide = true;
			for (int k = 0; k < 2; k++) { //количество точек у стороны
				if ((corner >> ((k + offset) % 4) & 1) != 1)
					isSide = false;
				else if (k == 1 && isSide)  {
					int idx = (offset+1)%4;
					int storage = 0;
					storage = (idx & 0xF) << 4 | (i & 0xF);
					temp[offset] = storage;
					int t = 0;
					for (int l = 0; l < 4; l++) {
						t = (t << 8) | temp[l] & 0xFF;
					}
					world.setTransition(x, y, t);
				}
			}
		}
		
		return c;
	}
	
	public int setCornerTransition(World world, int[] temp, int x, int y, int corner, int c, int i) {
		for (int offset = 0; offset < 4; offset++) {
			boolean isCorner = true;
			for (int k = 0; k < 3; k++) { //количество точек у угла
				if ((corner >> ((k + offset) % 4) & 1) != 1)
					isCorner = false;
				else if (k == 2 && isCorner)  {
					int idx = 4+offset;
					int storage = 0;
					storage = (idx & 0xF) << 4 | (i & 0xF);
					temp[offset] = storage;
					int t = 0;
					for (int l = 0; l < 4; l++) {
						t = (t << 8) | temp[l] & 0xFF;
					}
					world.setTransition(x, y, t);
				}
			}
		}
		
		return c;
	}

 
Тут абсолютно такий же принцип. Єдина відмінність — стартовий номер індексу текстури, щоб нам взяти потрібний і ще один цикл, який задає зсув, що означає з якої точки стартувати кут. Перевіряється суміжний кут (або сторона) проти годинникової стрілки, що починається з даної точки. Якщо хоч одна точка не є суміжним Тайл — переривається, ні кутка, ні сторони не виходить.
От і все, карта переходів у нас побудована! На кожен тайл доводиться по 5 біт. Один для зберігання тайла (256 можливих варіацій) і по біту на кожен кут для зберігання метаданих.
 
Залишилося тільки отрендеріть цю справу. Я буду розглядати старовинний deprecated-метод через immediate-mode (планую піти на VBO, зараз трошки треба розібратися зі структурою та динамічним апдейтом VBO, а також отрисовкой лише видимою його частини).
 
Ну, тут немає нічого складного:
 
 
public void renderTile(World world, int x, int y) {
		int w = 16, h = 16;
		int s = 0;

		if (tileID > 0) {
			for (int i = 0; i < 4; i++) {
				int t = world.getTransition(x, y);
				int src = ((t >> (3-i)*8) & 0xFF);
				int idx = src >> 4 & 0xF;
				int id = src & 0xF;
				int u = (idx%8)*16, v = 16 + 16*(idx/8) + (id-1)*48,
				u1 = u + w, v1 = v + h;
				if (id != 0) {
					GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, u, v, u1, v1); //не обращайте внимания на хардкод, всё равно будет переписан под VBO 
				}
			}
		}
	}

 
Що ми робимо тут? Ага, проходимся по кожним 8 бітам і дістаємо 4 перших і 4 останніх, для ID і переходу. Далі передаємо параметри OpenGL, він вже розподіляє отрисовку.
 
Результат:
 
 image
(Так-так, LWJGL-кинувся, вбудований в Swing).
 
Здається, ми щось забули? Малювати цілісний шматок тайла, якщо 4 навколишні точки йому рідні по висоті!
 
 
public void renderTile(World world, int x, int y) {
	int w = 16, h = 16;
		
	int s = 0;
	if (tileID > 0) {
		int c = 0;
		for (int i = 0; i < 4; i++) {
			int t = world.getTransition(x, y);
			int src = ((t >> (3-i)*8) & 0xFF);
			int idx = src >> 4 & 0xF;
			int id = src & 0xF;
			int u = (idx%8)*16, v = 16 + 16*(idx/8) + (id-1)*48,
			u1 = u + w, v1 = v + h;
			if (id != 0) {
				if (id == tileID)
					c++;
				GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, u, v, u1, v1);
			}
		}
			
		if (c == 4) {
			GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, 0, 48*(tileID-1), 16, (tileID-1)*48+16);
		}
	}
}

 
 image
 
Чогось не вистачає? Вірно, нам треба вирішити як малювати нижній тайл. Якщо чесно, у мене це вийшло вирішити майже випадково, але саме цей момент ще потребує доопрацювання. Поки це можна вважати прикрученим милицею, але на результат він не впливає.
 
Трошки змінимо наш метод:
 
 
public void renderTile(World world, int x, int y) {
		int w = 16, h = 16;
		
		int s = 0;
		if (tileID > 0) {
			for (int i = 1; i <= tilesNum; i++) {
				int corner = getTransitionCornerFor(world, x, y, i);
				int c = 0;
				if (corner > 0) {
					for (int k = 0; k < 4; k++)
						if ((corner >> k & 1) == 1) {
							c++;
						}
				}
				
				boolean flag = false;
				int fill = getFillCornerFor(world, x, y, i);
				if (fill > 0)
					for (int k = 0; k < 4; k++)
						if ((fill >> k & 1) == 1) {
							c++;
							if (k == 4 && c == 4)
								flag = true;
						}
				
				if (c == 4) {
					GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, 0, 48*(i-1), 16, (i-1)*48+16);
					if (flag)
						break;
				}
			}
			
			for (int i = 0; i < 4; i++) {
				int t = world.getTransition(x, y);
				int src = ((t >> (3-i)*8) & 0xFF);
				int idx = src >> 4 & 0xF;
				int id = src & 0xF;
				int u = (idx%8)*16, v = 16 + 16*(idx/8) + (id-1)*48,
				u1 = u + w, v1 = v + h;
				if (id != 0) {
					GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, u, v, u1, v1);
				}
			}
		}
	}

 
Додався ще один метод. Він майже еквівалентний методу, який пише біти суміжних тайлів. Ось він:
 
 
public int getFillCornerFor(World world, int x, int y, int height) {
		int corner = 0;
		if (world.getTile(x-1, y).zOrder > height)
			corner |= 0b0001;
		if (world.getTile(x, y).zOrder > height)
			corner |= 0b0010;
		if (world.getTile(x, y-1).zOrder > height)
			corner |= 0b0100;
		if (world.getTile(x-1, y-1).zOrder > height)
			corner |= 0b1000;
		return corner;
	}

 
Він визначає, все тайли в окрузі, висота яких більше висота переданого тайла.
 
Тобто ми перебираємо всі тайли для даної клітини (природно, перебирати варто лише автотайли) і дивимося скільки тайлів знаходяться вище даного. Не забуваємо, що перед цим ми вважаємо кількість точок, покритих даними Тайл. Якщо кількість точок даного тайла + сума точок інших тайлів перекривають даний == 4, то ми малюємо повний квад з даною текстурки і перериваємо цикл. Ось такі милиці.
 
Результат відмінний:
 
 image
 
Мабуть, на цьому все.
 
P.S. Чим цей спосіб краще того? Ну, WC3 наочно демонструє, що з такою системою можна добитися ландшафту неймовірною краси. Особисто мені здається, що вона більш гнучка, що, правда, створює деякі складності її реалізації. І так, вона все ж вимагає деякої, як я сказав вище, доопрацювання.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.