Pull to refresh

Третье практическое задание с сайта unity3dstudent.com

Reading time9 min
Views10K
Продолжим разбирать практические задания с unity3dstudent.com. На очереди последняя на данный момент задачка. Статья слегка задержалась, но, надеюсь, будет кому-то полезна.

Вот ссылка на оригинальное задание: www.unity3dstudent.com/2010/07/challenge-c03-beginner

Суть: игрок должен уметь перемещаться вправо/влево и стрелять по трём мишеням. Мишени при попадании должны падать, а игроку за каждую сбитую мишень начисляется очко. По достижении трёх очков показывается экран окончания игры.

Первая задача.
Вторая задача.




Часть 1: Сцена.


Добавим на сцену плоскость – пол, на который будут падать кубы-мишени, её размер пока изменять не будем. Предположим, что игрок будет стрелять из точки положения камеры в том же направлении, куда она смотрит. Для простоты предположим, что стрелять будем вдоль оси OX (напоминаю, направления осей показано в правом верхнем углу вкладки Scene).

Необходимо в соответствии с этим расположить камеру. Выберем на сцене плоскость. Если нажать на ось OX (на отвечающий ей конус-кнопку на первом скриншоте ниже), то мы «посмотрим» на выбранный объект (то есть плоскость) в направлении, параллельном оси OX (см. второй скриншот).

Скриншот 1


Скриншот 2

Не удивляйтесь, что перестала быть видна сама плоскость, так и должно быть. Теперь поставим камеру в ту же точку, из которой смотрим: выбираем в панели иерархии объект Main Camera, нажимаем GameObject -> Align With View (или нажмите Ctrl+Shift+F). Теперь из камеры видно плоскость в нужном ракурсе, но далековато, да и камера слишком низко расположена (при выбранном объекте камеры в правом нижнем углу вкладки Scene – предпросмотр вида камеры). На обведённое кругом пока не обращайте внимания.



Вернёмся к «нормальному» виду на сцену: для этого нужно выделить на сцене плоскость и нажать на кубик между стрелками осей в верхнем правом углу (сейчас он виден как квадрат) – он обведён красным кругом на скриншоте выше. Получим что-то вроде этого (может быть нужно будет удалиться от плоскости, покрутив колёсико мыши, чтобы камера попала в поле зрения):


На скриншоте выделена камера.

Теперь поднимем камеру повыше и пододвинем к плоскости (можно даже так, что ближний край плоскости станет не виден):



С камерой пока всё. Добавим в сцену освещение – точечный источник света (можно и любой другой, здесь цель добавления освещения – просто сделать сцену «посветлее», не более того; для красивого освещения, возможно, придётся использовать другие источники света) и приподнимем его:



Замечу, что у точечных источников света есть несколько редактируемых параметров. В частности, нас интересуют Intensity и Range. Первый – интенсивность света – отвечает за то, как ярко светит источник. Второй – дальность действия. То есть источник света может светить очень ярко, но лишь в небольшом объёме. И сколько бы мы не меняли параметр Intensity, дальше заданного радиуса свет от источника распространяться не будет.

По умолчанию выставляются параметры Range = 10 и Intensity = 1. Если выделить объект источника света, на сцене вокруг него обрисовывается шар – это и есть объём, в котором «светит» источник. В моём случае плоскость лежала внутри этого объема, так что Range можно не менять, но плоскость была как-то тускловато освещена, поэтому я увеличил Intensity до 4 (на скриншоте выше это уже сделано).

Создадим теперь префаб для мишени в панели Project, назовём его target (можно, конечно, обойтись и тремя отдельными объектами, но это неудобно, да и вдруг понадобится не три, а сто таких мишеней?). Добавим в сцену куб. Слегка разукрасим его, добавив текстуру. В прошлом разборе мы создавали материал с некоторой текстурой, почем бы не использовать его ещё раз? Если не помните, как создать материал – загляните в разбор предыдущей задачки.



