danilaxxl danilaxxl

CollectableItemData.cs

[CreateMenuItem(fileName = "newItem", menuName = "Data/Items/Collectable", order = 51]

GoloGames GoloGames

vadya_ivan, рад, что вам игра показалась интересной : )

P.S. Кстати уже доступна бесплатная демо-версия в Steam

vadya_ivan vadya_ivan

Визуал, задумка, музыка , механики, все в цель

GoloGames GoloGames

Ato_Ome, спасибо за позитивные эмоции, будем стараться : )

Ato_Ome Ato_Ome

Потрясающий результат, все так четенько, плавненько)
То ли саунд, то ли плавность напомнили мне игрушку World of Goo, удачи вам в разработке и сил побольше дойти до релиза!)

Cute Fox Cute Fox

Graphics are a little cool, good HD content. But this game doesn't cause nary interest me.
However the game is well done.

GMSD3D GMSD3D

Почему действие после всех условий выполняется?
[step another object]

Zemlaynin Zemlaynin

Jusper, Везде, но наугад строить смысла нет. Нужно разведать сперва территорию на наличие ресурсов.

Jusper Jusper

Zemlaynin, а карьеры можно будет везде запихать?
Или под них "особые" зоны будут?

Zemlaynin Zemlaynin

Это так скажем тестовое строительство, а так да у города будет зона влияния которую нужно будет расширять.

Jusper Jusper

А ссылка есть?

Jusper Jusper

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

split97 split97

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

split97 split97

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

ViktorJaguar ViktorJaguar

Почему я нигде не могу найти нормальный туториал, где покажут как экипировать предмет (например, меч) в определенную (выделенную под оружие) ячейку???

Логотип проекта 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, аа, все понял теперь, спасиб!

Справка