Титан defold-orbit-camera

Пойман титан:

Ценность этого титана: управление камерой в 3D.

Проект показывает как можно:

  • Следить за целью (персонажем или объектом)
  • Позволяет вращать вид мышью/сенсором
  • Позволяет приближать/отдалять (зум колёсиком)
  • Может упираться в стены (физика)

Первый взгляд на титана

Запустите проект для того, чтобы увидеть мощь титана в действии по клавише ctrl + B.

Это не колоссальный титан, но представляет для нас ценность в работе с 3D.

Перейдём в main.collection для того, чтобы посмотреть на игровой мир коллекции в 3D.


Должно получиться примерно так:

Зажмите ctrl + ЛКМ для того, чтобы можно было использовать поворот в пространстве этой коллекции:

(Здесь должна быть GIF-анимация, которая показывает вышеупомянутый процесс).

Зажмите atl + ЛКМ для того, чтобы можно было перемещаться в пространстве коллекции:

(Здесь должна быть GIF-анимация, которая показывает вышеупомянутый процесс).

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

(Здесь должна быть GIF-анимация, которая показывает вышеупомянутый процесс).

В панели Outline редактора можно просмотреть структуру открытой коллекции:

Нажмите на какой-либо из квадратиков, чтобы открыть структуру игрового объекта:

Нажми на игровой объект floor и переместите их в пространстве:

(Здесь должна быть GIF-анимация, которая показывает вышеупомянутый процесс).

Ты можешь вращать игровой объект:


(Здесь должна быть GIF-анимация, которая показывает вышеупомянутый процесс).

Ты можешь масштабировать игровой объект:


(Здесь должна быть GIF-анимация, которая показывает вышеупомянутый процесс).

Попробуй проделать вышеупомянутые действия с игровым объектом go2:

Изучаем анатомию этого титана

Игровые объекты могут содержать разные компоненты.
В нашей случае они ограничиваются такими компонентами как: игровой скрипт, камера,
модель, объект столкновения.

Затронем каждый компонент по отдельности:

Скрипт — это “треугольник на спине титана”. Отвечает за логику игрового объекта, если в скрипте написать: “изменяй позицию”, то игровой объект изменит позицию во время выполнения скрипта в игре.

Давайте перейдём в main.script и посмотрим на функцию go.animate():

go.animate("/cube", "position.x", go.PLAYBACK_LOOP_PINGPONG, -2, go.EASING_INOUTQUAD, 5)

Измените четвертый параметр (to) функции на 10. Что вы увидите?
В чём разница от минусового значения от плюсового?
Поиграйте с easing-функциями.
(Здесь должна быть GIF-анимация, которая показывает вышеупомянутый процесс).

Модель — сделайте двойной щелчок по какой-нибудь модели (значок пирамидки).


Можете посмотреть или изменить свойства этой модели.

Например, можете изменить свойство text0 (текстуру модели):

Камера — это “глаз”, с помощью которого мы видим игровые объекты в игре.
В этом проекте мы видим игровые объекты в перспективе, так как галочка Orthographic Projection снята:


Поставьте галочку и вы увидите:

Объект столкновения — отвечает за симуляцию физики в игре. Физику в игре можно “писать” самому с помощью типа объекта столкновения (Kinematic), или использовать встроенную физику Defold.
Измените в игровом объекте floor тип компонента collisionobject на Dynamic и измените массу на 1.0:


Запустите проект (ctrl + B).
(Здесь должна быть GIF-анимация, которая показывает вышеупомянутый процесс).

Сердце камеры — как я понимаю, это рендер скрипт.
В нашем случае — это orbit.script в папке orbit.
Давайте попробуем поиграться с ним.

Настройка различных свойств для камеры:

go.property("target", hash("")) -- игровой объект, к которому прикрепляется камера
go.property("distance", 10) -- начальная значение дистанции от камеры
go.property("distance_min", 2) -- минимальное значение приближения камеры
go.property("distance_max", 20) -- максимальное значение отдаления камеры
go.property("angle_x", 0) 
go.property("angle_y", 0)
go.property("angle_min", -1.5)
go.property("angle_max", 0.5)
go.property("collisions", false) -- столкновения с коллайдерами