Теперь из панели иерархии перетащим этот куб на префаб в панели Project, а сам куб удалим. Создадим три копии (или «образца»; по-английски, насколько я понимаю, это называется instance) префаба. Разместим их все примерно на той же высоте, что и камера:



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

Можно было заметить, что мишени у нас пока что без компонента Rigidbody, они же не должны падать до того, как в них попали. А как понадобится – мы сразу добавим им этот компонент и они упадут (в уроках с того же www.unity3dstudent.com неявно предлагается именно такой вариант, о другом способе – ближе к концу статьи, в третьей части).

Основная сцена готова, экран завершения игры опять же будет позже.

Часть 2: Скрипты.


Сначала определимся, какие скрипты нам нужны.

Во-первых, нужно из точки расположения камеры стрелять по мишеням, в том же скрипте (он будет «повешен» на саму камеру) будем двигать её вправо/влево. Этот скрипт назовём Playerscript.

Ещё к префабу мишени нужно прикрепить скрипт, который будет при соударении добавлять компонент Rigidbody и засчитывать игроку очко. Это будет Rigidbodytizer.

Но кто будет вести счёт? Можно было бы делать это в скрипте для камеры, но как-то некрасиво получается. Добавим в сцену пустой объект (GameObject->Create Empty) и назовём его GameManager – он будет считать набранные очки и при нужном количестве переходить на другую сцену – экран окончания игры. Этому объекту тоже нужен свой скрипт, пусть называется так же: GameManager.

Сначала разберёмся с тем, что нужно в первую очередь: скрипт GameManager. По сути нам нужны только метод Update для проверки счёта и пара переменных для хранения текущего счёта и количества очков, которые нужно набрать для победы (можно, конечно, обойтись использованием числа 3, но если захочется модифицировать игру, всплывут некоторые неудобства).

using UnityEngine;
using System.Collections;

public class GameManager : MonoBehaviour 
{
	public uint neededScore = 3;
	public static uint score = 0;
	
	// Update is called once per frame
	void Update () 
	{
		if (score == neededScore)
		{
			// будет добавлено позже
		}
	}
}

Переменная score описана как public static, так как в этом случае к ней можно будет обращаться из любого скрипта как GameManager.score. Конечно, не очень хорошо, что переменная, в которой хранится значение счёта, является public-членом, но для упрощения скриптовой логики оставим так. Не забудьте прикрепить только что написанный скрипт к объекту GameManager.

Теперь займёмся скриптом Rigidbodytizer. Методы Update или Start нам не понадобятся, добавим метод OnCollisionEnter():

void OnCollisionEnter()
{
	if (rigidbody == null)
	{
		gameObject.AddComponent("Rigidbody"); 
		GameManager.score++;
	}
}

Условие проверяет, добавлен ли уже компонент Rigidbody к объекту, и если нет, добавляет его и увеличивает число засчитанных очков. Обратите внимание, увеличение числа очков – тоже внутри условия! Иначе очко засчитается даже за соударение мишени с полом. Теперь прикрепим Rigidbodytizer к префабу target.

Осталось добавить стрелка – скрипт Playerscript. Но для начала нужен префаб «пули» — им и будем стрелять. Добавляем в проект префпаб “bullet”, а в сцену – сферу. Последнюю сожмём до размеров, например, 0.4 по всем измерениям: в Inspector у компонента Transform изменим все значения поля Scale на 0.4.

Можно добавить материал и сфере, это опционально. Перетащим сферу из панели Hierarchy на префаб bullet в панели Project и удалим сферу со сцены.

Как мы будем двигать камеру? Скорее, вопрос не «как» (поменять transform.Position – не проблема), а «в соответствии с чем?». У класса Input есть замечательный статический метода GetAxis, который по названию оси выдаёт некое float-значение. В том числе для строк “Horizontal” и “Vertical” мы получим значение от -1 до 1, показывающее степень отклонения от, соответственно, вертикальной и горизонтальной осей. Это сработает для стрелок и WASD на клавиатуре и для джойстика. При этом если для джойстика фраза про «отклонение от оси» имеет смыл, то для клавиатуры, как я понял, большую часть времени при нажатой клавише возвращается +1 или -1 (в зависимости от направления), хотя и есть некоторая инертность при нажатии и отпускании клавиши (значение, возвращаемое методом Input.GetAxis, проходит через промежуточное значение). Более подробно про этот метод – здесь.

С помощью полученной информации заставим камеру двигаться:
public class Playerscript : MonoBehaviour 
{
	public const float speed = 7; 
	public GameObject bulletPrefab;
	public float force = 1500;
	
	// Update is called once per frame
	void Update () 
	{
		float translation = Input.GetAxis("Horizontal") * Time.deltaTime * speed;			
		transform.position += new Vector3(0, 0, translation);
	}
}

Public переменная speed отвечает за максимальную скорость движения камеры. Значение, возвращаемое Input.GetAxis умножаем на Time.deltaTime для того, чтобы камера двигалась не быстрее speed метров в секунду, а не «метров за кадр». А bulletPrefab – префеб пули, которым будем стрелять. force – модуль силы, с которой будем эту самую пулю «кидать».

Прикрепим скрипт к камере, запустим сцену и попробуем нажимать стрелки вправо/влево. Всё отлично, пора и пострелять!

Добавим выстрел при нажатии на клавишу прыжка (пробел по умолчанию) в метод Update:
if (Input.GetButtonDown("Jump"))
{
	GameObject bullet = GameObject.Instantiate(bulletPrefab, transform.position, transform.rotation) as GameObject;
	bullet.rigidbody.AddForce(transform.forward * force);
}

Не забудем выбрать объект MainCamera и в панели Inspector в поле Bullet Prefab компонента Palyerscript (Script) перетащить префаб bullet из панели Project.

Можно запустить сцену – игра почти готова. Осталось сделать переход на другую сцену по окончании игры. Для этого нужно сначала создать новую сцену и обязательно включить её в список сцен для сборки.

Добавляем в проект новую сцену, сразу сохраним её под именем “win”. Выберем File -> Build Settings. Потом нужно нажать кнопку Add Current в появившемся окне (можно и перетащить сцену из панели Project в список Scenes In Build). Поменяем цвет фона у объекта Main Camera на какой-нибудь более оптимистичный:



Теперь нужно добавить текст (например, “You win!”). Нажимаем GameObject -> Create Other -> GUI Text. Появится новый объект с текстом по умолчанию “Gui Text”. Изменим поле Text компонента GUIText на нужный, также поменяем размер шрифта (поле Font Size). Но текст оказывается не по центру экрана:



На картинке выделена и причина такого поведения текста. Свойство Anchor показывает, где у текста «центр». Не физический центр, а тот центр, которым определяется положение текста. Сейчас выбрано значение “Upper left”, то есть текст «подвешен» за верхний левый угол. Как можно заметить, этот угол и в самом деле находится в центре экрана. Выберем значение «Middle center», и текст переместится в положенное место по середине экрана.

Почему именно середина экрана, и что делать, если нужно сместить текст? У GUIText есть ещё и группа полей Pixel Offset с полями X и Y. Задавая им нужные значения можно смещать текст на указанное количество пикселей относительно центра экрана (точнее, центра окна с игрой).

Добавим переход на эту сцену в нужный момент времени. Вернёмся на основную сцену и подправим метод Update в скрипте GameManager:
void Update () 
{
	if (score == neededScore)
	{
		Application.LoadLevel("win");
	}
} 

Это загрузит недавно созданную сцену при достижении нужного счёта.

