- PVSM.RU - https://www.pvsm.ru -
В прошлом году я написал пост [1] о своей игре Chronicles of cyberpunk, находящейся в разработке. С тех пор было сделано немало и игра наконец-то готова. Хочу рассказать, что сделал и с какими трудностями столкнулся.
Если коротко, то Chronicles of cyberpunk — это игра об Большом Брате, который с помощью суперкомпьютера контролирует жизнь людей в городе. Но однажды суперкомпьютер получает способность мыслить и главный герой должен остановить его, пока еще не слишком поздно. Геймплей включает в себя разговоры с основными и второстепенными персонажами, поиск кодов и предметов, а так же мини-битвы с боссами. Главный упор сделан на атмосферу и сюжет.
Разработка идет с 4 января 2015, а релиз намечен на 27 декабря 2017. Были использованы инструменты: Unity3d, Visual Studio, GitHub, Wrike, Blender.
У игрока может сложиться впечатление, что в игре все механики примитивные. И он скорее всего будет прав. Но, как ни странно, их здесь много и над некоторыми пришлось сидеть по несколько дней, недель, месяцев. Некоторые задачи были настолько сложными и специфичными, что я уже отчаивался их решить, а на форумах никто не мог помочь… вообще никто! Это очень угнетало. Вот некоторые из них:
Но это все цветочки. Самым хардкорным было сделать игровой цикл и систему сохранений. Сразу скажу, что я осознаю то, насколько код ниже ужасен, поэтому прошу помидорами в меня не кидать)) Буду рад любым предложениям, как можно было сделать его лучше.
1. Игровой цикл
В игре 9 сцен + 3 дополнительные сцены (титры, главное меню, самая первая PRELOAD-сцена). Когда игрок перемещается между сценами, вся информация стирается и если в одной сцене игрок взял ключ от двери, находящейся на другой сцене, то на этой другой сцене нужно эту информацию как-то получить. А как обратиться к компонентам той сцены, все данные об которой уже стерты?
Я использую скрипт GameManager, который содержит функцию DontDestroyOnLoad(), которая инициализируется на самой первой сцене Preload. Инициализация этой функции — единственное назначение сцены Preload. После нее сразу автоматически загружается следующая. Объект, который содержит скрипт, помечен тегом Acts, поэтому, когда загружается новая сцена и инициализируются ее объекты, движок быстро находит нужный объект по тегу, после чего с ним можно работать. Такой скрипт существует на протяжении всей игры на всех сценах в единственном экземпляре. В нем я и храню все нужные состояния: номер сцены, на которой мы находимся, номер текущей миссии и т.п.
public string nameOfLastLoadedScene;
private GameObject player;
public int currentActNumber { get; set; } // инкремент в конце каждого акта
private Act_0 act0;
private Act_1 act1;
private Act_2 act2;
//..
private Act_2 act28;
void Start ()
{
DontDestroyOnLoad(this);
SceneManager.LoadScene("Home");
}
void OnLevelWasLoaded()
{
player = GameObject.FindGameObjectWithTag("PlayerOnMainScene");
PlacingPlayerNearHouse();
}
// При загрузке главной сцены размещаем игрока рядом с домом, откуда он выходил
void PlacingPlayerNearHouse()
{
switch (nameOfLastLoadedScene)
{
case "": player.transform.position = new Vector3(-4.42f, 0.65f, 49.26f); break;
case "": player.transform.position = new Vector3(-14.8f, 0.65f, -7.2f); break;
case "": player.transform.position = new Vector3( 0.44f, 0.65f, 6.89f); break;
case "": player.transform.position = new Vector3(36.63f, 0.65f, 6.9f); break;
case "": player.transform.position = new Vector3( 9.45f, 0.65f,-36.73f); break;
case "": player.transform.position = new Vector3(-0.32f, 0.65f, -9.34f); break;
default: break;
}
}
В зависимости от номера текущего акта загружаем определенное поведение
void Update()
{
ActManager();
}
// Запускать определенный акт в зависимости от currentActNumber
void ActManager()
{
Debug.Log("currentActNumber: " + currentActNumber);
if (SceneManager.GetActiveScene().name != "PRELOAD" &&
SceneManager.GetActiveScene().name != "STARTSCREEN")
{
GameObject objWithActScripts = GameObject.FindGameObjectWithTag("Acts");
switch (currentActNumber)
{
case 0: act0 = objWithActScripts.GetComponent<Act_0>();
act0.StartAct(); break;
case 1: act1 = objWithActScripts.GetComponent<Act_1>();
act1.StartAct(); break;
//..
}
}
}
И такая схема позволяет программировать последовательность шагов для каждого акта отдельно
public void StartAct1(MonoBehaviour mb)
{
switch (stepNumber)
{
case 0:
// проигрываем анимацию открытия глаз
mb.StartCoroutine(OpenCloseEyesAnimation());
break;
case 1:
// отображаем подсказку "нажмите любую клавишу, чтобы проснуться"
ShowTip(contentToPrint.tipsTasks[0]);
stepNumber++;
break;
case 2:
// при нажатии на любую кнопку очищаем текст подсказки,
// анимация закрытия глаз, аудио зевания
if (Input.anyKey)
{
audioYawn.Play();
ShowTip("");
mb.StartCoroutine(OpenCloseEyesAnimation());
}
break;
case 3:
// отключаем игрока в кровати и включаем основного игрока,
// анимация открытия глаз
PlayerInBedDisable();
mb.StartCoroutine(OpenCloseEyesAnimation());
break;
case 4:
// блокируем перемещение, отображаем диалог с дроном
ShowUIAndPrintMessage(0, 0);
stepNumber++;
break;
//..
}
}
2. Сохранение/загрузка
Какие стояли задачи: загружать нужную сцену, номер акта, номер шага, позицию игрока и т.п. Сначала использовал функции PlayerPrefs.SetInt(), PlayerPrefs.GetInt(), но после загрузки игры в стим (она еще недоступна) столкнулся с проблемой, что если загрузить обновление, то все сохранения затираются. Поэтому решил сохранять в файл в папку AddData:
// файлSaves.cs
[System.Serializable]
public class Saves
{
public string dateTime;
public float transformPositionX;
public float transformPositionY;
public float transformPositionZ;
public int latestSaveSlot;
public int actNumber;
public string sceneName;
public int slotImage;
public int currentActiveSlot;
public int stepNumber;
}
// файл SaveLoad.cs
public Saves saves;
public void ButtonSave()
{
SetSlotImage();
saves.dateTime = System.DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss") + " ";
saves.transformPositionX = playerTransform.position.x;
saves.transformPositionY = playerTransform.position.y;
saves.transformPositionZ = playerTransform.position.z;
saves.actNumber = gameManagerScript.currentActNumber;
SaveStepNumber();
saves.sceneName = SceneManager.GetActiveScene().name;
saves.slotImage = imageNumberForCurrentSlot;
dateText[currentActiveSlot].text = saves.dateTime;
var serializedSave = JsonUtility.ToJson(saves);
var saveFileName = Application.persistentDataPath + "/Save_" + PlayerPrefs.GetInt("currentActiveSlot") + ".save";
File.WriteAllText(saveFileName, serializedSave);
//==================================
savesForAllSlots.latestSaveSlotForAll = currentActiveSlot;
var serializedSaveForAll = JsonUtility.ToJson(savesForAllSlots);
var saveFileNameForAll = Application.persistentDataPath + "/Save_" + "ForAll" + ".save";
File.WriteAllText(saveFileNameForAll, serializedSaveForAll);
ButtonsActivation();
}
public void ButtonLoad()
{
loadSaveGameImage.SetActive(true);
CheckForContinueButton();
isLoadButtonPressed = true;
if (!isItContinueButton)
LoadJSON();
SceneManager.LoadScene(saves.sceneName);
gameManagerScript.currentActNumber = saves.actNumber;
LoadStepNumber();
}
private void LoadJSON()
{
if(File.Exists(Application.persistentDataPath + "/Save_" + PlayerPrefs.GetInt("currentActiveSlot") + ".save"))
{
var saveFileName = Application.persistentDataPath + "/Save_" + PlayerPrefs.GetInt("currentActiveSlot") + ".save";
string saveFilecontent = File.ReadAllText(saveFileName);
Saves deSerializedSave = JsonUtility.FromJson<Saves>(saveFilecontent);
saves = deSerializedSave;
}
}
Так, функция сохранения начала работать. Но была еще одна проблема. Для каждого акта есть цепочка шагов, которые игрок проходит один за другим. На акте №2 шагов 22 и если мы на шаге №8 подойдем к док-станции дрона, введем пароль, то дрон улетит на кухню (шаг №11). Если потом начать новую игру и загрузить номер шага №11, то получится, что дрон будет в док-станции, а должен быть на кухне. То есть номер шага мы загружаем корректно, а анимацию перемещения дрона не проигрываем. И таких мелочей куча, хранить их все в переменных (координаты и состояния всех объектов для каждого шага), а потом загружать довольно сложно. Поэтому я создал еще один метод, который является копией игрового цикла, но убрал оттуда строки, где ожидается активность со стороны игрока. И при загрузке игры мы просто в ускоренном темпе прогоняем все шаги от нулевого до требуемого, устанавливая актуальные состояния для всех объектов.
// Быстро перебираем все шаги для загрузки сохранения
public void QuickAct(int lastStepNumber)
{
Time.timeScale = 5;
for (int i = 0; i < lastStepNumber; i++)
switch (i)
{
case 0:
stepNumber++;
break;
case 1: // Текстовое интро
startTextCanvas.SetActive(true);
PrintStartText();
break;
case 2: // Инициализация объектов акта
openCloseEyesCanvas.SetActive(true);
startTextCanvas.SetActive(false);
elders.SetActive(true);
break;
//..
case 22: // Выходим на улицу
break;
}
Time.timeScale = 1;
}
Вот как это работает:
Понимаю, что это очень криво, но на момент написания скриптов (да и сейчас тоже) всеми моими знаниями в разработке игр были крупицы информации с форумов, видеоуроков и документации.
В определенный момент разработки мне захотелось узнать, что люди думают об игре. Поэтому зарегистрировался на игровых форумах и начал постить результаты работы. Постепенно появлялись люди, которые делились впечатлениями об игре. Вообще это одна из самых классных вещей — получать обратную связь от людей, которые хотят поиграть/поиграли в то, что ты сделал. Может быть кто-то тоже захочет создать сообщество вокруг своей игры, вот список сайтов:
В игре все (кроме музыки) сделано с нуля. Не были использованы никакие шаблоны и готовые ассеты:
И все это сосредоточено в одной единственной игре. Это очень круто — видеть, как придуманный тобой мир обретает форму и оживает, а все механики правильно работают. Вообще разработка игр — это лучший способ самовыражения, который я только знаю. Ты можешь создать все, что крутится у тебя в голове — мир, героев, сюжет. И самое крутое, что чем больше ты учишься, то тем более крутые штуки сможешь делать.
Я с самого начала не придумывал сюжет, а просто записывал разные интересные идеи. А потом садился и думал, как это все можно соединить. Было очень интересно узнать, что в итоге получится. Самые интересные идеи приходили совершенно неожиданно. В результате, как мне кажется, получился интересный и запутанный сюжет с большим количеством связей между объектами. В какой то момент даже решил сделать энциклопедию [37] по игре (осторожно, спойлеры) и начать работать над своей игровой вселенной. Вся игра будет состоять из трех частей, а мои остальные игры тоже будут происходить в этой вселенной.
Я эту штуку подсмотрел здесь же, на хабре. Суть в том, чтобы делать коммит каждый день. Если не сделал сегодня, то завтра придется делать за вчера задним числом и за сегодня. Это заставляет тебя хоть немного, но работать каждый день, при чем не заниматься планированием, а реально что-то делать, что можно закоммитить. В общем, всем советую попробовать
Заметил, что чем больше всего я в игру добавляю, то тем больше она мне нравится. И так может длиться до бесконечности, поэтому определился с датой релиза — 27.12.2017. До тех пор буду доводить все до совершенства.
Может быть это не самая крутая игра и может в нее никто не будет играть, но мне все равно. Главное, что я постарался, сделал все так хорошо, как смог, и мне нравится финальный результат, а сразу после релиза начну работать над второй частью.
Всем спасибо за внимание.
P.S. Кому интересно, у игры уже есть страница в Steam. Ссылку не выкладываю, чтобы не нарушать правила Хабра.
Автор: dimaCyberpunk
Источник [38]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/268361
Ссылки в тексте:
[1] написал пост: https://habrahabr.ru/post/308908/
[2] facebook.com: http://facebook.com
[3] twitter.com: http://twitter.com
[4] idlethumbs.net: http://idlethumbs.net
[5] gamedev.net: http://gamedev.net
[6] forums.indiegamer.com: http://forums.indiegamer.com
[7] badlogicgames.com: http://badlogicgames.com
[8] indiedb.com: http://indiedb.com
[9] lemmasoft.renai.us: http://lemmasoft.renai.us
[10] ascensiongamedev.com: http://ascensiongamedev.com
[11] wildfiregames.com: http://wildfiregames.com
[12] forum.unity3d.com: http://forum.unity3d.com
[13] forums.tigsource.com: http://forums.tigsource.com
[14] vk.com: http://vk.com
[15] gamedev.ru: http://gamedev.ru
[16] gamesmaker.ru: http://gamesmaker.ru
[17] indiedb.ru: http://indiedb.ru
[18] make-games.ru: http://make-games.ru
[19] gcup.ru: http://gcup.ru
[20] smartprogress.do: http://smartprogress.do
[21] kanobu.ru: http://kanobu.ru
[22] reddit.com: http://reddit.com
[23] habrahabr.ru: http://habrahabr.ru
[24] gamin.me: http://gamin.me
[25] cyberpunk.dirty.ru: http://cyberpunk.dirty.ru
[26] connect.unity.com: http://connect.unity.com
[27] itch.io: http://itch.io
[28] youtube.com: http://youtube.com
[29] gamejolt.com: http://gamejolt.com
[30] moddb.com: http://moddb.com
[31] wikia.com: http://wikia.com
[32] store.steampowered.com: http://store.steampowered.com/
[33] humblebundle.com: http://humblebundle.com
[34] gog.com: http://gog.com
[35] музыкальный трек: http://kozyr.org/2017/07/25/music-for-the-game/
[36] freesound.org: http://freesound.org/
[37] энциклопедию: http://ru.chronicles-of-cyberpunk.wikia.com/wiki/Chronicles_of_cyberpunk
[38] Источник: https://habrahabr.ru/post/342144/
Нажмите здесь для печати.