Функция для установки камеры в определенную позицию.

local function set_camera(self)
	self.center = self.target ~= hash("") and go.get_world_position(self.target) or vmath.vector3(0)

	local rot = vmath.quat_rotation_y(self.angle_y) * vmath.quat_rotation_x(self.angle_x)
	local pos = vmath.rotate(rot, vmath.vector3(0, 0, self.distance)) + self.center
	
	go.set_rotation(rot)

	if self.collisions then
		local result = physics.raycast(self.center, pos, {hash("default")})
		if result then
			pos = (pos - self.center) * result.fraction + self.center
		end
	end
	
	go.set_position(pos)
end

Различные команды инициализации компонентов:

function init(self)
	msg.post(".", "acquire_input_focus")
	msg.post("@render:", "use_camera_projection")
	msg.post("camera", "acquire_camera_focus")

	self.touch_down = false

	set_camera(self)
end

Обновление камеры в каждом кадре:

function update(self, dt)
	if self.target ~= hash("") and self.center ~= go.get_world_position(self.target) then
		set_camera(self)
	end
end

Обработка ввода:

function on_input(self, action_id, action)
	if action_id == hash("touch") then
		self.touch_down = true
		if action.released then
			self.touch_down = false
		end
	end

	if self.touch_down and action_id == nil then

		self.angle_x = self.angle_x + action.dy * 0.01
		self.angle_y = self.angle_y - action.dx * 0.01

		self.angle_x = math.min(self.angle_x, self.angle_max)
		self.angle_x = math.max(self.angle_x, self.angle_min)
		
		set_camera(self)
	end

	if action_id == hash("wheel_down") then
		self.distance = self.distance + 0.2
		self.distance = math.min(self.distance, self.distance_max)
		set_camera(self)
	elseif action_id == hash("wheel_up") then
		self.distance = self.distance - 0.2
		self.distance = math.max(self.distance, self.distance_min)
		set_camera(self)
	end
end

На данный момент, для того, чтобы поворачивать камеру, на требуется зажать ЛКМ.
Давайте уберём эту настройку.

Закомментируем эти строки:

-- в init()
	--self.touch_down = false

-- в on_input()
	-- if action_id == hash("touch") then
	-- 	self.touch_down = true
	-- 	if action.released then
	-- 		self.touch_down = false
	-- 	end
	-- end

А self.touch_down = true вынесем за блок кода, отвечающий за обработку действия "touch":

function on_input(self, action_id, action)
	-- if action_id == hash("touch") then
	-- 	self.touch_down = true
	-- 	if action.released then
	-- 		self.touch_down = false
	-- 	end
	-- end
	self.touch_down = true
	if self.touch_down and action_id == nil then

		self.angle_x = self.angle_x + action.dy * 0.01
		self.angle_y = self.angle_y - action.dx * 0.01

		self.angle_x = math.min(self.angle_x, self.angle_max)
		self.angle_x = math.max(self.angle_x, self.angle_min)
		
		set_camera(self)`Текст «как есть» (без применения форматирования)`
	end
-- остальной код

(Здесь должна быть GIF-анимация, которая показывает вышеупомянутый процесс).

Давайте изменим единицу зума в обработке ввода “wheel_down” и “wheel_up”:

self.distance = self.distance + 1
self.distance = self.distance - 1

-- измените на 
self.distance = self.distance + 5
self.distance = self.distance - 5

Намного быстрее происходит приближение и отдаление.
Давайте увеличим максимальные значения для отдаления.
За это отвечает пользовательское свойство self.distance_max:

go.property("distance_max", 20)
-- измените на 
go.property("distance_max", 2000)

Мы можем препятствовать камере заходить за игровые объекты, имеющие объекты столкновения:


Для этого нужно установить галочку в панели Properties:

Пока я не понял почему, но из скрипта значение этого свойства не меняется:

go.property("collisions", false)

Поэтому настраиваем в панели редактора Properties.

Давайте также через панель Properties изменим целевой объект, к которому прикрепляется камера. Измените значение свойства target на /go2.
Запустите проект:

В Defold существуют как локальные, так и мировые координаты.
Локальные координаты — показывают относительность внутри родителя. Какую позицию игровой объект или компонент занимает, в иерархии родитель-ребенок.

Мировые координаты — показывают положение внутри всей игровой сцены, в нашем случае, коллекция main.collection.

self.center — это не позиция камеры, а позиция точки, вокруг которой камера вращается (центр вращения).

self.center = self.target ~= hash("") and go.get_world_position(self.target) or vmath.vector3(0)

Центр вращения равен или мировой позиции целевого объекта, или нулевому вектору.

Создание угла поворота:

local rot = vmath.quat_rotation_y(self.angle_y) * vmath.quat_rotation_x(self.angle_x)
local pos = vmath.rotate(rot, vmath.vector3(0, 0, self.distance)) + self.center

rot — это угол вращения (кватернион — специальный способ записать повороты в 3D).

  • self.angle_y — вращение вверх-вниз (как головой кивать)
  • self.angle_x — вращение влево-вправо (как головой мотать)

pos — это позиция камеры:

  • vmath.vector3(0, 0, self.distance) — камера в начале координат, но далеко назад по оси Z (расстояние self.distance)
  • vmath.rotate(rot, ...) — поворачиваем эту позицию на угол rot
  • + self.center — сдвигаем на центр цели

Аналогия:

  • Представь, что камера стоит в 5 метрах сзади от персонажа.
  • Ты берёшь эту камеру и крутишь её вокруг персонажа (как на стержне).
  • Вот где она окажется — это новая позиция.

Попробуй поиграться с значениями:

local pos = vmath.rotate(rot, vmath.vector3(0, 0, self.distance)) + self.center
local pos = vmath.rotate(rot, vmath.vector3(0, 1, self.distance)) + self.center
local pos = vmath.rotate(rot, vmath.vector3(1, 0, self.distance)) + self.center
local pos = vmath.rotate(rot, vmath.vector3(0, 0, 0)) + self.center

Устанавливаем вращение камеры на угол rot:

go.set_rotation(rot)

Проверка на столкновения с игровыми объектами:

	if self.collisions then
		local result = physics.raycast(self.center, pos, {hash("default")})
		if result then
			pos = (pos - self.center) * result.fraction + self.center
		end
	end

Что здесь:

  • physics.raycast — это луч от персонажа (self.center) до камеры (pos).
  • Если луч попал в стену (result) — камера “придвигается” к персонажу ближе.
  • result.fraction — на сколько процентов луч прошёл до препятствия (0.5 = луч пошёл на половину).

Аналогия:

  • Если камера “врезается в стену”, она автоматически подтягивается поближе к персонажу, чтобы не быть под стеной.

Устанавливаем финальную позицию камеры:

go.set_position(pos)

Визуально:

Персонаж (self.center)
   |
   | <- расстояние self.distance
   |
   [Камера] <- вращается вокруг персонажа под углами angle_x и angle_y

Говорим рендер-скрипту: “Используй мою камеру для показа”:

msg.post("@render:", "use_camera_projection")

Обработка сообщений находится в main.render_script.
В game.project установлен именно этот рендер:

Сообщение объекту с ID “camera”: “Ты теперь активная камера.”

## `msg.post("camera", "acquire_camera_focus")`

“Камера вращается вокруг персонажа на расстоянии, смотря на него. Если камера упирается в стену, она подтягивается поближе. Камера всегда повёрнута так, чтобы ‘смотреть’ на персонажа.”

Выражаю благодарность abadonna · GitHub за то, что делится проектами на GitHub.
И astrochili (Roman Silin) · GitHub за то, что собрал список проектов по Defold.

Где найти титанов: