Редактор уровней для MySpaceShooter

Я второй раз участвовал в интенсиве на образовательной платформе Skillbox по созданию на Unity космического шутера. Сама идея игры новизной не блещет, но захотелось доделать её до «релиза». Большинство игр состоят из уровней, миссий, локаций и т.п., процесс прохождения которых сопровождается усложнением геймплея. Разработка качественных уровней — головная боль дизайнеров. Для её удобства можно создать редактор уровней. Если игра станет популярной, хороший редактор уровней упростит разработку дополнений, в том числе и фанатами игры.

Разрабатывать приложение удобнее всего на языке C#. На нем пишутся все скрипты в игре на Unity. Я использую среду разработки Microsoft Visual Studio 2019. В решение (solution) со скриптами игры добавим проект на основе стандартного шаблона Windows Forms Application (.NET Framework).

Содержание

Класс 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 ниже :

Редактор уровней
рис. 1 Итоговый вид редактора уровней.

Для DataGridView установим свойство AutoSizeColumnsMode в Fill, чтобы столбцы занимали всё пространство по горизонтали. Кнопкам добавим обработчики нажатий (Click). Для выбора загружаемого и сохраняемого файла будем использовать стандартные диалоги : openFileDialog и saveFileDialog.

рис. 2 Выбор файла для загрузки
рис. 3 Выбор файла для сохранения

Код класса главной формы полностью ниже :

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 :

рис. 4 Данные уровней в CSV файле.

Реализация загрузки в игру массива уровней в 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 ниже :

рис. 5 Отображение информации об уровне.

Обзор игры и описание особенностей её разработки читайте в моей следующей статье.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *