ruggaraxe ruggaraxe

Jusper, Отлично пишите! :)

Jusper Jusper

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

Jusper Jusper

ruggaraxe,

Да, в этом плане все ок. Логично, что графен на старой машине, если не упарываться, не взлетит. Но я рад, что это было не 5 фпс, как даже в некоторых АА (типа Pillars of Eternity в некоторых схватках...

Jusper Jusper

ruggaraxe,

Подкреплю ее к публикации.

ruggaraxe ruggaraxe

Jusper, вот ссылка на анкету (я затупил со ссылкой с топике, сорри)
https://docs.google.com/forms/d/e/1FAIpQLSd_Wn53lJFrnfGpWI2IX...

ruggaraxe ruggaraxe

Jusper, честно говоря, да на 800х600 даже не проверяли... :) сорри. Ориентировались на FullHD и выше. Хотя над интерфейсом конечно же надо еще хорошенько поработать.
Тултипы постараемся сделать обязательно к следующей версии...

Jusper Jusper

GenElCon,

4x стратегия, понятно.

GenElCon GenElCon

Jusper,

Наверное. В прошлом они сделали Endless Legend - посмотри и сразу станет ясно в какую сторону они работают.

Jusper Jusper

GenElCon,

Я не очень понял по трейлеру геймплей. Это что-то типа цивы? Или это RTS?

GenElCon GenElCon

Humankind от разработчиков Endless Legends (и Space, но тут важно именно Legends).
А также согревающие сердца олдов трейлеры Port Royal 4 и Knights of Honor.

Jusper Jusper

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

Jusper Jusper

Первое, оно же самое тяжелое - UI. Я конечно, понимаю, что 800x600 совсем уже не в моде (завтра проверю на нормальной широформатной машине). Заблюренный текст я еще прочитать могу, но вот конкретно размер его крайне мал...

...
Jusper Jusper

ruggaraxe, я поиграл на старом маке 2012 года (Macbook Pro, Intel HD 4000), рад что с учетом довольно нагруженной по свету и теням картинке игруля не лагает как последняя сволочь (лагает конечно, но очень терпимо...

Jusper Jusper

Вот тут можно посмотреть игровой процесс. Видно, что в Новиграде просаживается FPS.

Jusper Jusper

С учетом тотального количества наигранных на свиче часов, думаю, что именно Switch станет для меня платформой, где я пройду Ведьмака.

Jusper Jusper

alexprey, это первое. Второе это постэффект, которыЙ засвечивает весь песок.

alexprey alexprey

Jusper,

Да, по мне так перебор с интенсивностью освещения

alexprey alexprey

Jusper, в игре используется таже моделька для юнита, только вид сверху)

Jusper Jusper

Пацаны, я опоздал.

Слэш Полигон в эфире.

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

Справка