GameMaker Studio 2. Урок 3. Камера. Разрешение экрана
Привет!
Это третья часть гайда, где мы будем делать:
- Камеру с зумом, которая будет следовать за игроком, посредством настроек и кода.
- Изменение разрешения экрана и переключение режимов экрана
- Сделаем ещё пару скриптов для управления перемещением игрока. Какой из них оставить - решать вам.
Скажу сразу, что будет много кода.
Также немного затронем тему следующего гайда - глобальные переменные.
Также обозначу, что у объекта oPlayer можно выключить свойство Persistent.
Ссылка на первый гайд
Ссылка на второй гайд
P.S.
За спрайты огромное спасибо моему другу
https://vk.com/parijer
Ссылка на мою группу ВК
https://vk.com/club209675869
Камера.
Включение камеры.
Перейдём в комнату с игроком.
Во вкладке слева-снизу (Properties - Room1) открываем вкладку "Viewports and Cameras"
Ставим галочку напротив "Enable Viewports" и "Clear Viewport Background"
Немного теории.
Камера - объект, который содержит информацию о том, что камера видит и что нужно отображать на экране. При этом, камера имеет два параметра:
View (Вид) - то, что камера видит, основываясь на позиции, проекции и повороте камеры.
View Port - область дисплея, на которой будет отображаться то, что камера видит.
Первый способ создания камеры. Простой.
1. Открываем "Viewport 0"
2. Ставим галочку напротив "Visible" для себя.
3. В "Camera Properties" выставляем нужные нам настройки камеры, меняя параметры "Width" и "Height". Поиграйтесь сами, посмотрите, как меняется область камеры и выберите нужные себе значения.
4. Чуть ниже есть надпись "Object Following". Здесь выбираем объект, за которым камера будет следовать. Выбираем игрока.
5. По сути, всё. Камера готова. Но, как вы можете заметить, игрок её двигает только тогда, когда подойдёт к границе. Чтобы это исправить, в "Horizontal Border" и "Vertical Border" выставьте те же параметры, что выставляли в "Camera Properties".
6. Изменяя "Horizontal Speed" и "Vertical Speed" вы сможете настроить "плавность" слежения камеры.
Всё. Камера готова и будет следовать за персонажем - игроком.
Второй способ создания камеры. Сложный.
Сложный он по той причине, что делать мы это всё будем непосредственно через код. :)
Для начала - просто включите Viewports и Clear Viewport BackgroundПриступим
1. Создаём объект oCamera.
2. Теория.
2. Сделаем нашу комнату с главным меню (rmMainMenu) "Persistent" - постоянной.
3. Перейдём в объект oGameManager. Создаём у него событие "Create". В нём пропишем код:
global.CameraSizes = [[320, 180],[640, 360], [960, 540]] // Разрешённые размеры экрана.
global.CameraNum = array_length(global.CameraSizes) - 1 // Переменная, которая будет отображать, какой из массивов мы используем, допустимые значения от 0 до (длина списка - 1)
global.CameraWidth = global.CameraSizes[global.CameraNum][0] // Ширина камеры.
global.CameraHeight = global.CameraSizes[global.CameraNum][1] // Высота камеры.
#macro CameraScale 2 // Масштаб камеры. Константа.
#macro CameraSpeed 0.1 // Скорость камеры. Константа.
window_set_fullscreen(false) // Выключаем полноэкранный режим при запуске :)
var windowWidth = global.CameraWidth * CameraScale // Ширина окна = ширина камеры * масштаб
var windowHeight = global.CameraHeight * CameraScale // Высота окна = высота камеры * масштаб
surface_resize(application_surface, global.CameraWidth * CameraScale, global.CameraHeight * CameraScale); // Переопределяем "поверхность", чтобы соотношение сторон спрайтов соответствовало нашему экрану.
window_set_size(global.CameraWidth * CameraScale, global.CameraHeight * CameraScale); // Устанавливаем размер окна (Необязательно, но желательно)
window_set_position(display_get_width() / 2 - windowWidth / 2, display_get_height() / 2 - windowHeight / 2); // Располагаем наше окно по центру дисплея
На этом шаге мы провели инициализацию настроек. Как вы можете заметить, мы использовали новый тип переменных - это глобальные и макро переменные. Их можно применять в любых частях программы, подробнее об их работе будет в следующем гайде.
Что происходит в данном коде: мы задаём базовые размеры камеры и её масштаб. Говорим, что хотим запускаться в оконном режиме, после чего устанавливаем ширину окна равную размеру камеры, помноженному на масштаб. Масштаб необходим для корректного отображения, но вообще - с этими настройками можно поиграться самому.
Любые переменные, где используется число, а не переменная, можно спокойно взять и настроить под себя.Однако, нужно не только изменить размер окна - этого недостаточно для корректного отображения. Нужно также изменить размер нашего "холста".
Gamemaker Studio 2 не отрисовывает ничего непосредственно на экран. Для отрисовки наш движок использует "холст" - surface, иначе - application_surface. Таким образом, при изменении размеров окна, нужно менять и размеры холста, как следствие - проводить подгонку UI под новые размеры.4. Перейдём к объекту oCamera. Разместим его в комнате, где есть игрок. Создадим событие "Create", где создадим саму камеру, скажем, за кем ей следить и настроим её.
Код:
// Если в комнате есть игрок - следим за ним, иначе - за комнатой.
if instance_exists(oPlayer)
{
cameraTarget = oPlayer;
}
else
{
cameraTarget = room;
}
global.Camera = camera_create_view(0, 0, global.CameraWidth, global.CameraHeight); // Создаём камеру по нужной нам ширине и высоте.
// Включаем и устанавливаем камеру.
view_enabled = true;
view_visible[0] = true;
view_set_camera(0, global.Camera);
5. Создадим step событие у камеры, где сделаем так, чтобы она плавно двигалась за игроком и не показывала то, что находится вне комнаты. Также сделаем возможность приближать и отдалять камеру на колёсико мыши - зум.
Код:
var cameraX = camera_get_view_x(global.Camera);
var cameraY = camera_get_view_y(global.Camera);
// Делаем переменные стабильными для зума
cameraWidth = camera_get_view_width(global.Camera);
cameraHeight = camera_get_view_height(global.Camera);
var targetX = cameraTarget.x - cameraWidth / 2;
var targetY = cameraTarget.y - cameraHeight / 2;
// Поддерживаем входные значения в диапазоне от 0 до размеров комнаты
targetX = clamp(targetX, 0, room_width - cameraWidth);
targetY = clamp(targetY, 0, room_height - cameraHeight);
// Возвращаем позицию камеры. Делает передвижение плавным
cameraX = lerp(cameraX, targetX, CameraSpeed);
cameraY = lerp(cameraY, targetY, CameraSpeed);
// Автоподгон размера камеры, если вдруг вышли за рамки
if cameraWidth > 1440 or cameraWidth < 320
{
cameraWidth = global.CameraWidth
cameraHeight = global.CameraHeight
}
// Зум
var wheel = mouse_wheel_down() - mouse_wheel_up(); // Возвращает true/false (-1 / 0 / 1)
if (wheel != 0)
{
wheel *= 0.1; // * 10%
// Определяем, сколько добавить к ширине / высоте
var addWidth = cameraWidth * wheel;
var addHeight = cameraHeight * wheel;
// Где 1440 и 320 - можете вписать свои значения. Это область, которую мы будем показывать.
// Не советую делать привязку к текущему экрана, т.к. тогда чем меньше будет разрешение, тем меньше будет максимальная видимая область.
// Для большого разрешения тоже будет минус - ничего не будет видно.
if (cameraWidth + addWidth < 1440) and (cameraWidth + addWidth > 320)
{
cameraWidth += addWidth;
cameraHeight += addHeight;
// Делаем входные значения стабильными
var prevWidth = cameraWidth;
var prevHeight = cameraHeight;
cameraWidth = clamp(cameraWidth, CameraWidth / 2, room_width);
cameraHeight = clamp(cameraHeight, CameraHeight / 2, room_height);
// Исправляем искривление при зуме. Если разрешение 16:9 - все ок, иначе - исправляем.
if (cameraWidth / cameraHeight == 1.777777777777778) &&
(prevWidth == cameraWidth || prevHeight == cameraHeight) {
// Фиксим позицию камеры
cameraX -= addWidth / 2;
cameraY -= addHeight / 2;
// Опять делаем значения стабильными
cameraX = clamp(cameraX, 0, room_width - cameraWidth);
cameraY = clamp(cameraY, 0, room_height - cameraHeight);
}
else {
cameraWidth = prevWidth - addWidth;
cameraHeight = prevHeight - addHeight;
}
}
}
camera_set_view_pos(global.Camera, cameraX, cameraY);
camera_set_view_size(global.Camera, cameraWidth, cameraHeight);
Всё. Камера готова, её можно отдалять-приближать и она будет следить за игроком!
Теперь сделаем меню настроек, чтобы мы могли менять наше разрешение, включать полноэкранный режим и немного перепишем интерфейс.1. Немного изменим расположение нашего меню: так как размер окна у нас может динамически изменяться, неизменяемые значения в расположении кнопок нам сделают только хуже. С-но, нам нужно перейти к объекту oMMenu и в "Create" изменить код следующим образом:
buttons = [oMStart, oMLoad, oMSettings, oMExit]
var wwidth = global.CameraWidth * CameraScale
var wheight = global.CameraHeight * CameraScale
var bwidth = wwidth * 0.25
var bheight = wheight * 0.1
for (var i = 0; i < array_length(buttons); ++i)
{
//instance_create_layer(room_width / 2, room_height / 2 - 50 + 100 * i, "Instances", buttons[i], {width : bwidth, height : bheight})
inst = instance_create_layer(wwidth / 2, wheight * 0.5 + bheight * i, "Instances", buttons[i])
inst.width = bwidth
inst.height = bheight
}
5. Так как код для отрисовки обводки кнопок у нас везде одинаковый, сделаем из него функцию, которую везде будем вызывать. Назовём её: scrOutlinedBox.
Код там будет следующий:
function scrOutlinedBox()
{
if point_in_rectangle(dmxg, dmyg, x - width / 2, y - height / 2, x + width / 2, y + height / 2)
{
draw_set_color(c_white)
draw_set_alpha(0.15)
draw_rectangle(x - width / 2, y - height / 2, x + width / 2, y + height / 2, false)
draw_set_alpha(1)
}
}
dmxg и dmyg мы не объявляем, так как мы их уже используем в нашем коде, это важно. По сути, это просто сокращение кода.
Не забудьте добавить отрисовку спрайта через draw_sprite_streched() :)
Код для текста кнопки выбора размеров:
scrOutlinedText(x, y, c_white, c_black, "Разрешение экрана: " + string(global.CameraSizes[global.CameraNum][0] * CameraScale) + "x" + string(global.CameraSizes[global.CameraNum][1] * CameraScale), depth, fontArialRusSmall, fa_center, fa_middle)
Код для кнопки смены режима экрана:
var fullscreen = window_get_fullscreen()
if fullscreen == 0
{
fullscreen = "Выкл."
}
else
{
fullscreen = "Вкл."
}
scrOutlinedText(x, y, c_white, c_black, "Полноэкранный режим: " + fullscreen, depth, fontArialRusSmall, fa_center, fa_middle)
Теперь немного настроим логику.
step у oSFullscreen:
dmxg = device_mouse_x_to_gui(0)
dmyg = device_mouse_y_to_gui(0)
if point_in_rectangle(dmxg, dmyg, x - width / 2, y - height / 2, x + width / 2, y + height / 2)
and mouse_check_button_pressed(mb_left)
{
window_set_fullscreen(!window_get_fullscreen())
}
step у oSBack
dmxg = device_mouse_x_to_gui(0)
dmyg = device_mouse_y_to_gui(0)
if point_in_rectangle(dmxg, dmyg, x - width / 2, y - height / 2, x + width / 2, y + height / 2)
and mouse_check_button_pressed(mb_left)
{
room_goto(rmMainMenu)
}
step у oSSize
dmxg = device_mouse_x_to_gui(0)
dmyg = device_mouse_y_to_gui(0)
if point_in_rectangle(dmxg, dmyg, x - width / 2, y - height / 2, x + width / 2, y + height / 2)
and mouse_check_button_pressed(mb_left)
{
if global.CameraNum < array_length(global.CameraSizes) - 1
{
global.CameraNum += 1
}
else
{
global.CameraNum = 0
}
global.CameraWidth = global.CameraSizes[global.CameraNum][0]
global.CameraHeight = global.CameraSizes[global.CameraNum][1]
surface_resize(application_surface, global.CameraWidth * CameraScale,global.CameraHeight * CameraScale) // Пересобираем surface
window_set_size(global.CameraWidth * CameraScale,global.CameraHeight * CameraScale);
oSSettings.surface_changed = true;
}
С-но, что мы здесь делаем:
1) Меняем размер окна
2) Меняем размер холста
3) Так как размер холста изменился, нам нужно изменить расположение всех элементов GUI, которые мы располагали до этого (они же ставились под старые размеры разрешения). Поэтому мы обращаемся к oSSettings и говорим, что наш холст изменился и устанавливаем новые значения.
4) Чтобы oSSettings это поняла, в Create нам нужно добавить surface_changed = false.
Здесь же создадим пустой список: all_buttons = []
Затем добавить в цикл for, в конец, строку:
array_push(all_buttons, inst)
Таким образом, мы запоминаем ID тех кнопок, что мы уже поставили.
Затем в step написать следующий код:
if surface_changed
{
var windowWidth = global.CameraWidth * CameraScale
var windowHeight = global.CameraHeight * CameraScale
var bwidth = windowWidth * 0.4
var bheight = windowHeight * 0.1
for (var i = 0; i < array_length(all_buttons); ++i)
{
all_buttons[i].x = windowWidth / 2
all_buttons[i].y = windowHeight / 2 + (windowHeight * 0.1) * i
all_buttons[i].width = bwidth
all_buttons[i].height = bheight
}
surface_changed = false
}
5) Расположение интерфейса нам нужно поменять и в другом меню - oMMenu.
Здесь нужно будет добавить два ивента: Room Start и Room End, а в Create добавить две переменные:
old_size = global.CameraWidth * CameraScale
new_size = global.CameraWidth * CameraScale
Теперь, что мы делаем.
Когда происходит событие "конец комнаты" (смена комнаты) - мы запоминаем текущий размер экрана.
Когда происходит событие "начало комнаты" - мы сравниваем старый размер с новым и если они разные - меняем расположение всех кнопок.
Код Room End
old_size = global.CameraWidth * CameraScale
Код Room Start
new_size = global.CameraWidth * CameraScale
if new_size != old_size
{
var windowWidth = global.CameraWidth * CameraScale
var windowHeight = global.CameraHeight * CameraScale
var bwidth = windowWidth * 0.25
var bheight = windowHeight * 0.1
for (var i = 0; i < array_length(all_buttons); ++i)
{
all_buttons[i].x = windowWidth / 2
all_buttons[i].y = windowHeight * 0.5 + bheight * i
all_buttons[i].width = bwidth
all_buttons[i].height = bheight
}
}
Подобный код нам теперь придётся писать везде, где есть GUI, так как наши элементы интерфейса мы закрепляем лишь единожды, а здесь - располагаем их заново.
Осталось подправить одну недоработку в коде у камеры, а именно - Зум.
В step у камеры, перед зумом добавим проверку, которая будет принудительно изменять размер камеры допустимый, если мы выходим за границы.
if cameraWidth > global.CameraWidth * 1.5 or cameraWidth < global.CameraWidth * 0.5
{
cameraWidth = global.CameraWidth
cameraHeight = global.CameraHeight
}
Теперь немного про скрипты.
Подправим передвижение. Сделаем его немного удобнее для нашей игры.
Сделаем следующее: если игрок подойдёт к краю комнаты и продолжит идти, то окажется на противоположной её стороне.
Для этого в "step" событии напишем следующий код после вызова функции для передвижения:
move_wrap(true, true, sprite_width);
Если наш объект зайдёт за границы экрана на ширину своего спрайта - мы его развернём. Используем только ширину, т.к. грани спрайта у нас одинаковые. Если бы ширина и высота разнились, пришлось бы писать этот код дважды: для ширины и для высоты соответственно, причём вместо "true" выставляя "false" в нужных местах соответственно.
Чтобы почитать о функции подробно - наведитесь на неё и нажмите на колёсико мыши.
Сделаем возможность передвижения камеры с помощью мыши: нажав ПКМ, мы перетаскиваем камеру туда, куда нам нужно.
Так как камера у нас привязана к игроку, передвигать мы будем именно игрока.
Создадим скрипт, который назовём: "scrMoveToMouse"
Код, который в нём будет написан, будет проверять, нажата ли ПКМ и если да - передвигаться игрока к координатам мыши в комнате, делая это плавно.
function scrMoveToMouse()
{
// Перемещение игрока к мыши (и камеры к игроку как следствие)
if mouse_check_button(mb_right)
{
x = lerp(x, mouse_x, 0.05)
y = lerp(y, mouse_y, 0.05)
}
}
Всё. Достаточно вызвать функцию. Теперь, нажимая ПКМ, мы будем двигаться в нужную нам точку.
Похожая функция, но для передвижения в определённую сторону в том случае, если мышь находится у границ экрана.
Создадим ещё один скрипт и назовём его: "scrMoveToEdge"
Нам нужно проверять позицию мыши относительно GUI, после чего проверять X и Y:
- Находится ли мышь у левой или правой границы. Для этого возьмём 10% от текущего разрешения за основу. Соответственно, в одном случае - умножим итоговую ширину экрана на 0.1 - если мышь слева и на 0.9 - если справа.
- Аналогичные действия для верха и низа, но с высотой экрана.
Код:
function scrMoveToEdge()
{
var dmxg = device_mouse_x_to_gui(0)
var dmyg = device_mouse_y_to_gui(0)
if dmxg < (CameraScale * CameraWidth) * 0.1
{
x -= player_speed
}
else if dmxg > (CameraScale * CameraWidth) * 0.9
{
x += player_speed
}
if dmyg < (CameraScale * CameraHeight) * 0.1
{
y -= player_speed
}
else if dmyg > (CameraScale * CameraHeight) * 0.9
{
y += player_speed
}
}
Как вы могли заметить, у нас здесь стоит две связи: if else if. Сделано это для того, чтобы игрок мог передвигаться по диагонали, наводясь в углы.
Чтобы не происходило наслоение друг на друга, сделаем так, чтобы каждая функция возвращала true в случае использования и false, если нет.
Код.
scrMove
function scrMove(spd)
{
var key_up = keyboard_check(ord("W")) or keyboard_check(vk_up);
var key_left = keyboard_check(ord("A")) or keyboard_check(vk_left);
var key_right = keyboard_check(ord("D")) or keyboard_check(vk_right);
var key_down = keyboard_check(ord("S")) or keyboard_check(vk_down);
var movement_dir = point_direction(0, 0, key_right - key_left, key_down - key_up);
var movement_input = (key_right - key_left != 0) or (key_down - key_up != 0);
if (movement_input)
{
var h_speed = lengthdir_x(spd, movement_dir);
var v_speed = lengthdir_y(spd, movement_dir);
x += h_speed;
y += v_speed
return true
}
else
{
return false
}
}
scrMoveToEdge
function scrMoveToEdge(pspeed)
{
var dmxg = device_mouse_x_to_gui(0)
var dmyg = device_mouse_y_to_gui(0)
if dmxg > (CameraScale * global.CameraWidth) * 0.1
and dmxg < (CameraScale * global.CameraWidth) * 0.9
and dmyg > (CameraScale * global.CameraHeight) * 0.1
and dmyg < (CameraScale * global.CameraHeight) * 0.9
{
return false
}
if dmxg < (CameraScale * global.CameraWidth) * 0.1
{
x -= pspeed
}
else if dmxg > (CameraScale * global.CameraWidth) * 0.9
{
x += pspeed
}
if dmyg < (CameraScale * global.CameraHeight) * 0.1
{
y -= pspeed
}
else if dmyg > (CameraScale * global.CameraHeight) * 0.9
{
y += pspeed
}
return true
}
scrMoveToMouse
function scrMoveToMouse()
{
// Перемещение игрока к мыши (и камеры к игроку как следствие)
if mouse_check_button(mb_right)
{
x = lerp(x, mouse_x, 0.05)
y = lerp(y, mouse_y, 0.05)
return true
}
else
{
return false
}
}
Осталось настроить вызов функций.
Функции выполняются в любом случае. И в любом случае возвращают какой-то результат. Если мы пропишем функцию в проверку - она будет выполняться и возвращать результат. Но, так как мы возвращаем и true, и false, нам, соответственно, нужно сделать проверку на false:
Если мы не двигаемся на клавиши -> Если мы не двигаемся к мыши -> Если мы не двигаемся к краю.
Таким образом, если мы двигаемся на клавиши - мы не сможем двигаться к мыши и к краю.
Если мы двигаемся к мыши - мы не сможем двигаться к краю.
Код в Step у oPlayer:
if !(scrMove(player_speed))
{
if !(scrMoveToMouse())
{
if !(scrMoveToEdge(player_speed))
{
}
}
}
move_wrap(true, true, sprite_width); // Разворот
Реализуем это именно через лесенку, так как нам нужно, чтобы одновременно выполнялся только один скрипт.
Как растянуть один спрайт на весь фон
1. Выбираем наш фон в "Layers - Room1"
2. Выбираем нужный спрайт.
3. Ставим галочки:
3.1. Horizontal Tile
3.2. Vertical Tile
На этом мы подходим к концу.
Приношу свои извинения, если всё выглядит рвано или слишком мало объяснений. В процессе всё несколько раз переписывалось и могли остаться шероховатости.
В следующем гайде будет подробно разобрана иерархия объектов, виды переменных (локальные, глобальные, обычные, макро), как их используют и для чего.
В этом же гайде я сделал чуточку больше, чем хотел изначально и надеюсь, что вам это будет на пользу.
Видео с тем, как это всё выглядит по факту.
Насчёт кнопок: как бы вы их реализовали?
Также, через Nine Slice и отрисовку текста отдельно или сделав готовые кнопки с текстом?
Я склоняюсь ко второму варианту, но для тестирования предпочту использовать первый :)
Оставшиеся гайды:
- Иерархия объектов. «Объекты-родители» и их «дети». Решение часто встречающихся проблем и немного про то, как удобно выстраивать взаимодействие с объектами. Глобальные переменные.
- Массивы и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке. Включая объяснение, в каких случаях лучше использовать встроенные функции, в каких – писать свои с нуля.
- Пути. Один большой гайд, включая скрипты поиска путей для различных сеток в т.ч. с примерами из моего личного проекта.
- Иные способы хранения информации в GMS2, когда их стоит или не стоит использовать.
- Сохранение. Встроенное VS самописное.
- Звуки.
Ссылка на файл, чтобы всё можно было потыкать самому:
https://disk.yandex.ru/d/A3TRUz39ByvjOQ
Есть вопрос - задавай, постараюсь ответить.
Что-то не получилось, вылезла ошибка и так далее - тоже пиши, помогу или поможем.
Ну и я всегда рад критике.