Доброго времени суток! В этой статье я хочу поделиться опытом разработки своей игры с использованием игрового движка Unity.
Концепция игры заключается в том что вам нужно взять на себя управление звездолетом и уничтожить как можно большее количество метеоритов. На вашем пути будут появляться вражеские звездолеты, которые будут мешать вам и после их уничтожения будут появляться «капсулы» после подбора которых будет доступен новый тип оружия. Игра будет называться Galaxy Desteroid.
Разработка
Графика игры состоит из следующих текстур:
На основе этих текстур были созданы следующие префабы
где:
asteroidrotate — метеорит который нужно уничтожать
enemy — вражеский звездолет
explosionasteroid, explosionenemy, explosionplayer — это анимации взрыва созданные с использованием particle system
gunactivator(s) — это капсулы которые будут активировать разный тип оружия в игре
Все остальное типа laser и т.п. это и есть оружие.
Игра будет включать в себя 2 сцены: главное меню и игровая сцена.
Главное меню
Где «menu» это главное меню а «1» это игровая сцена.
В качестве фона я использовал спрайт с именем «space», на нем нарисован космос.
Далее создаем скрипт «menu.cs» (Щелкаем правой кнопкой → выбираем Create → C# Script) и «вешаем» его на background. background это спрайт со 100% прозрачностью на котором создаются основные элементы управления (START/EXIT) а space служит просто для декорации.
Содержимое скрипта:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using UnityEngine.SceneManagement;
public class menu : MonoBehaviour {
public GUIStyle mystyle;//объявляется для того чтобы изменять начертание GUI компонентов(шрифт, размер и.т.п.)
string score;
void Start ()
{
StreamReader scoredata = new StreamReader (Application.persistentDataPath + "/score1.gd");//создание файловой переменной
score = scoredata.ReadLine ();//чтение строки
scoredata.Close ();//закрытие файловой переменной
}
// Update is called once per frame
void Update () {
}
void OnGUI(){
GUI.Box (new Rect (Screen.width*0.15f, Screen.height*0.8f, Screen.width*0.7f, Screen.height*0.1f), "MAX DESTROYED:"+score,mystyle);
if (GUI.Button (new Rect (Screen.width*0.15f, Screen.height*0.25f, Screen.width*0.7f, Screen.height*0.1f), "START",mystyle))
{
SceneManager.LoadScene (1);//Загрузка игровой сцены
}
if (GUI.Button (new Rect (Screen.width*0.15f, Screen.height*0.4f, Screen.width*0.7f, Screen.height*0.1f), "EXIT",mystyle))
{
Application.Quit();//Выход из игры
}
}
}
Еще не забываем повесить на «space» скрипт activemenu. Он служит для того чтобы создать анимацию движения фона меню. Потом создаем копию «space» и ставим ее чуть выше.
Содержимое скрипта activemenu:
using UnityEngine;
using System.Collections;
public class activemenu : MonoBehaviour {
float speed=-0.1f;
void Start () {
}
// Update is called once per frame
void Update () {
transform.Translate (new Vector3 (0f,speed,0f));
if (transform.position.y < -12f)
{
transform.position=new Vector3(0f,13f,0f);
}
}
}
Должно получиться примерно вот так:
Создание игровой сцены
Игровая сцена состоит из следующих ключевых объектов:
космос (спрайт «space»)
игрок
метеориты
вражеские корабли
прочее(лазеры и взрывы).
Космос организован также как в главном меню. Здесь можно ничего не трогать.
Далее нужно обратить внимание на то что в игре будут постоянно генерироваться из префабов, такие объекты как метеориты, выстрелы и враги. И те объекты которые упустил игрок нужно удалять, чтобы в лишний раз не нагружать память.
Это можно сделать следующим образом. Создаем новый игровой объект на сцене(у меня это «controlcountobjects»), добавляем к нему компонент boxcollider и растягиваем его вокруг игровой зоны.
Далее добавляем на него скрипт со следующим содержимым:
using UnityEngine;
using System.Collections;
public class systemcontrolobjects : MonoBehaviour {
void Start ()
{
}
void Update () {
}
void OnTriggerExit2D(Collider2D col)
{
Destroy(col.gameObject);
}
}
В этом скрипте происходит удаление элементов при выходе из box collider объекта controlcountobject. Таким образом получается некая зона при выходе из которой происходит удаление объектов.
Генерация метеоритов
Добавляем данный скрипт на камеру:
using UnityEngine;
using System.Collections;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
public class blockgenerator : MonoBehaviour {
public GameObject asteroid;//Добавляем сюда метеорит из префабов
float x,y,timer;
float timerespawn=0.25f;//Период возрождения метеоритов. С помощью данной переменной можно контролировать плотность трафика метеоритов.
bool trigtime=false;//Переключатель для отсчета времени. Если его значение true то тогда отсчитывается время для генерации следующего метеорита.
public int score;//Подсчет общего числа подбитых метеоритов за текущую игру и если данное число превысит рекорд то тогда оно будет записано в файл рекорда. С этой переменной будут взаимодействовать другие скрипты. Каждый созданный метеорит будет передавать команду, которая будет увеличивать данную переменную на единицу.
public float data;
void Start ()
{
score = 0;//
timer = timerespawn;
StreamReader scoredata = new StreamReader (Application.persistentDataPath + "/score1.gd");
data = float.Parse(scoredata.ReadLine ());
scoredata.Close ();
}
void Update ()
{
if (timer==timerespawn)//Если установлено время для отсчета генерации метеорита то:
{
x = Random.Range (-2.5f, 2.5f);//1)Выбираем рандомную координату в которой появится метеорит из динамического диапазона.
Instantiate(asteroid, new Vector3(x,5.5f,-2.17f),transform.rotation);//2)Генерация метеорита из префаба. x выбирается рандомно из указанного выше диапазона.
trigtime = true;//3)Активируем переключатель для отсчета времени. После его активации в течение времени указанного в timerrespawn метеориты не будут появляться.
}
if (trigtime==true)//Проверка переключателя
{
timer = timer-Time.deltaTime;//Отсчет времени для генерации слудующего метеорита.
}
if (timer < 0)//Если время периода истекает то тогда:
{
timer = timerespawn;//1)Сброс времени на число записанное в timerespawn
trigtime = false;//2)Блокирование отсчета времени
//Здесь происходит возврат значений на "по умолчанию". При их сбросе снова будет происходить генерация метеоритов и отсчет времени. И так по кругу
}
}
}
Метеорит
Выше можно заметить, то что метеориты которые появляются на сцене разного размера и еще они вращаются. Это все потому что метеорит реализован с помощью двух GameObject, где один находится внутри другого (матрешка).
Внешний объект «asteroidrotate» описывает движение метеорита, содержит circle collider и запускает эффект взрыва, в случае столкновения, а «aster» рандомно при своем создании задает скорость направления вращения и размер.
Скрипт для asteroidrotate:
using UnityEngine;
using System.Collections;
public class asteroidlogic : MonoBehaviour {
public GameObject explosion;//Здесь нужно добавить префаб взрыва
float speedasteroid=-0.1f;//Скорость движения метеорита
void Start ()
{
}
void Update ()
{
transform.Translate (new Vector3 (0, speedasteroid, 0f));//Движение метеорита вниз по экрану с заранее указанной скоростью
}
void OnTriggerEnter2D(Collider2D col)
{
if (col.tag == "systemcontrol")
{
return;//Возврат необходим чтобы избежать конфликта с объектом controlcountobjects
}
if (col.tag == "laser") //Проверка на попадание выстрелов игрока в метеорит
{
Destroy (col.gameObject);//Удаляем сам выстрел
GameObject.Find ("Main Camera").GetComponent<blockgenerator> ().score++;//Добавляем единицу к общему числу подбитых метеоритов
Instantiate(explosion, transform.position,transform.rotation);//Генерируем взрыв из префабов
Destroy (this.gameObject);//Удаление метеорита
}
}
}
Скрипт для aster:
using UnityEngine;
using System.Collections;
public class rotator : MonoBehaviour {
int f;
float sc;
void Start ()
{
f = Random.Range (-5, 5);
sc = Random.Range (0.1f,0.2f);
transform.localScale = new Vector3 (sc,sc,sc);//размер
}
void Update ()
{
transform.rotation *= Quaternion.AngleAxis (f,new Vector3(0,0,1));//вращение
}
}
Игрок
Кидаем спрайт с звездолетом на игровую сцену, добавляем на него box collider(не забываем отмечать «Is Triger»), добавляем rigibody(нужно заморозить Y координату). Все это необходимо чтобы игрок мог взаимодействовать с другими игровыми объектами.
Можно еще добавить компонент particle system, который будет изображать струю из двигателя.
В скрипте для игрока происходит установление «связи» с такими префабами как explosionplayer, laser, laser3x, laser3xhor и т.п. а также организация управления и подбор «капсул» для активации других видов оружия. Управление устроено так что игровой объект просто движется за пальцем пользователя и попутно ведет огонь.
Содержимое скрипта:
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
public class carconroller : MonoBehaviour
{
public GameObject laser;//Эта переменная будет связана с префабом laser
public GameObject laser3x;//Эта переменная будет связана с префабом laser3x
public GameObject laserhor;//Эта переменная будет связана с префабом laserhor
public GameObject laser3xhor;//Эта переменная будет связана с префабом laser3xhor
public GameObject sphere;//Эта переменная будет связана с префабом sphere
public GameObject sphere3x;//Эта переменная будет связана с префабом sphere3x
public GameObject explosionplayer;//Эта переменная будет связана с префабом explosionplayer
float x,y,z,x1,y1;
bool trigtime=false;
public float speedreset=0.25f;//период перезарядки орудия в миллисекундах
float timer;
Vector2 startpos;
Vector2 startcar;
//объекты laser...sphere3x являются оружием а ниже булевы переменные их соответствующие активаторы
public bool gun1 = true;//По умолчанию активировано 1 оружие(laser), оно является базовым и имеет неограниченный боезапас, в отличие от других видов(laser3x...)
public bool gun2 = false;
public bool gun3 = false;
public bool gun4 = false;
public bool gun5 = false;
public bool gun6 = false;
//При активации оружия отличного от базового боезапас будет заканчиваться. Когда боезапас закончится будет автоматически активировано базовое оружие.
int guncount=0;//Количество боезапаса для оружия, которое отличается от базового. Для каждого из дополнительного вида оружия устанавливается свое число боезапаса.
void Start ()
{
timer = speedreset;
y = laser.transform.position.y;
z = laser.transform.position.z;
}
public void Update ()
{
if (timer < 0)
{
timer = speedreset;
trigtime = false;
}
if (Input.GetMouseButton(0))//Отслеживание нажатия на экран
{
Vector2 pos = Camera.main.ScreenToWorldPoint (Input.mousePosition);//Запись в переменную pos координат места, где произошло касание экрана.
transform.position = pos;//присвоение позиции игровому объекту координат из переменной pos
transform.position = new Vector2 (transform.position.x,transform.position.y+1f);//корректировка координат игрока(необязательно)
if (timer==speedreset)//проверка истечения времени(время перезарядки)
{
if (gun1 == true)//проверка базового орудия
{
Instantiate (laser, new Vector2(transform.position.x,transform.position.y+1.1f), transform.rotation);//Генерация выстрелов из префаба laser в месте текущей позиции игрока(с некоторыми поправками)
trigtime = true;
}
if (gun2 == true && guncount > 0)//В случае активации 2-го орудия выстрелы будут генерироваться по данному коду
{
guncount--;//Снижение числа боеприпасов
if (guncount == 0) //В случае если боеприпасы закончатся то активируем 1 орудие а остальные блокируем
{
gun1 = true;
gun2 = false;
gun3 = false;
gun4 = false;
gun5 = false;
gun6 = false;
}
Instantiate (laser3x, new Vector2(transform.position.x,transform.position.y+1.1f), transform.rotation);//Генерация выстрелов из префаба laser3x
trigtime = true;
}
//Ниже для остальных видов "вооружения" все происходит аналогично
if (gun3 == true && guncount > 0)
{
guncount--;
if (guncount == 0)
{
gun1 = true;
gun2 = false;
gun3 = false;
gun4 = false;
gun5 = false;
gun6 = false;
}
Instantiate (laserhor, new Vector2(transform.position.x,transform.position.y+1.1f), transform.rotation);
trigtime = true;
}
if (gun4 == true && guncount > 0)
{
guncount--;
if (guncount == 0)
{
gun1 = true;
gun2 = false;
gun3 = false;
gun4 = false;
gun5 = false;
gun6 = false;
}
Instantiate (laser3xhor, new Vector2(transform.position.x,transform.position.y+2f), transform.rotation);
trigtime = true;
}
if (gun5 == true && guncount > 0)
{
guncount--;
if (guncount == 0)
{
gun1 = true;
gun2 = false;
gun3 = false;
gun4 = false;
gun5 = false;
gun6 = false;
}
Instantiate (sphere, new Vector2(transform.position.x,transform.position.y+1.1f), transform.rotation);
trigtime = true;
}
if (gun6 == true && guncount > 0)
{
guncount--;
if (guncount == 0)
{
gun1 = true;
gun2 = false;
gun3 = false;
gun4 = false;
gun5 = false;
gun6 = false;
}
Instantiate (sphere3x, new Vector2(transform.position.x,transform.position.y+2f), transform.rotation);
trigtime = true;
}
}
if (trigtime == true)
{
timer = timer - Time.deltaTime;//Отсчет времени для перезарядки
}
}
}
void OnTriggerEnter2D(Collider2D col)
{
if (col.tag == "systemcontrol")
{
return;//Возврат необходим чтобы избежать конфликта с объектом controlcountobject
}
if (col.tag == "gunactivator2")//проверка на пересечение с "капсулой" для gunactivator2
{
gun2 = true;//активация второго орудия
guncount = 85;//установка количества боеприпасов
gun1 = false;//отключение остальных "орудий"
gun3 = false;
gun4 = false;
gun5 = false;
gun6 = false;
Destroy (col.gameObject);//уничтожение "капсулы" с которой произошло пересечение
return;
}
//Ниже все аналогично
if (col.tag == "gunactivator3")
{
gun3 = true; guncount = 50;
gun1 = false;
gun2 = false;
gun4 = false;
gun5 = false;
gun6 = false;
Destroy (col.gameObject);
return;
}
if (col.tag == "gunactivator4")
{
gun4 = true; guncount = 15;
gun1 = false;
gun3 = false;
gun2 = false;
gun5 = false;
gun6 = false;
Destroy (col.gameObject);
return;
}
if (col.tag == "gunactivator5")
{
gun5 = true; guncount = 100;
gun1 = false;
gun3 = false;
gun4 = false;
gun2 = false;
gun6 = false;
Destroy (col.gameObject);
return;
}
if (col.tag == "gunactivator6")
{
gun6 = true; guncount = 50;
gun1 = false;
gun3 = false;
gun4 = false;
gun5 = false;
gun2 = false;
Destroy (col.gameObject);
return;
}
//Если столкновение с "капсулами" не произошло то это значит что игрок столкнулся с лазером противника, метеоритом или с вражеским звездолетом. Значит нужно удалить игрока со сцены
Handheld.Vibrate ();//активация вибрации(для смартфонов)
if (GameObject.Find ("Main Camera").GetComponent<blockgenerator> ().score>GameObject.Find ("Main Camera").GetComponent<blockgenerator> ().data)//Если число сбитых метеоритов в текущей игровой сессии выше чем в файле с игровым рекордом, то делаем запись нового рекорда в файл
{
StreamWriter scoredata=new StreamWriter(Application.persistentDataPath + "/score1.gd");
scoredata.WriteLine(GameObject.Find ("Main Camera").GetComponent<blockgenerator> ().score);
scoredata.Close();
}
Instantiate (explosionplayer, transform.position, transform.rotation);//Генерация взрыва звездолета игрока
Destroy (col.gameObject);//Удаление объекта с которым произошло пересечение
Destroy(this.gameObject);//Удаление игрока с игровой сцены
}
}
Активаторы оружия.
Результат работы скрипта.
Когда игрок будет удален а в файл будет записан новый рекорд уничтоженных метеоритов, то тогда нужно переходить на главное меню.
Создаем скрипт со следующим содержимым и подключаем его на камеру:
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
public class Exit : MonoBehaviour {
public GameObject target;//Добавляем сюда звездолет игрока
float timer=3f;
void Start ()
{
}
void Update ()
{
if (Input.GetKey (KeyCode.Escape))//Если будет нажата кнопка назад во время игры, то:
{
if (GameObject.Find ("Main Camera").GetComponent<blockgenerator> ().score>GameObject.Find ("Main Camera").GetComponent<blockgenerator> ().data)
{
StreamWriter scoredata=new StreamWriter(Application.persistentDataPath + "/score1.gd");
scoredata.WriteLine(GameObject.Find ("Main Camera").GetComponent<blockgenerator> ().score);
//Запись в файл рекорда переменную score из "blockgenerator", если она больше нее
scoredata.Close();
}
SceneManager.LoadScene (0);//Загрузка главного меню
}
if (!GameObject.Find ("playercar"))//Если игрок был удален то по истечению времени(в секундах) указанного в timer будет открыто главное меню
{
timer = timer - Time.deltaTime;
if (timer < 0)
{
SceneManager.LoadScene (0);
}
}
}
}
Данный скрипт наблюдает за состоянием игрока.
Взрывы
Эффект взрыва можно создать с помощью Particle System. Но тогда он будет зацикленным и вечно повторяться. Чтобы этого не было, на префаб взрыва нужно добавить следующий скрипт.
using UnityEngine;
using System.Collections;
public class DestroyAsteroid : MonoBehaviour {
void Start ()
{
}
void Update ()
{
Destroy (this.gameObject,0.4f);//Удаление игрового объекта после его создания. Время жизни 400 миллисекунд. Значение можно изменить.
}
}
Выстрелы
Выстрелы генерируются из места нахождения игрока во время его движения.
В скрипте для выстрела нужно задать только скорость и направление.
using UnityEngine;
using System.Collections;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
public class laser : MonoBehaviour {
float speedlaser=0.5f;
void Start ()
{
}
void Update ()
{
transform.Translate (new Vector3 (0, speedlaser, 0f));
}
}
Генерация противников
Генерация противников устроена аналогично генерации метеоритов.
Содержимое скрипта Enemygenerator:
using UnityEngine;
using System.Collections;
public class enemygenerator : MonoBehaviour {
public GameObject enemy;
bool trigtime=false;
float speedreset=2f;
float timer,x;
void Start ()
{
timer = speedreset;
}
void Update ()
{
if (timer < 0)
{
timer = speedreset;
trigtime = false;
}
if (timer == speedreset)
{
x = Random.Range (-2.5f, 2.5f);//Задаем местоположение
Instantiate(enemy, new Vector2(x,5.5f),transform.rotation);//Генерация противника из префаба
trigtime = true;
}
if (trigtime == true)
{
timer = timer - Time.deltaTime;
}
}
}
Логика противника
Поведение противника организовано таким образом, что он просто летит по прямой, ведет огонь и в случае своего поражения может оставить капсулу для активации игроком нового оружия
Выглядит это вот так:
using UnityEngine;
using System.Collections;
public class enemylogic : MonoBehaviour {
public GameObject explosionenemy;//Префаб вызрыва
public GameObject laserenemy;//Префаб выстрела
//Префабы капсул для активации других видов оружия
public GameObject gunactivator2;
public GameObject gunactivator3;
public GameObject gunactivator4;
public GameObject gunactivator5;
public GameObject gunactivator6;
//
bool trigtime=false;
float speedreset=1.5f;//Время перезарядки
float timer;
float speedenemy = -0.02f;//Скорость и направление
float x;
void Start ()
{
timer = speedreset;
}
void Update ()
{
if (timer < 0)
{
timer = speedreset;
trigtime = false;
}
if (timer == speedreset)
{
Instantiate (laserenemy, new Vector2(transform.position.x,transform.position.y-0.4f), transform.rotation);
trigtime = true;
}
if (trigtime == true)
{
timer = timer - Time.deltaTime;
}
transform.Translate (new Vector3 (0, speedenemy, 0f));
}
void OnTriggerEnter2D(Collider2D col)
{
if (col.tag == "systemcontrol")
{
return;
}
if (col.tag == "Player")
{
Instantiate (explosionenemy, transform.position, transform.rotation);
Destroy(this.gameObject);
}
if (col.tag == "laser")
{
x = Random.Range (0f, 100f);//Генерируется любое число от 0 до 100
if (x > 1f && x < 5f) //если число в диапазоне от 1 до 5 то создается капсула gunactivator2
{
Instantiate (gunactivator2, transform.position, transform.rotation);
}
//Внизу все аналогично
if (x > 20f && x < 25f)
{
Instantiate (gunactivator3, transform.position, transform.rotation);
}
if (x > 40f && x < 45f)
{
Instantiate (gunactivator4, transform.position, transform.rotation);
}
if (x > 60f && x < 65f)
{
Instantiate (gunactivator5, transform.position, transform.rotation);
}
if (x > 80f && x < 85f)
{
Instantiate (gunactivator6, transform.position, transform.rotation);
}
Instantiate (explosionenemy, transform.position, transform.rotation);
Destroy(this.gameObject);
}
}
}
Каждый префаб «капсулы» с оружием просто движется по направлению к игроку и в случае пересечения уничтожается.
using UnityEngine;
using System.Collections;
public class gunactivatorlogic : MonoBehaviour {
float speed = -0.025f;
void Start ()
{
}
void Update ()
{
transform.Translate (new Vector3 (0, speed, 0f));
}
}
Итог
Публикация
Выход игры на google play не увенчался особым успехом. За все время пребывания в маркете набралось чуть больше 500 загрузок.
Автор: WRXM4STER