Pull to refresh

Unity3d script basics

Reading time 13 min
Views 110K


Предисловие


Эта статья будет посвящена новичкам в скриптовании Unity, но не совсем новичкам в синтаксисе JavaScript’а или любого другого подобного языка программирования. Тут надо маленько уточнить что JavaScript который использует Unity немного отличается от оригинального в пользу улучшенной поддержки ООП и называется соответственно Unity JavaScript. В нем например появились классы и их наследование, о чем не знают начинающие разработчики на Unity знакомые с объектно ориентированными возможностями оригинального JavaScript, поэтому отвергают его используя C# или Boo.

Благодарности


В первую очередь хочу сказать огромное спасибо автору написавшему статью Unity — бесплатный кроссплатформенный 3D движок (и браузерный тоже), и урок Unity3D для начинающих — Туториал 1, статьи очень хорошие, но вот продолжения нету уже довольно долго, в связи с этим я хотел бы помочь автору данных статей просвящать хабралюдей на тему Unity и я думаю он будет не против если я напишу апгрейд его урока.

Резюме


Я долго думал с чего начать и в итоге решил – начать с начала. То есть, с чего начинается большинство игр? Нет не правильно не с заставок и логотипов студии производителя, а с главного меню! В этом уроке я подробно расскажу об основных элементах игрового GUI, о том как загружать сцены из скрипта, а также о том как заставить различные скрипты взаимодействовать друг с другом. В этом уроке мы создадим главное меню для «шутера» созданного в уроке: Unity3D для начинающих — Туториал 1, также я расскажу что всетаки делал скрипт в том уроке, мы немного изменим его и научимся настраивать его во время игры, прямо из нашего игрового меню.

1. Labels и GUIStyle


Итак поехали, открываем наш проект который мы создали благодаря уроку. Создаем новую сцену (File -> New scene), далее создадим пустой объект внутри сцены (GameObject -> Create Empty) и назовем его например Menu and settings. Теперь создадим пустой файл JavaScript (Assets -> Create -> JavaScript), перетаскиваем его на наш объект Menu and settings и кликнем на самом скрипте два раза, откроется диалоговое окно редактирования скрипта.

Мы видим что файл скрипта у нас не совсем пустой, в нем находится функция Update, про неё я расскажу позже, пока что удалим её и напишем следующее:
public var welcomeLabel : GUIStyle;   //1

function OnGUI(){       //2
    GUI.Label(new Rect(Screen.width / 2, 0, 50, 20),"Welcome",welcomeLabel);  //3
}


Сохраним изменения и нажимаем Play.
Видим сверху, не совсем по центру, почти незаметное слово Welcom, написанное черным шрифтом:

Но что такое? Там должно быть слово Welcome!
На самом деле последняя буква потерялась потому что мы объявили ширину квадрата в котором написан текст недостаточной для того чтобы влезло все слово.
Но давайте обо всем по порядку, выключаем Play mode.

Если кому не понятно, пункты ниже будут соответствовать комментариям в нашем коде:
  1. Переменная welcomeLabel c типом GUIStyle. Идем во вкладку Hierarchy и выбираем наш GameObject (GO) — Menu and settings, видим что в нашем скрипте появилось новое поле Welcome Label. Разверните его, как видите в нем много дочерних полей, я думаю кто работал с JavaScript’ом и CSS в вебе уже наверное догадались для чего нужны эти свойства.
  2. Функция OnGUI(), она вызывается в каждом кадре для рендеринга и обработки GUI событий, все что связано с GUI необходимо вызывать в этой функции.
  3. Ф-я Label() класса GUI. Первым аргументом в эту функцию мы передаем структуру Rect, по сути это квадрат который задается координатами х и y верхнего левого угла, шириной и высотой, у нас как видим координата х = Screen.width / 2, тоесть ширина экрана деленная на 2, это нужно для того чтобы выровнять текст по центру, вторым аргументом передается текстовая строка «Welcome», и третий аргумент – это стиль с помощью которого мы будем задавать разные параметры нашему Label’у c помощью поля Welcome Label.

Теперь разверните поле Welcome Label и установите следующие параметры:


Нажмите на Play и полюбуйтесь на результат:

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

Лирическое отступление Begin


Вообще в качестве редактора скриптов вы можете использовать любую среду разработки которая нравится вам, все скрипты создаются в папке Assets вашего проекта. Например вы можете использовать свободную среду разработки MonoDevelop, хотя она предназначена в основном для кодинга C# и других .NET языков, для JS тоже сойдет. Она также включена в дистрибутив Unity (чтобы установить её надо поставить галочку перед началом установки Unity), а чтобы скрипты открывались постоянно через эту программу надо зайти в Edit -> Prefences… и в поле External Script Editor указать путь к нашему редактору, также надо синхронизировать все созданные вами скрипты с файлом проекта достаточно нажать Assets -> Sync MonoDevelop Project (кнопку не надо нажимать каждый раз после создания очередного скрипта достаточно нажать один раз, дальнейшие скрипты будут включены в файл проекта автоматически). Не буду описывать все достоинства этого редактора, пусть за меня это сделают скриншоты.

Стандартный редактор:


MonoDevelop:

Лирическое отступление End



2. Buttons и GUISkin


Двигаемся дальше, и модифицируем наш скрипт:
public var welcomeLabel : GUIStyle;
public var customSkin : GUISkin;    //1
public var playGameRect : Rect;     //2
public var optionsRect : Rect;      //2
public var quitRect : Rect;         //2




function OnGUI(){
        GUI.Label(new Rect(Screen.width / 2, 0, 50, 20),"Welcome",welcomeLabel);
           
        GUI.skin = customSkin;                    //3
           
        GUI.Button(playGameRect,"Play Game");     //4
        GUI.Button(optionsRect,"Options");        //4
        GUI.Button(quitRect,"Quit");              //4
           
}


Теперь создадим новый GUISkin, Assets -> Create -> GUI Skin, назовем его Menu Skin, теперь переходим на наш объект Menu and settings и перетаскиваем только что созданный Menu Skin на новое поле Custom Skin, также установим следующие настройки в остальных полях (Play Game Rect, Options Rect, Quit Rect):


Нажимаем Play, результат должен быть следующий:


Перейдем на наш Menu Skin во вкладке Project и чуть увеличим размер шрифта наших кнопок, тоесть разворачиваем поле Buttons и ставим Font Size равным 16, все это можно делать не выходя из Game mode и при выходе из него изменения сохранятся, так как мы меняем свойства префаба.

Теперь разберем наш скрипт:
  1. GUISkin это по сути универсальный набор стандартных настроек GUI позволяет очень гибко менять их.
  2. Про Rect мы уже знаем, только теперь объявляем переменные с модификатором доступа public вне функции дабы получить возможность настраивать их (переменные) из редактора.
  3. Присваиваем настройки нашего GUISkin переменной skin класса GUI, с этого момента все дальнейшие функции класса, будут использовать данный скин, если мы в какой-то момент хотим перестать использовать скин в скрипте и сбросить настройки на стандартные достаточно написать перед этим GUI.skin = null.
  4. Функция Button() по сути ничем не отличается от функции Label, за исключением того что возвращает bool значение (true когда кликаем на кнопку, false естественно во всех остальных случаях).


3. Меню Options и Sliders


Модифицируем скрипт дальше, используем возвращаемое значение ф-ции Button():
public var welcomeLabel : GUIStyle;
public var customSkin : GUISkin;
public var playGameRect : Rect;
public var optionsRect : Rect;
public var quitRect : Rect;


private var optionsMode = false;           //1

public var _bulletImpulse : float = 300;   //2
public var _shootDelay : float = 1;        //2



function OnGUI(){
    if(!optionsMode){                //1
        GUI.Label(new Rect(Screen.width / 2, 0, 50, 20), "Welcome", welcomeLabel);

        GUI.skin = customSkin;
       
        GUI.Button(playGameRect,"Play Game");
               
        if(GUI.Button(optionsRect,"Options")){
            optionsMode = true;             //1
        }
       
        GUI.Button(quitRect,"Quit");
               
    }else{
                               
        GUI.Label(new Rect(Screen.width / 2, 0, 50, 20), "Options", welcomeLabel);
               
        GUI.skin = customSkin;   //5
               
        GUI.Label(new Rect(270, 75, 50, 20),"Bullet Impulse");
        _bulletImpulse = GUI.HorizontalSlider(new Rect(50, 100, 500, 20), _bulletImpulse,10,700);//3
        GUI.Label(new Rect(560, 95, 50, 20), _bulletImpulse.ToString());//4
       
        GUI.Label(new Rect(270, 125, 50, 20),"Shoot Delay");
        _shootDelay = GUI.HorizontalSlider(new Rect(50, 150, 500, 20), _shootDelay, 0.1, 3);//3
        GUI.Label(new Rect(560, 145, 50, 20), _shootDelay.ToString());//4
               
        if(GUI.Button(new Rect(20, 190, 100, 30),"<< Back")){
            optionsMode = false;        //1
        }
       
    }
        
}


  1. Обычно мы привыкли чтобы при открытии какого – либо подменю на экране было только оно, а основное меню исчезало, за этим у нас и будет следить данная переменная.
  2. Собственно это и есть наши игровые опции, мы вернемся к ним позже.
  3. Функция HorizontalSlider() «рисует» (Вот неожиданность!) горизонтальный слайдер, первый принимаемый аргумент (Rect) оставлю без комментариев, второй аргумент – это текущая величина отвечающая за позицию ползунка на слайдере, третий аргумент – левое значение интервала величин, и последний – правое значение, функция возвращает float значение (при передвижении ползунка слайдера) расположенное между левой и правой величиной.
  4. Обратите внимание на последний аргумент в функции Label() – это float значение преобразованное в string функцией ToString(), это важно т.к. компилятор скриптов Unity не умеет преобразовывать число в строку сам, и выдаст ошибку.
  5. GUI.skin сбрасывается при переходе в другое меню, чтобы этого не происходило необходимо присваивать наш customSkin перед рисованием нового меню

Ну давайте же посмотрим что у нас получилось, нажимаем Play и на нашу кнопку Options:

Но что же опять такое? Что за Bullet и Shoot? Должно же быть Bullet Impulse и Shoot Delay! Опять не хватило ширины этих *** квадратов и неужели нам нужно опять лезть в скрипт и менять там ширину?

Нет! Не надо! У нас же есть наш замечательный GUISkin по имени Menu Skin. Не выходя из Play mode заходим в него, разворачиваем поле Label, в опции Text Clipping выбираем значение Overflow и убираем галочку напротив Word Wrap, вуаля, все встало на свои места, выходим из Game mode.

4. Play Game and Quit


Добавляем интерактивности нашим остальным кнопкам:
public var welcomeLabel : GUIStyle;
public var customSkin : GUISkin;
public var playGameRect : Rect;
public var optionsRect : Rect;
public var quitRect : Rect;


private var optionsMode = false;

public var _bulletImpulse : float = 300;
public var _shootDelay : float = 1;

function OnGUI(){
    if(!optionsMode){
        GUI.Label(new Rect(Screen.width / 2, 0, 50, 20),"Welcome",welcomeLabel);

        GUI.skin = customSkin;

        if(GUI.Button(playGameRect,"Play Game")){
            Application.LoadLevel("Test Scene");  //1
        }
           
        if(GUI.Button(optionsRect,"Options")){
            optionsMode = true;
        }
                   
        if(GUI.Button(quitRect,"Quit")){
            Application.Quit();                   //2
        }
                   
    }else{
                                           
        GUI.Label(new Rect(Screen.width / 2, 0, 50, 20), "Options",welcomeLabel);
                   
        GUI.skin = customSkin;
                   
        GUI.Label(new Rect(270, 75, 50, 20),"Bullet Impulse");
        _bulletImpulse = GUI.HorizontalSlider(new Rect(50, 100, 500, 20),_bulletImpulse,10,700);
        GUI.Label(new Rect(560, 95, 50, 20),_bulletImpulse.ToString());
           
        GUI.Label(new Rect(270, 125, 50, 20),"Shoot Delay");
        _shootDelay = GUI.HorizontalSlider(new Rect(50, 150, 500, 20),_shootDelay,0.1,3);
        GUI.Label(new Rect(560, 145, 50, 20),_shootDelay.ToString());
                   
        if(GUI.Button(new Rect(20, 190, 100, 30),"<< Back")){
            optionsMode = false;
        }
           
    }  
                           
}


Если вы войдете в Play mode и попробуете понажимать на эти кнопки, то скорее всего при нажатии на Play Game в консоли (Window -> Console) появится ошибка, а при нажатии на Quit вообще ничего не произойдет, давайте разберемся почему:
  1. Ф-я LoadLevel() класса Application загружает сцену по имени “Test Scene” (так я назвал сцену из урока Unity3D для начинающих — Туториал 1), в эту функцию можно так же передавать номер сцены. У вас должно быть возник вопрос: где же взять этот номер? Все просто, cохраните вашу сцену если вы еще этого не сделали, File -> Save Scene As… и назовите её Menu Scene, далее идем в File -> Build Settings… и перетаскиваем в появившееся окошко сначала нашу сцену Menu Scene, затем Test Scene.


    С правой стороны от имени сцены мы видим её порядковый номер (по умолчанию первой всегда загружается сцена с номером 0). Кто еще не догадался наша Test Scene не хотела грузиться потому что она не была зарегистрирована в Build Settings, но теперь все будет ОК. Закройте окошко Build Settings, войдите в Play mode и нажмите на кнопку Play Game. Ура! Наша сцена загружается!
  2. Но кнопка Quit по прежнему не работает. Не беспокойтесь, она и не будет работать если проект запускается в редакторе либо в web плеере. Для того чтобы кнопка заработала нужно чтобы наш проект стал самостоятельным приложением, для этого надо его «построить» и запустить. Для этого заходим в Build Settings и нажимаем на кнопку Build And Run, указываем папку где будет храниться готовое приложение, сохраняем, выбираем настройки, запускаем, видим наше главное меню, и нажимаем Quit, о чудо приложение закрывается.


5. DontDestroyOnLoad и дригие нехорошие слова


Для начала давайте разберем скрипт из урока Unity3D для начинающих — Туториал 1 и выясним какие настройки он использует и для чего он их использует:
public var bulletImpulse = 300;   //1
public var shootSpeed;            //2
public var bullet : GameObject;   //3
 
public var lastShotTime : float;  //4
 
function Start() {                //5
    lastShotTime = 0;
}

function Update () {              //6
 
    if (Input.GetKey(KeyCode.Mouse0)) {    //7
        if (Time.time>(lastShotTime + shootSpeed)){//8
                var bull_clone : GameObject;   //9
                       
                bull_clone = Instantiate(bullet, transform.position, transform.rotation);//10
                Physics.IgnoreCollision(bull_clone.collider, collider);//11
                bull_clone.rigidbody.AddForce(transform.forward*bulletImpulse, ForceMode.Impulse);//12
                lastShotTime = Time.time;//13
        }
    }
}


  1. импульс который дает пинок под зад передается нашей пули
  2. shootSpeed, не очень удачное название для данного параметра т.к. это по сути задержка между выстрелами, в скрипте MainMenu опцию которую в последствии нам предстоит получить вместо этой я назвал _shootDelay
  3. собственно сама пуля, может быть как префабом, так и объектом внутри сцены
  4. время последнего выстрела
  5. Ф-я Start() вызывается ОДИН раз перед ПЕРВЫМ методом Update
  6. Ф-я Update() вызывается в каждом кадре (вообще в официальной документации все функции взаимодействующие с физикой рекомендуют прописывать в функции FixedUpdate() во избежание разнообразных багов с физикой, но для нашего проектика это не критично так как он очень маленький).
  7. Если нажата левая кнопка мыши.
  8. И если текущее время больше времени последнего выстрела + задержка между выстрелами.
  9. Объявляем переменную с типом GameObject.
  10. Ф-я Instantiate() клонирует объект и возвращает его c заданной позицией и углом поворота в мировом пространстве. Cобственно первый аргумент bullet, это и есть копируемый объект, тоесть наша пуля, вторым аргументом мы обращаемся к классу transform игрового объекта к которому прикреплен скрипт(в нашем случае это объект Player), собственно класс Transform хранит информацию о физическом расположении игрового объекта, из него мы получаем две переменные необходимые нам, это position – хранящую позицию в мировом пространстве, и rotation – хранящую угол поворота.
  11. Обращаемся к объекту Physics и вызываем функцию IgnoreCollision которая заставляет его игнорировать все столкновения (коллизии) между коллайдером объекта (Player) к которому прикреплен скрипт, и коллайдерами клонов нашей пули, все это значит что между этими двумя объектами больше не будут происходить физические взаимодействия, и наш Player просто напросто будет ходить сквозь шары — клоны.
  12. обращаемся к объекту rigidbody нашей пули, который контролирует позицию объекта через физическую симуляцию, и вызываем функцию AddForce() которая добавит определенное физическое воздействие (в нашем случае импульс) на наш объект, первый параметр это вектор, в нашем случае направленный вперед относительно трансформации нашего Player’a и помноженный на силу импульса, второй параметр это режим силы.
  13. сохраняем текущее время, теперь — это время последнего выстрела

Итак перерабатываем этот поток информации и выясняем что нам по нужны всего лишь две переменные (bulletImpulse и shootSpeed) и как многие уже догадались нам нужно получить их из GameObject’а Menu and settings, а точнее из скрипта который к нему прикреплен.

Но вот получилась какая незадача. Для того чтобы получить нужные нам данные из скрипта объекта Menu and settings он должен находиться в одной сцене с нашим объектом Player, и как многие уже могли заметить, при нажатии нашей кнопки Play Game все объекты из сцены Menu Scene уничтожаются и во вкладке Hierarchy у нас появляются объекты из второй сцены (Test Scene).

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

Добавим в скрипт MainMenu следующую функцию:
function Awake(){
    DontDestroyOnLoad(this);
}


Войдем в Play mode и нажмем на Play Game. Круто! Меню не исчезло и накладывается у нас теперь поверх всего, это означает что объект Menu and settings не уничтожился, а так же это означает его присутствие во вкладке Hierarchy.

Но меню же теперь постоянно видно! Не порядок и с этим надо бороться, а следовательно модифицируем скрипт (это последний раз):
public var welcomeLabel : GUIStyle;
public var customSkin : GUISkin;
public var playGameRect : Rect;
public var optionsRect : Rect;
public var quitRect : Rect;


private var optionsMode = false;
private var menuMode = true;   //1
private var gameMode = false;  //1

public var _bulletImpulse : float = 300;
public var _shootDelay : float = 1;

function Awake(){
    DontDestroyOnLoad(this);
}


function OnGUI(){
    if (Input.GetKey(KeyCode.Escape)){  //2
        menuMode = true;                //1
        optionsMode = false;  
        Time.timeScale = 0;             //3
                
        if(gameMode){                   //1
            var ml = GameObject.Find("Player").GetComponent(MouseLook);  //4
            ml.enabled = false;  //4
        }
    }

    if(menuMode){
        if(!optionsMode){                       
                        
            GUI.Label(new Rect(Screen.width / 2, 0, 50, 20), "Welcome",welcomeLabel);
                
            GUI.skin = customSkin;
                        
            if(!gameMode){              //1
                if(GUI.Button(playGameRect, "Play Game")){
                    menuMode = false;   //1
                    gameMode = true;    //1
                    Time.timeScale = 1; //3
                    Application.LoadLevel("Test Scene");
                }
            }else{
                if(GUI.Button(playGameRect,"Resume")){
                    var _ml = GameObject.Find("Player").GetComponent(MouseLook);//4
                    _ml.enabled = true; //4
                    Time.timeScale = 1; //3
                    menuMode = false;   //1
                }
            }
                        
            if(GUI.Button(optionsRect,"Options")){
                optionsMode = true;
            }
                        
            if(GUI.Button(quitRect,"Quit")){
                Application.Quit();
            }
                        
        }else{
                                                        
            GUI.Label(new Rect(Screen.width / 2, 0, 50, 20),"Options",welcomeLabel);
                        
            GUI.skin = customSkin;
                        
            GUI.Label(new Rect(270, 75, 50, 20), "Bullet Impulse");
            _bulletImpulse = GUI.HorizontalSlider(new Rect(50, 100, 500, 20), _bulletImpulse,10,700);
            GUI.Label(new Rect(560, 95, 50, 20), _bulletImpulse.ToString());
                        
            GUI.Label(new Rect(270, 125, 50, 20), "Shoot Delay");
            _shootDelay = GUI.HorizontalSlider(new Rect(50, 150, 500, 20), _shootDelay,0.1,3);
            GUI.Label(new Rect(560, 145, 50, 20), _shootDelay.ToString());
                        
            if(GUI.Button(new Rect(20, 190, 100, 30), "<< Back")){
                optionsMode = false;
            }
        
        }
    }

}


  1. Для того чтобы меню было более гибким оно должно знать в каком состоянии сейчас находится.
  2. Добавим в скрипт возможность вызывать меню по нажатию клавиши Escape.
  3. Когда мы вызываем меню во время игры, она должна вставать на паузу, самый простой способ это сделать, обнулить переменную timeScale класса Time, по сути своей эта переменная отвечает за скорость игрового процесса, где значение 1 – нормальная скорость, значения меньше 1 – slo-mo, выше 1 – быстрая скорость, и 0 – пауза
  4. Ну вот собственно и мы и добрались до взаимодействия между скриптами, данная конструкция выполняет следующие действия: ф-я Find() класса GameObject ищет GO по имени и возвращает его, следом идет функция GetComponent() которая находит класс в нашем вернувшемся GO (класс MouseLook) и возвращает его, затем мы присваиваем bool значение переменной enabled внутри этого класса что позволяет включить либо отключить скрипт MouseLook (true и false соответственно). Все эти операции нужны нам для того чтобы при переходе в главное меню наш Player не мог вертеть «головой» (перевод игры в режим паузы не поможет, можете проверить закомментировав данные строчки).


