Я второй раз участвовал в интенсиве на образовательной платформе Skillbox по созданию на Unity космического шутера. Сама идея игры новизной не блещет, но захотелось доделать её до «релиза». Большинство игр состоят из уровней, миссий, локаций и т.п., процесс прохождения которых сопровождается усложнением геймплея. Разработка качественных уровней — головная боль дизайнеров. Для её удобства можно создать редактор уровней. Если игра станет популярной, хороший редактор уровней упростит разработку дополнений, в том числе и фанатами игры.
Разрабатывать приложение удобнее всего на языке C#. На нем пишутся все скрипты в игре на Unity. Я использую среду разработки Microsoft Visual Studio 2019. В решение (solution) со скриптами игры добавим проект на основе стандартного шаблона Windows Forms Application (.NET Framework).
Содержание
- Класс Level.cs
- Главная форма приложения «Редактор уровней для MySpaceShooter»
- Формат сохранения данных
- Реализация загрузки в игру массива уровней в Unity
- Отображение информации об уровне
Класс Level.cs
В общем случае для описания уровня используется какой-то набор параметров, который проще всего задать в классе. Там же логично прописать методы обработки этих данных, чтобы облегчить взаимодействие с «внешним миром». Создадим класс Level.cs. Код привожу ниже :
public class Level
{
/// <summary>
/// Номер уровня
/// </summary>
[DisplayName("Номер уровня")]
public int Num { get; set; }
/// <summary>
/// Число кораблей противника: 123 => 1 - L, 2 - M, 3 - S
/// </summary>
[DisplayName("Число кораблей противника")]
public int N_Chips { get; set; }
/// <summary>
/// Число астероидов: 123 => 1 - L, 2 - M, 3 - S
/// </summary>
[DisplayName("Число астероидов")]
public int N_Asters { get; set; }
/// <summary>
/// Опыт за уровень
/// </summary>
[DisplayName("Приз - опыт")]
public int PrizeExp { get; set; }
/// <summary>
/// Золото за уровень
/// </summary>
[DisplayName("Приз - золото")]
public int PrizeGold { get; set; }
/// <summary>
/// Очки умений за уровень
/// </summary>
[DisplayName("Приз - очки умений")]
public int PrizeSkillPoints { get; set; }
}
Блок /// </summary> нужен для показа всплывающей подсказки при наведении на переменную в MVS 2019 и как комментарий, конечно же. Атрибут [DisplayName(«Номер уровня»)] задает имя столбца при отображении в заголовках элемента DataGridView. Для числа кораблей и астероидов их количество по типам (размерам) будем хранить в десятичных разрядах — 2 переменных вместо 6, т.к. игра простая и до 10 штук каждого вида вполне хватит. S, M, L — маленькие, средние и большие соответственно.
Главная форма приложения «Редактор уровней для MySpaceShooter»
Разделим главную форму приложения с помощью SplitContainer по горизонтали на две части. В верхнюю большую часть поместим DataGridView, в нижнюю две кнопки и метку для поясняющего текста. Должно получиться как на рис. 1 ниже :
Для DataGridView установим свойство AutoSizeColumnsMode в Fill, чтобы столбцы занимали всё пространство по горизонтали. Кнопкам добавим обработчики нажатий (Click). Для выбора загружаемого и сохраняемого файла будем использовать стандартные диалоги : openFileDialog и saveFileDialog.
Код класса главной формы полностью ниже :
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LevelSpaceShuterDesign
{
public partial class MainFormLD : Form
{
private BindingList<Level> myLevelCollection;
private string path = Application.StartupPath + "/arrLevels.csv";
public MainFormLD()
{
InitializeComponent();
myLevelCollection = new BindingList<Level>();
dataGridView1.DataSource = myLevelCollection;
}
private void butLoad_Click(object sender, EventArgs e)
{
string pathCsvFile = path;
openFileDialog1.Title = "Выбирите файл с массивом уровней";
openFileDialog1.Filter = "Файлы конфигураций уровней(*.csv)|*.csv|Все файлы(*.*)|*.*";
openFileDialog1.InitialDirectory = Application.StartupPath;
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
pathCsvFile = openFileDialog1.FileName;
}
if (File.Exists(pathCsvFile))
{ //Проверка существования файла сохранения
string[] lines = File.ReadAllLines(pathCsvFile);
//MessageBox.Show(lines[0]);
myLevelCollection = new BindingList<Level>();
for(int i=1; i< lines.Length; i++)
{
Level l = new Level();
string[] lp = lines[i].Split(';');
l.Num = int.Parse(lp[0]);
l.N_Chips = int.Parse(lp[1]);
l.N_Asters = int.Parse(lp[2]);
l.PrizeExp = int.Parse(lp[3]);
l.PrizeGold = int.Parse(lp[4]);
l.PrizeSkillPoints = int.Parse(lp[5]);
myLevelCollection.Add(l);
}
dataGridView1.DataSource = myLevelCollection;
}
}
private void butSave_Click(object sender, EventArgs e)
{
string pathCsvFile = path;
saveFileDialog1.FileName = path;
saveFileDialog1.Filter = "Файлы конфигураций уровней(*.csv)|*.csv|Все файлы(*.*)|*.*";
saveFileDialog1.InitialDirectory = Application.StartupPath;
if (saveFileDialog1.ShowDialog() == DialogResult.OK)
{
pathCsvFile = saveFileDialog1.FileName;
}
using (StreamWriter streamWriter = new StreamWriter(pathCsvFile))
{
streamWriter.WriteLine("Number;Chips;Asteroids;Exp;Gold;SkillPoints");
foreach(Level l in myLevelCollection)
{
streamWriter.WriteLine(string.Format("{0:D};{1:D};{2:D};{3:D};{4:D};{5:D}", l.Num, l.N_Chips, l.N_Asters, l.PrizeExp, l.PrizeGold, l.PrizeSkillPoints));
}
}
}
}
}
Для хранения массива данных уровней используем BindingList<Level> myLevelCollection, который и установим в dataGridView1.DataSource, чтобы изменения данных в таблице также автоматически сохранялись в массиве myLevelCollection.
Формат сохранения данных
Массив уровней будем хранить в файлах в CSV формате. Разделитель ‘;’ вместо стандартной запятой. Библиотеку для работы с файлами в CSV формате (например, CsvHelper) использовать большой необходимости нет. При сохранении первой запишем строку с именами полей. Далее в цикле выведем поля каждого уровня через ‘;’ на отдельной строке. При загрузке обратная процедура. Каждую строку данных делим по ‘;’ на массив подстрок, преобразуем в int и присваиваем соответствующим полям нового экземпляра класса Level, который добавляем в массив. Файл с данными показан на рис. 4 :
Реализация загрузки в игру массива уровней в Unity
Для хранения параметров массива уровней в проекте игры создадим класс всего с одной переменной типа ArrayList. Параметры уровня храним в полях класса Level, который скопируем из редактора уровней. Массив уровней в игре единственный и доступ к нему может понадобиться из разных классов, поэтому для его хранения объявим переменную public static MyArrLevels mAL = null; в классе GameManager (главный класс управления игрой).
public class MyArrLevels
{
public ArrayList aLev = new ArrayList();
}
Для операций загрузки и сохранения данных игры я использую public static class SaveLoad (Создание статичного класса позволит использовать методы без объявления его экземпляров). Один из его методов предназначен для загрузки массива уровней в статический экземпляр класса MyArrLevels. Сама загрузка выполнена аналогично загрузке в редакторе уровней.
public static MyArrLevels LoadArrLevelsMF(string file_path)
{
if (File.Exists(file_path))
{ //Проверка существования файла сохранения
MyArrLevels data = new MyArrLevels();
string[] lines = File.ReadAllLines(file_path);
for (int i = 1; i < lines.Length; i++)
{
Level l = new Level();
string[] lp = lines[i].Split(';');
l.Num = int.Parse(lp[0]);
l.N_Chips = int.Parse(lp[1]);
l.N_Asters = int.Parse(lp[2]);
l.PrizeExp = int.Parse(lp[3]);
l.PrizeGold = int.Parse(lp[4]);
l.PrizeSkillPoints = int.Parse(lp[5]);
data.aLev.Add(l);
}
return data; //Возвращение данных
}
else
{
return new MyArrLevels();
}
}
Отображение информации об уровне
Как и для хранения массива уровней, так и для хранения массива данных игроков в игре есть свой класс и класс для хранения данных выбранного игрока. Их экземпляры объявлены статическими в классе GameManager. Перед началом прохождения уровня информация о нём показывается в полях специальной панели как на рис. 5 ниже :
Обзор игры и описание особенностей её разработки читайте в моей следующей статье.