На этом основная часть заканчивается, получилось вполне играбельно. Стоило бы, конечно, добавить надпись с текущим результатом в основную сцену, но пусть это будет небольшим заданием :) Замечу, что досутп к тексте объекта, имеющего компонент GUIText осуществляется так:
guiText.text = "Hello world!";


Часть 3: Эстетическая.


И снова ощущение, что в сцене что-то не так! Что сделает порядочный кубик, если в него врежется порядочная пуля (хотя у нас это, скорее, ядро)? Правильно, отлетит! Но почему же не отлетает сейчас? Ответ вполне логичен.

Когда в методе OnCollisionEnter мы добавляем мишени компонент Rigidbody, ядро уже столкнулось с мишенью без Rigidbody. Но нет же метода, вызывающегося, когда «ещё чуть-чуть, и будет столкновение»! Поэтому проблема решается проще.

Что по сути должно происходить? Мишень висит в воздухе. Как такое возможно? Если она не имеет массы? Начнём с того, что так не бывает. А зачем нам отсутствие массы? Чтобы на тело не действовала сила притяжения к земле. Так может это выключается?

Прошлый абзац – от начала и до конца лирическое отступление, на деле обошлось без этих рассуждений. Я просто добавил префабу мишени компонент Rigidbody и случайно заметил там поле Use Gravity. Вот и нашёлся тот самый выключатель силы тяжести.

Кстати, если запустить игру сейчас (проделав только описанное абзацем выше), получится забавный эффект: мишени разлетаются, но не падают вниз под действием силы тяжести.

Пришла пора изменить скрипт Rigidbodytizer (теперь он уже, правда, не оправдывает своего названия).
void OnCollisionEnter()
{
	if (!rigidbody.useGravity)
	{
		rigidbody.useGravity = true;
		GameManager.score++;
	}
}


Ну вот, другое дело. Теперь игра выгладит как-то так:



Второй момент, который хотелось бы затронуть: управление камерой. Движение камеры только вдоль одной прямой накладывает значительные ограничения на положение кубиков (чтобы их ещё и сбить можно было). По аналогии с движением вправо/влево добавьте движение вверх/вниз (придётся использовать
 Input.GetAxis("Vertical") 
). Тогда можно будет размещать кубики-мишени где угодно в пределах видимости, и в них можно будет попасть.

Напоследок отмечу, что если размещать мишени только над плоскостью, логично было бы ограничить движение вправо/влево границами плоскости, внеся небольшие изменения в Playerscript. Добавим парочку переменных и метод Start (пояснения в коде):
float zBoundMin, zBoundMax; // ограничения на перемещение вдоль оси Z
	
void Start()
{
	// ограничивающий параллелепипед объекта с названием "Plane"
	Bounds planeBounds = GameObject.Find("Plane").collider.bounds;
	zBoundMin = planeBounds.min.z; // минимальное значение Z
	zBoundMax = planeBounds.max.z; //максимальное значение Z
}


Конечно, можно перевычислять ограничения на координату Z каждый кадр, но это неэффективно, а в случае неподвижной плоскости ещё и бессмысленно.

Теперь изменим метод Update: после изменения вектора transform.position добавим ещё строчку:
transform.position = new Vector3(transform.position.x,transform.position.y, Mathf.Clamp(transform.position.z, zBoundMin, zBoundMax));


К сожалению, отдельно поменять координату Z вектора transform.position нельзя. Функция Mathf.Clamp(x, a, b) возвращает значение x, если зажать его между значениями a и b (то есть если, например, x меньше a, то функция вернёт значение a)

Если вы добавили перемещение вверх/вниз, убедитесь схожим образом, что камера не уходит ниже плоскости и не поднимается слишком высоко.

Часть 4: Заключительная.


Похожими рассуждениями «а что, если добавить сюда XYZ?» и с помощью идей тестировавших игру друзей я модифицировал игру до примерно вот такого состояния:


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

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

До новых встреч.
Tags:
Hubs:
+19
Comments15

Articles

Change theme settings