Mariya Mariya

Dreaman, Спасибо! =)

Dreaman Dreaman

Mariya, законченный вариант реально классно выглядит! Молодец :)

Mariya Mariya

Всем привет!
Сегодня хочу показать законченный вариант домика Сырны.

alexprey alexprey

StarPlosion: Битва с пиратами за планету

Wings' might Wings' might

Всем привет)
Добавил на этой неделе начальное окно, новую валюту и достижения:

alexprey alexprey

shadeborn, может быть они нашли способ как обойти полноценный запуск этих сервисов при запуске игры? Если так, то почему бы и не использовать единый клиент?) В любом случае, чем дальше все идет, там это все больше становится похоже на эту шутку

...
shadeborn shadeborn

Это всё круто, но...зачем? Ок есть Стим. Хорошо, теперь еще и ЕГС есть. Благо, комп не зашкварен Оригином...но у людей и он стоит. И для работы игр внутри этих сервисов нужны, собсно, эти сервисы. Так поверх этих сервисов нужно накатить еще один...

Devion Devion

согласен, есть кейсы которые без них вообще не делаются, не знать или избегать их определенно неправильно

Так точно, либо любое изменение в объекте на стороне редактора.

alexprey alexprey

Devion, только рад за дополнение, Спасибо)

хм, а вот это интересно, не пробовал никогда такой кейс.

Devion Devion

лёш, ты же не против если я дополню? )

скриптаблы, для понимания могут храниться на уровне ассета и на уровне сцены.

Mariya Mariya

Всем привет!
Начали работу над мебелью в домик Сырны, и первым сделали чайный столик с чайным сервизом. А так же продолжаем работу над анимациями.

Wings' might Wings' might

Всем привет)
За неделю в игру было добавлено меню настроек, переделана старая локация, добавлены новые враги и повышена производительности

alexprey alexprey

А мне кажется это такой особый хитрый ход, хотя с другой стороны как разработчики успеют к этому подготовиться получше 🤔

Dreaman Dreaman

Всем привет!
Для проекта "Mental State" разработано новое устройство, которое уже полностью функционирует внутри игрового мира. Оно носит название "Репульсивер". Совместно с очередными большими воротами это устройство образует новую головоломку...

...
Mariya Mariya

Всем привет!
На этой неделе мы научили Сырну летать!

Tartal Tartal

alexprey, кастомизации - создание внешности персонажа? Я всегда любил это, но не думаю, что это будет к месту в мясном шутере)
EfimovMax, да, есть немного)

Tartal Tartal

Jusper, точно не помню, уже как полгода точно) Я вроде в Дискорде немного обсуждал эту тему. Пока немножко попробовал движок - мне очень нравится. Ну, в конце концов, он идеально подходит под жанры, с которыми я хочу работать...

alexprey alexprey

Первая тема крутая очень!

Логотип проекта Unity

Разбираемся с модулями и Assembly Definition

Наконец-то, свершилось! Теперь вы можете разбивать код в Unity на самостоятельные сборки, разделяя их по смыслу. Теперь ключевое слово internal будет иметь гораздо больше смысла в вашем коде, а вы сами можете реализовать полноценную модульную архитектуру. Все это доступно вам с новой Unity 2017.3.

Данная статья призвана рассказать вам чуть-чуть об организации модульной разработки. Здесь мы будет касаться как Unity, так и некоторых других аспектов, которые помогут вам начать.

Зачем вам это

Это скажется на вашей продуктивности. Очень часто вы пишите какой-то код, и используете его в разных проектах. Из первого скопировали во второй, и из второго в третий, оттуда в четвертый, потом вернулись на первый нашли баг и там что-то исправили. И вот вам приходится прыгать и вносить правку на втором, третьем, четвертом. А потом вы пересели на четвертый, что-то дописали, но уже было лень вносить правку на старых проектах (потом вспомню). Потом вы конечно не вспомнили, и тоже самое сделали с первым, и вот у вас уже код везде разный, ой-ой батюшки как быть! Ответ простой - разбейте репозиторий на независимые модули, которые не тянут игровой зависимости и можно использовать в любом проекте.
Что же касается возможности разбить сборки внутри Unity, то это скорее песня про то, чтобы разделить эти модули по уровню доступа, не давая жать вне сборки на какие-то рычаги, которые этот код могут сломать. Ну и просто сделать навигацию по модульной иерархии более удобной.

Чем дольше вы пользуетесь модульностью, тем больше она становится вам выгодна. Так как модули разделяются по смыслу (модуль для поиска пути, сетевого взаимодействия, игровые архивы, работа с движком, математика, и т.п.) вам будет проще делать новые проекты, так как скорее вам что-то понадобится из того, что вы уже написали. И вам не нужно будет перерывать тонны проектов на жестком диске (ну блин это же где-то здесь я писал!), достаточно будет просто добавить модуль с этим назначением в ваш проект. А еще вы можете расшарить модули коллегам по работе, и вести их совместно, ускоряя разработку друг другу.

Unity

Для начала обсудим самое сокровенное - как разбить проект на модули внутри Unity. Для этого нам понадобятся специальные файлы с расширением .asmdef. Эти файлы хранят в себе информацию о будущей сборке - ее название, зависимости и платформы.
Любая папка, в которой вы создадите данный файл будет отныне считаться отдельной сборкой. Вы можете создать файл через контекстное меню Project Window, выбрав пункт Create/Assembly Definition:

Разбираемся с модулями и Assembly Definition — Unity — DevTribe: Разработка игр (разработка игр, разработка игр)

После этого в открытой папке создастся файл .asmdef, который выглядит в инспекторе следующим образом:

Есть всего несколько простых правил, которым вы должны следовать в разбиении проекта на модули:

  • Нельзя создавать 2 .asmdef файла в одной папке
  • Зависимости не должны быть круговыми
  • Полноценный модуль включает в себя 2 .asmdef файла - MyExampleModule.asmdef и MyExampleModule.Editor.asmdef (для игровой сборки и сборки редактора соответственно).
  • Сборки редактора обязательно должны быть внутри папки Editor, а в Platforms должна быть указана всего одна платформа - Editor
  • Старайтесь продумать иерархию внутри своего модуля таким образом, чтобы папка Editor была всего одна.

Подключаем видимость internal в сборке редактора

Так же есть очень важная деталь, о которой вы возможно не догадаетесь. Дело в том, что сборка редактора - вещь довольно специфичная, и часто в ней могут понадобиться некоторые данные игровой сборки, которые было бы хорошо закрыть от других модулей. Для этого в игровой сборке вы можете использовать уровень доступа internal. Чтобы получить к таким переменным доступ вы должны создать файл AssemblyInfo.cs в корне вашей игровой сборки с похожим содержимым:

AssemblyInfo.cs
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("EngineModule.Editor")]
[assembly: InternalsVisibleTo("EngineModule.Editor.dll")]

Естественно название модуля у вас будет другим. Важно указывать оба InternalsVisibleTo, так как Unity и ваша программная среда требуют этого для корректной работы.

Удаляем лишние файлы из иерархии сборок

Так же, вы можете заметить, что многие файлы, не имеющие отношения к вашей текущей сборке могут отображаться внутри нее - .txt, .shader, .xml и прочие файлы добавляются к каждой сборке.
Чтобы исправить это - скопируйте скрипт, написанный ниже в любую папку Editor вашего проекта. Чтобы изменения подействовали от вас так же потребуется немного ручной работы: удалите все .csproj файлы в корне вашего проекта, а после выберите в Unity пункт Assets/Open C# Project. Это заставит все ваши файлы сгенерироваться с дополнительными изменениями.

SolutionPostprocessor.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;

namespace EngineModule.Editor
{
    public class SolutionPostprocessor : AssetPostprocessor
    {
        private const string c_AssemblyCSharp = "Assembly-CSharp";
        private const string c_AssemblyCSharpFirstpass = "Assembly-CSharp-firstpass";
        private const string c_AssemblyCSharpEditor = "Assembly-CSharp-Editor";
        private const string c_AssemblyCSharpEditorFirstpass = "Assembly-CSharp-Editor-firstpass";
        private static readonly char[] c_Slashes = new [] {'\\', '/'};

        public static readonly bool Enable = true;
        public static readonly bool UseCrossPathForStandartAsseblies = false; //Если true то ищет общий путь для стандартных сборок (которые не asmdef)
        
        public static void OnGeneratedCSProjectFiles()
        {
            if (!Enable)
                return;
            var filenames = Directory.GetFiles(".", "*.csproj", SearchOption.TopDirectoryOnly);
            for (int i = 0; i < filenames.Length; i++)
            {
                FixCsproj(Path.GetFileNameWithoutExtension(filenames[i]));
            }
        }
        
        private static void OnAssemblyCompilationFinished(string s, CompilerMessage[] compilerMessages)
        {
            if (!Enable)
                return;
            var filename = Path.GetFileNameWithoutExtension(s);
            FixCsproj(filename);
        }

        private static void FixCsproj(string csprojName)
        {   
            if (string.IsNullOrEmpty(csprojName))
                return;
            
            var csproj = csprojName + ".csproj";
            if (!File.Exists(csproj))
                return;

            var doc = XDocument.Load(csproj);
            if (doc.Root == null)
                return;

            var allAsmdefPaths = Directory.GetFiles("Assets", "*.asmdef", SearchOption.AllDirectories).Select(Path.GetDirectoryName).ToArray();
            
            var asmdef = Directory.GetFiles("Assets", csprojName + ".asmdef", SearchOption.AllDirectories).FirstOrDefault();
            var asmDefRootDir = Path.GetDirectoryName(asmdef);
            bool isPlugins = csprojName == c_AssemblyCSharpEditorFirstpass || csprojName == c_AssemblyCSharpFirstpass;
            bool isEditor = csprojName == c_AssemblyCSharpEditor || csprojName == c_AssemblyCSharpEditorFirstpass;
            if (asmDefRootDir != null)
            {
                isPlugins |= IsPlugins(asmDefRootDir);
                isEditor |= IsEditor(asmDefRootDir);
            }
            
            var includedPaths = new List<string>();
            var itemGroups = GetElements(doc.Root, "ItemGroup").ToList();
            foreach (var itemGroup in itemGroups)
            {
                if (UseCrossPathForStandartAsseblies && asmDefRootDir == null)
                {
                    var compiles = GetElements(itemGroup, "Compile").ToList();
                    foreach (var compile in compiles)
                    {
                        var includeAttribute = compile.Attribute("Include");
                        if (includeAttribute != null)
                            includedPaths.Add(includeAttribute.Value);
                    }
                }

                var nones = GetElements(itemGroup, "None").ToList();
                
                foreach (var none in nones)
                {
                    var includeAttribute = none.Attribute("Include");
                    if (includeAttribute != null)
                    {
                        if (CanRemove(includeAttribute.Value, asmDefRootDir, isEditor, isPlugins, allAsmdefPaths))
                            none.Remove();
                        else if (UseCrossPathForStandartAsseblies && asmDefRootDir == null)
                            includedPaths.Add(includeAttribute.Value);
                    }
                }
            }
            
            var baseDirBuilder = new StringBuilder();
            if (asmDefRootDir != null)
                baseDirBuilder.Append(asmDefRootDir);
            else if (UseCrossPathForStandartAsseblies && includedPaths.Count > 0)
            {
                var crossString = CrossStarts(includedPaths);
                var crossDirectories = GetDirectories(crossString);
                var crossDirString = string.Join("\\", crossDirectories.ToArray());
                baseDirBuilder.Append(crossDirString);
            }
            else
                baseDirBuilder.Append("Assets");

            var baseDir = baseDirBuilder.ToString();

            var propGroups = GetElements(doc.Root, "PropertyGroup").ToList();
            foreach (var propGroup in propGroups)
            {
                var baseDirectoryProperty = GetElements(propGroup, "BaseDirectory").FirstOrDefault();

                if (baseDirectoryProperty != null)
                {
                    baseDirectoryProperty.Value = baseDir;
                }
            }
            

            doc.Save(csprojName + ".csproj");
        }

        private static string CrossStarts(IList<string> strings)
        {
            if (strings.Count == 0)
                return null;

            var s0 = strings[0];
            int maxLength = s0.Length;
            for (int i = 1; i < strings.Count; i++)
            {
                var length = strings[i].Length;
                if (length < maxLength)
                    maxLength = length;
            }
            
            var result = new StringBuilder();
            for (int i = 0; i < maxLength; i++)
            {
                for (int j = 1; j < strings.Count; j++)
                {
                    var sOther = strings[j];
                    if (s0[i] != sOther[i])
                        return result.ToString();
                }
                result.Append(s0[i]);
            }
            return result.ToString();
        }

        private static List<string> GetDirectories(string path)
        {
            var directories = path.Split(c_Slashes, StringSplitOptions.None).ToList();
            directories.RemoveAt(directories.Count - 1); //remove filename
            return directories;
        }

        private static IEnumerable<XElement> GetElements(XElement parent, string localName)
        {
            var rootElements = parent.Elements();
            foreach (var rootElement in rootElements)
                if (rootElement.Name.LocalName == localName)
                    yield return rootElement;
        }