6. Получаем настройки


Ну наконец то, мы дошли до финишной прямой все что нам осталось сделать, это получить настройки из объекта Menu and settings. Модифицируем скрипт из урока Unity3D для начинающих — Туториал 1:
//public var bulletImpulse = 300;   //1
//public var shootSpeed : float = 1;  //1
public var bullet : GameObject;  

public var lastShotTime : float;  

function Start() {          
        lastShotTime = 0;
}

function FixedUpdate () {    

    if (Input.GetKey(KeyCode.Mouse0)) {
       
        var go : GameObject = GameObject.Find("Menu and settings");//2
        var shootSpeed : float = go.GetComponent(MainMenu)._shootDelay;//3     
                       
        if (Time.time>(lastShotTime + shootSpeed)) {  
                               
            var bulletImpulse : float = go.GetComponent(MainMenu)._bulletImpulse;   //3
                                                                       
            var bull_clone : GameObject;  
                                                                                                                       
            bull_clone = Instantiate(bullet,transform.position,transform.rotation);  
                                                                                                                                                                                                                               
            Physics.IgnoreCollision(bull_clone.collider, collider);
            bull_clone.rigidbody.AddForce(transform.forward*bulletImpulse, ForceMode.Impulse);
                                                                                                                                               
            lastShotTime = Time.time;  

        }
    }
}


  1. Здесь эти переменные больше нам не потребуются так мы получим их значения из другого скрипта, я закомментировал их чтобы было понятнее.
  2. Получаем объект Menu and settings.
  3. Получаем компонент MainMenu, и его переменные.

Все, можете запускать и проверять. Наиболее явное безумие начинается при уменьшении опции Shoot Delay до минимума, шары должны вылетать из вашего Player’a с большой частотой, словно пули из пулемета. Ну в общем, думаю теперь вы сами разберетесь, ведь теперь вы владеете силой, силой скриптов Unity.

Заключение


Если после прочтения статьи вы подумали что скрипты в Unity нужны в основном только для того чтобы рендерить главное меню или катать шары по ландшафту, то вы глубоко ошибаетесь, это только капля в море. Они могут гораздо больше. Например можно запрограммировать искусственный интеллект, управлять скелетной анимацией, взорвать ядерную бомбу в конце концов, все зависит только от ваших знаний и умения использовать то что дает вам движок. Ну а эта статься я думаю пригодится людям начинающим осваивать дебри скриптования в Unity, и даст им стимул для движения вперед.
Tags:
Hubs:
+41
Comments 15
Comments Comments 15

Articles