        private static bool CanRemove(string path, string asmdefRoot, bool isEditor, bool isPlugins, string[] asmdefPaths)
        {
            if (isEditor != IsEditor(path))
            {
                return true;
            }

            if (isPlugins != IsPlugins(path))
            {
                return true;
            }

            if (asmdefRoot != null)
            {
                if (!path.StartsWith(asmdefRoot))
                {
                    return true;
                }
            }

            if (asmdefPaths.Length > 0)
            {
                for (int i = 0; i < asmdefPaths.Length; i++)
                {
                    var asmdefPath = asmdefPaths[i];
                    if ((asmdefRoot == null || !asmdefRoot.StartsWith(asmdefPath)) && asmdefPath != asmdefRoot)
                    {
                        if (path.StartsWith(asmdefPath))
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        private static bool IsPlugins(string path)
        {
            return path.Contains("\\Plugins\\") || path.EndsWith("\\Plugins");
        }

        private static bool IsEditor(string path)
        {
            return path.Contains("\\Editor\\") || path.EndsWith("\\Editor");
        }

        [InitializeOnLoad]
        public static class SolutionInitOnLoad
        {
            static SolutionInitOnLoad()
            {
                CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished;
            }
        }
    }
} 

Репозитории

Как я и обещал, здесь мы коснемся еще одной темы, важной для организации модульной работы - это организация ваших репозиториев. Чаще всего без этого ваши действия выше просто не имеют смысла, так как основной смысл модулей в том, что их можно свободно вытащить из одного проекта в другой.

Мне не хочется разводить воды, ради того чтобы статья казалась больше, так что опишу кратко.

Предполагается что вы умеете создавать репозитории. Если нет - откройте bitbucket, скачайте SourceTree, а дальше само пойдет.

Вы должны создать несколько репозиториев для одного и того же проекта. По одному на каждый модуль и еще один под сам проект. Например, проект MyGame имеет собственный репозиторий, в него входят модули EngineModule, PathfindingModule, InventoryModule. Соответственно 3 репозитория под модули, и один основной => 4 репки.

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

После того как вы затянете себе главный модуль, вы можете подключить к нему другие, сделав из них подмодули. Например в SourceTree это делается через меню "Repository/Add submodule..."

Разбираемся с модулями и Assembly Definition — Unity — DevTribe: Разработка игр (разработка игр, разработка игр)

Все ваши подмодули должны быть расположены где-то внутри папки Assets основного модуля.

Я так же считаю хорошей практикой создавать песочницы - то есть такие главные модули, которые предназначены для тестирования отдельного подмодуля. Например, модуль песочницы PathfindingSandbox, включает себя только подмодули EngineModule и PathfindingModule, то есть только те модули которые нужны чтобы разрабатывать поиск пути. Такие дополнительные песочницы упростят вам тестирование и работу с отдельными модулями, когда их станет больше.



О! Мы как раз юзаем модульную структуру проекта.
Спасибо, что поделился опытом. Как только в порядок приведем процессы, попробуем воспользоваться инструкцией.
Это только на 2017.3 пашет?

ага, 2017.3 и выше. Но выше пока ничего нет.
* На 2017.2 тоже есть, но очень сырое.

Вау, модульную организацию завезли, огонь! За небольшой обзор спасибо!
Остался один не закрытый вопрос...

Так же, вы можете заметить, что многие файлы, не имеющие отношения к вашей текущей сборке могут отображаться внутри нее - .txt, .shader, .xml и прочие файлы добавляются к каждой сборке.

Не особо понял о чем идет речь... это файлы которые мы сами добавляем в модуль и они тащятся в студийный проект?

Это файлы, лежащие в разных папках в Unity. Когда Unity генерит файлы сборок она не особо запаривается и добавляет эти файлы во все сборки сразу.
Список всех форматов, которые затрагиваются подобным можно найти в Edit/Project Settings/Editor:

Предположим, что ты добавил какой-нибудь ReadMe.txt в один из модулей - в этом случае во всех остальных сборках будет отмечаться папка этого модуля с единственным файлом.
Пометил в примере папки, которые не относятся к определенному модулю, но тем не менее отображаются:

После применения скрипта это выглядит вот так:

А если смотреть через MonoDevelop, то там еще приятнее, так как скрипт умеет находить базовую директорию, а монодевелоп умеет их обрабатывать.

Devion, аа, все понял теперь, спасиб!