Media is too big
VIEW IN TELEGRAM
Вот еще одно небольшое видео, на котором видно, что объект с коллайдером 🤩
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9
Media is too big
VIEW IN TELEGRAM
Тест с большими камушками 🤩
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11
Media is too big
VIEW IN TELEGRAM
Деревья!😧 🙌
Стало намного интереснее) Причем деревья, которые поблизости, являются "призраками" и синхронизируются с копией на сервере. А вот деревья вдалеке - существуют только на клиенте) При приближении они конечно же заменяются на призраки🥔
В комментариях еще скрин🫥
Стало намного интереснее) Причем деревья, которые поблизости, являются "призраками" и синхронизируются с копией на сервере. А вот деревья вдалеке - существуют только на клиенте) При приближении они конечно же заменяются на призраки
В комментариях еще скрин
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥18👍1
Сегодня расскажу про то, что крайне редко кастомизируется - аллокаторы в DOTS! ☺️
Все, кто использует нативные коллекции из Unity.Collections, знакомы с аллокаторами, которые Unity предоставляет по умолчанию. Это Persistent - для объектов с долгим времени жизни, Temp - для однокадровых объектов, TempJob - для объектов, передаваемых в Job. Но мало кто знает, что можно написать свой аллокатор и использовать также, как и дефолтные! (Правда с некоторыми оговорками😔 )
На самом деле Unity предоставляет все необходимое API, чтобы можно было реализовать свой аллокатор и использовать в нативных коллекциях. Все, что нужно сделать, так это реализовать интерфейс
Где
Самое интересное - метод🤭 ) выяснять некоторые детали.
В качестве входного параметра нам приходит
Правила тут такие:
- если
- если
- в ином случае, мы должны освободить память.
А вот что возвращает метод?🤔 Указан возвращаемый тип как
- код
- код
После того, как написали свой аллокатор, его можно использовать, но как? Каждый аллокатор должен возвращать свой🔍
Но в случае с кастомным аллокатором есть некоторые ограничения:
1. Нельзя использовать
2.😒 Чтобы создать NativeArray<> надо использовать
Но это сильно ограничивает, в Job уже не получится задиспоузить массив, созданный таким образом😢
3. Нет поддержки многопоточности! То есть нельзя создать инстанс аллокатора в главном потоке, а потом использовать в других потоках. Причина в том, что на каждый аллокатор также создаются свои safety checks - проверки "от дурака". И🤦
Зачем вообще использовать свой аллокатор? В большинстве задач это не требуется, но иногда, все же может быть лучше свое решение, чем то, что предоставляет Unity. Я использую кастомный аллокатор на базе smmalloc по той причине, что мне в какой-то момент понадобились аллокации, которые быстры как и TempJob/Temp, но и живут больше 4 и 1 кадра соответственно.
Все, кто использует нативные коллекции из Unity.Collections, знакомы с аллокаторами, которые Unity предоставляет по умолчанию. Это Persistent - для объектов с долгим времени жизни, Temp - для однокадровых объектов, TempJob - для объектов, передаваемых в Job. Но мало кто знает, что можно написать свой аллокатор и использовать также, как и дефолтные! (Правда с некоторыми оговорками
На самом деле Unity предоставляет все необходимое API, чтобы можно было реализовать свой аллокатор и использовать в нативных коллекциях. Все, что нужно сделать, так это реализовать интерфейс
AllocatorManager.IAllocator:public struct TestAllocator : AllocatorManager.IAllocator
{
public AllocatorManager.TryFunction Function { get; }
public AllocatorManager.AllocatorHandle Handle { get; set; }
public Allocator ToAllocator { get; }
public bool IsCustomAllocator { get; }
public int Try(ref AllocatorManager.Block block) { }
public void Dispose() { }
}
Где
AllocatorManager.TryFunction Function { get; } - делегат функции int Try(ref AllocatorManager.Block block) { }, которую AllocatorManager вызывает при попытке аллоцировать/реаллоцировать/освободить память.Самое интересное - метод
Try, тут мне пришлось опытным путем (ну как всегда в Unity В качестве входного параметра нам приходит
ref AllocatorManager.Block block, который содержит некоторые детали запроса. А именно сколько элементов и с каким размером и выравниванием надо аллоцировать. Также тут хранится и указатель на память, этот указатель нам и надо заполнить. Правила тут такие:
- если
block.Range.Pointer равен IntPtr.Zero, то мы должны аллоцировать память;- если
block.Range.Pointer не равен IntPtr.Zero и block.Range.Items больше 0, то есть уже раньше аллоцировали этот блок памяти и он валиден, то мы его должны реаллоцировать;- в ином случае, мы должны освободить память.
А вот что возвращает метод?
int, надо же что-то вернуть. А возвращается тут код ошибки. Из того, что я выяснила:- код
0 - все успешно;- код
-1 - операция прошла неуспешно.После того, как написали свой аллокатор, его можно использовать, но как? Каждый аллокатор должен возвращать свой
AllocatorHandle - это всего лишь указатель на запись во внутренней таблице аллокаторов, по которой Unity ищет указатель на функцию аллокатора. Кстати, Allocator.Persistent/Temp/TempJob - также неявно конвертируются в AllocatorHandle. Но в случае с кастомным аллокатором есть некоторые ограничения:
1. Нельзя использовать
UnsafeUtility.Malloc/Free, вместо этого необходимо использовать AllocatorManager.Allocator/Free.(T*)AllocatorManager.Allocate(handle, itemSizeInBytes, alignmentInBytes, length);
AllocatorManager.Free(handle, pointer, length);
2.
NativeArray<> не поддерживает кастомный аллокатор! Причина в том, что он написан для использования только enum Allocator CollectionHelper.CollectionHelper.CreateNativeArray<T, TestAllocator>(length, ref allocator, options);
CollectionHelper.Dispose(array);
Но это сильно ограничивает, в Job уже не получится задиспоузить массив, созданный таким образом
3. Нет поддержки многопоточности! То есть нельзя создать инстанс аллокатора в главном потоке, а потом использовать в других потоках. Причина в том, что на каждый аллокатор также создаются свои safety checks - проверки "от дурака". И
SafetyHandle внутри AllocatorManager хранятся в UnsafeList, который не является потокобезопасным. Опять же недоработка от Unity.Зачем вообще использовать свой аллокатор? В большинстве задач это не требуется, но иногда, все же может быть лучше свое решение, чем то, что предоставляет Unity. Я использую кастомный аллокатор на базе smmalloc по той причине, что мне в какой-то момент понадобились аллокации, которые быстры как и TempJob/Temp, но и живут больше 4 и 1 кадра соответственно.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9❤6🤯2❤🔥1👍1
Разбираюсь сейчас со спайками, и немного углубляюсь в unsafe мир ☺️
Вот что интересного выяснила: память желательно выравнивать! Вот да, недостаточно просто выделить кусок памяти, желательно выровнять его по размеру кэшлайна. Разница - огромная!😧
Размер кэшлайна в Job System можно узнать через константу
А вот выровнять можно через CollectionHelper:
Где T - ваш тип данных. Достаточно просто, а разница - на скриншоте)
Вот что интересного выяснила: память желательно выравнивать! Вот да, недостаточно просто выделить кусок памяти, желательно выровнять его по размеру кэшлайна. Разница - огромная!
Размер кэшлайна в Job System можно узнать через константу
JobsUtility.CacheLineSize (64 байта).А вот выровнять можно через CollectionHelper:
// Считаем сколько памяти (в байтах) нам нужно, заодно выравниваем по кэшлайну, чтобы было кратко CacheLineSize
var someDataSize = CollectionHelper.Align(UnsafeUtility.SizeOf<T> * length, JobsUtility.CacheLineSize);
// Выделяем (аллоцируем) память по посчитанному размеру и указываем, что выравниваем по CacheLineSize
var someData = (byte*)AllocatorManager.Allocate(allocatorHandle, someDataSize, JobsUtility.CacheLineSize);
Где T - ваш тип данных. Достаточно просто, а разница - на скриншоте)
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥20❤🔥2❤2👌2🦄2
Мехмат ЮФУ (Южный Федеральный Университет) не так давно взял у меня интервью, и вот оно вышло 🥔
https://t.me/igromech/85
https://t.me/igromech/85
Please open Telegram to view this post
VIEW IN TELEGRAM
Telegram
ИгроМех
Иногда канал в телеграме выглядит так классно, что нельзя пройти мимо. Вот и наш @xivol настолько заинтересовался, что взял интервью у Елизаветы, которая ведет канал @infinity_world_developer_diary
По ссылке вы найдете обстоятельный разговор о процедурной…
По ссылке вы найдете обстоятельный разговор о процедурной…
🔥17👍7❤4🥰2
Что-то давно я не писала, ох уж эти выходные 🤭
Продолжаю избавляться от спаек в рантайме, много экспериментов, проверок теорий и различных исправлений. Некоторые моменты приходится вообще переосмыслить, так как оказалось, что я выбрала неправильное направление. Но это нормально, и главное тут не унывать🫥
Вместе с Profiler пользуюсь Profile Analyzer, который очень сильно помогает анализировать конкретные маркеры, и даже сравнивать их между несколькими снапшотами. Так что, очень советую этот инструмент взять на заметку, если еще не взяли🥔
Только учтите, что время там показывается на кадр) То есть, например, медиана - за кадр + кол-во кадров, в которых этот маркер есть. Я так долго думала, почему запись в память (пусть и random access) занимает 1.4 мс😄 А это 200к операций в кадр оказалось) Глупо получилось конечно 🤪
Что же касается своих маркеров, то все очень просто.
Шаг 1. Создаете сам маркер, желательно статик:
Шаг 2. Используете маркер
Шаг 3. Находим наш маркер в анализаторе по названию, который указали в конструкторе маркера.
Продолжаю избавляться от спаек в рантайме, много экспериментов, проверок теорий и различных исправлений. Некоторые моменты приходится вообще переосмыслить, так как оказалось, что я выбрала неправильное направление. Но это нормально, и главное тут не унывать
Вместе с Profiler пользуюсь Profile Analyzer, который очень сильно помогает анализировать конкретные маркеры, и даже сравнивать их между несколькими снапшотами. Так что, очень советую этот инструмент взять на заметку, если еще не взяли
Только учтите, что время там показывается на кадр) То есть, например, медиана - за кадр + кол-во кадров, в которых этот маркер есть. Я так долго думала, почему запись в память (пусть и random access) занимает 1.4 мс
Что же касается своих маркеров, то все очень просто.
Шаг 1. Создаете сам маркер, желательно статик:
private static readonly ProfilerMarker s_SomeProfilerMarker = new("Some Marker");Шаг 2. Используете маркер
// Первый вариант использования
using(s_SomeProfilerMarker.Auto())
{
// тут какой-то код, который мы хотим оценить
}
// Второй вариант использования
s_SomeProfilerMarker.Begin();
// тут какой-то код, который мы хотим оценить
s_SomeProfilerMarker.End();
Шаг 3. Находим наш маркер в анализаторе по названию, который указали в конструкторе маркера.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8🔥5🥰2👍1
Не задача с собеседования, но все же может быть интересно ☺️
Вопрос звучит вот так: будет ли валидно состояние outJobHandle? Если да, то почему?🔍
Вопрос звучит вот так: будет ли валидно состояние outJobHandle? Если да, то почему?
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔5🥴5👍2
Пока все еще решаю проблемы спаек, пришлось переделать всю систему генерации. Зато она теперь вся (почти) под Burst. Шаги генерации теперь тоже unmanaged и под Burst, а не только Jobs.
Но вот пост-процессоры чанков, которые создают отображение данных чанков в сцене все также остались managed типами. От этого не убежать никуда пока что😔 Поэтому тут приходится не просто углубляться в unmanaged часть, проектировать то, как данные должны лежать, как к ним должен совершаться доступ и многое другое. Но и приходится связывать managed и unmanaged части.
Главная проблема тут - Burst: у него множество ограничений, но при этом очень хочется его использовать везде, где только возможно. Но что делать, если в unmanaged и burstable типе вдруг очень нужен managed тип? Вот прям совсем нужен.🔍
Если это свойство или метод, то можно пометить их атрибутом
А если нужен прям инстанс целого класса? Вот тут решение немного посложнее. Такие инстансы лучше хранить где-нибудь отдельно, не в нашем красивом unmanaged и burstable типе, потому что в ином случае, он сразу перестанет быть таковым. Как один из вариантов - статика для managed, а в нашем burstable типе - просто какой-нибудь идентификатор. И для удобства свойство с атрибутом
Как пример пвседокода ниже:
Но вот пост-процессоры чанков, которые создают отображение данных чанков в сцене все также остались managed типами. От этого не убежать никуда пока что
Главная проблема тут - Burst: у него множество ограничений, но при этом очень хочется его использовать везде, где только возможно. Но что делать, если в unmanaged и burstable типе вдруг очень нужен managed тип? Вот прям совсем нужен.
Если это свойство или метод, то можно пометить их атрибутом
ExcludeFromBurstCompatTesting указав причину, почему Burst не может компилировать этот кусок кода. Да, Burst пропустит это свойство или метод, удобненько. Правда и обращаться к свойству/методу нельзя будет из под Burst. Поэтому такой вариант больше для логического объединения под одним типом.А если нужен прям инстанс целого класса? Вот тут решение немного посложнее. Такие инстансы лучше хранить где-нибудь отдельно, не в нашем красивом unmanaged и burstable типе, потому что в ином случае, он сразу перестанет быть таковым. Как один из вариантов - статика для managed, а в нашем burstable типе - просто какой-нибудь идентификатор. И для удобства свойство с атрибутом
ExcludeFromBurstCompatTesting.Как пример пвседокода ниже:
public struct SomeBurstableStruct
{
private static SomeManagedType[] s_Instances;
private int m_ManagedInstanceId;
[ExcludeFromBurstCompatTesting("Takes managed types")]
public SomeManagedType ManagedInstance
{
get => s_Instances[m_ManagedInstanceId];
}
}
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🌚2
Generic типы и FunctionPointer
Иногда не задумываешься о каких-то вещах, пока эта самая вещь тебя не стукнет по голове🤭
При работе с FunctionPointer'ами, в один солнечный денек, я столкнулась с тем, что при вызове делегата я получаю краш! Причем получаю в 100% случаях. Делегат вызывается вот так:
Первая мысль - делегат не живой, но дебаг показал, что поинтер живее всех живых. Вторая мысль - потеряла атрибуты соглашений о вызове. Мало ли где-то вызываю не через Cdecl🔍 Но вот проблема, все правильно. Тогда в чем же причина краша? 🤔
В логах я вижу вот такое:
Что конечно же никак не помогает. Функция не доступна? Но поинтер же живой! А давайте проверим, будет ли делегат работать через самый обычный Invoke?
И вот так все работает! Удивительно. То есть делегат точно живой, а причина куда глубже.😔
Во время дальнейших исследований, в один момент, я заметила, что на краш влияет то, откуда я вызываю делегат!
🔸 если я вызываю из значимого дженерик типа (struct SomeType<T> where T : unmanaged), то краш будет в 100% случаях;
🔸 если я вызываю из обычного типа (struct/class SomeType), то краша уже не будет.
Поразбиравшись (и пообщавшись с AI) я выяснила, что и правда, дженерик может повлиять на вызов делегата!🗣 И виновник тут - JIT компилятор, который определенным образом работает с дженерик типами:
🔹 если дженерик параметр - ссылочный тип, то JIT создает одну версию для всех конкретных типов параметра;
🔹 есди дженерик параметр - значимый тип, то JIT создает под каждый конкретный тип свою версию дженерик типа.
Почему так?
Потому что инстансы значимых типов влияют на layout типа, в котором они находятся. Все мы знаем, что инстанс значимого типа находится или на стеке (отдельно или как часть другого значимого типа) или в куче как часть ссылочного типа.
Вот и получается, что значимый дженерик параметр, даже если он выступает просто как ограничитель, может повлиять на layout нашего изначального типа.
И так как для вызова FunctionPointer надо точно знать адрес не только метода, который мы вызываем, но и точный адрес откуда вызываем, то при попытке сделать это, мы можем получить краш.
Решение
Их тут может быть несколько:
1. Избавиться от generic параметра.
2. Вынести вызов FunctionPointer в отдельный тип без дженериков, можно даже просто статик.
Note: забыла написать, что это правдиво для JIT (Mono), тогда как для IL2CPP скорее всего такой проблемы нет (не тестировала, но там AOT все таки)
Иногда не задумываешься о каких-то вещах, пока эта самая вещь тебя не стукнет по голове
При работе с FunctionPointer'ами, в один солнечный денек, я столкнулась с тем, что при вызове делегата я получаю краш! Причем получаю в 100% случаях. Делегат вызывается вот так:
((delegate * unmanaged[Cdecl] <void>)someFunctionPointer.Value)();
Первая мысль - делегат не живой, но дебаг показал, что поинтер живее всех живых. Вторая мысль - потеряла атрибуты соглашений о вызове. Мало ли где-то вызываю не через Cdecl
В логах я вижу вот такое:
0x000002299F7F03A6 ((<unknown>)) (function-name not available)
Что конечно же никак не помогает. Функция не доступна? Но поинтер же живой! А давайте проверим, будет ли делегат работать через самый обычный Invoke?
someFunctionPointer.Invoke.Invoke();
И вот так все работает! Удивительно. То есть делегат точно живой, а причина куда глубже.
Во время дальнейших исследований, в один момент, я заметила, что на краш влияет то, откуда я вызываю делегат!
🔸 если я вызываю из значимого дженерик типа (struct SomeType<T> where T : unmanaged), то краш будет в 100% случаях;
🔸 если я вызываю из обычного типа (struct/class SomeType), то краша уже не будет.
Поразбиравшись (и пообщавшись с AI) я выяснила, что и правда, дженерик может повлиять на вызов делегата!
🔹 если дженерик параметр - ссылочный тип, то JIT создает одну версию для всех конкретных типов параметра;
🔹 есди дженерик параметр - значимый тип, то JIT создает под каждый конкретный тип свою версию дженерик типа.
Почему так?
Потому что инстансы значимых типов влияют на layout типа, в котором они находятся. Все мы знаем, что инстанс значимого типа находится или на стеке (отдельно или как часть другого значимого типа) или в куче как часть ссылочного типа.
Вот и получается, что значимый дженерик параметр, даже если он выступает просто как ограничитель, может повлиять на layout нашего изначального типа.
И так как для вызова FunctionPointer надо точно знать адрес не только метода, который мы вызываем, но и точный адрес откуда вызываем, то при попытке сделать это, мы можем получить краш.
Решение
Их тут может быть несколько:
1. Избавиться от generic параметра.
2. Вынести вызов FunctionPointer в отдельный тип без дженериков, можно даже просто статик.
Note: забыла написать, что это правдиво для JIT (Mono), тогда как для IL2CPP скорее всего такой проблемы нет (не тестировала, но там AOT все таки)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13❤3❤🔥2🤩2
Кастомные Jobs
В процессе пересмотра концептов генерации мне понадобились свои типы джобов.🔍 Да, Unity предоставляет все необходимое API, чтобы создавать не просто свои джобы, но и свои типы джоб! Иногда (редко) это бывает очень нужно.
Зачем нужны кастомные типы джоб?
1️⃣ В первую очередь, это уменьшает дублирование кода. Допустим у вас появилась какая-то своя коллекция, которая требует определенного алгоритма итерирования. И, чтобы не писать везде этот алгоритм, можно использовать кастомную джобу.
2️⃣ Во-вторых, это дает больше контроля. Появляется возможность решать что и как, и в каком порядке обрабатывать. Хотите параллельно? Такое возможно. Хотите работать с определенными данными в джобе через параметры? Тоже можно.
3️⃣ В-третьих, это отличная возможность усложнить упростить себе жизнь. Джобы - хороший инструмент для создания цепочек вызовов (прямо как реактивщина), но из-за ограничений unmanaged мира, не все можно реализовать. Кастомные джобы частично помогут решить эту проблему.
Как создавать?
Немного кода, немного бойлерплейта,соли сверху 🧂 и готово) Но кода и правда мало.
🔸 Самое главное описать интерфейс, который все конкретные реализации вашей джобы будут реализовывать.
Интересное тут - атрибут
А в Execute могут разные параметры на ваш вкус.
🔶 Реализовать ту самую структуру с магией:
В которой также должен быть метод Execute, вот только параметры в нем меняться не могут.
Этот метод вызывается уже самими потоками, когда они работают с джобой. Наша главная задача тут - написать логику работы джобы. Где-то это может быть простой вызов Execute, как в примере выше; где-то же - итерирование по коллекции и передача элементов в Execute через параметры; где-то - сложные вычисления со сложными условиями, то есть все, чего требуют задачи для их решения, может быть тут.
🔶 В
Фактически тут надо передать тип нашей структуры-обработчика, тип интерфейса нашей джобы и указатель на метод Execute, чтобы он потом вызывался.
🔶 Чтобы зашедулить наши магические джобы, необходимо получить параметры для шедулинга:
Где также указывается и режим выполнения. Он может быть Single - один поток (не обязательно main), Parallel - множество потоков, и Run - только тот поток, откуда вызываем джобу (опять же не обязательно main).
А дальше остается только вызывать Schedule и все, джоба улетела выполнять свои дела.
Совет: отправку джобы планировщику можно оформить в виде метода-расширения.
Ссылочки с примерами
- пример с single thread джобой вот тут
- пример с parallel thread джобой вот тут
В процессе пересмотра концептов генерации мне понадобились свои типы джобов.
Зачем нужны кастомные типы джоб?
1️⃣ В первую очередь, это уменьшает дублирование кода. Допустим у вас появилась какая-то своя коллекция, которая требует определенного алгоритма итерирования. И, чтобы не писать везде этот алгоритм, можно использовать кастомную джобу.
2️⃣ Во-вторых, это дает больше контроля. Появляется возможность решать что и как, и в каком порядке обрабатывать. Хотите параллельно? Такое возможно. Хотите работать с определенными данными в джобе через параметры? Тоже можно.
3️⃣ В-третьих, это отличная возможность
Как создавать?
Немного кода, немного бойлерплейта,
🔸 Самое главное описать интерфейс, который все конкретные реализации вашей джобы будут реализовывать.
[JobProducerType(typeof(JobCustomSingleExtensions.JobCustomSingleStruct<>))]
public interface IJobCustomSingle
{
void Execute();
}
Интересное тут - атрибут
JobProducerType, задача которого указать на структуру, в которой происходит вся магия.А в Execute могут разные параметры на ваш вкус.
🔶 Реализовать ту самую структуру с магией:
internal struct JobCustomSingleStruct<TJob>
where TJob : struct, IJobCustomSingle
В которой также должен быть метод Execute, вот только параметры в нем меняться не могут.
public static void Execute(ref JobCustomSingleStruct<TJob> fullData, IntPtr additionalPtr, IntPtr bufferRangePatchData,ref JobRanges ranges, int jobIndex)
{
fullData.JobData.Execute();
}
Этот метод вызывается уже самими потоками, когда они работают с джобой. Наша главная задача тут - написать логику работы джобы. Где-то это может быть простой вызов Execute, как в примере выше; где-то же - итерирование по коллекции и передача элементов в Execute через параметры; где-то - сложные вычисления со сложными условиями, то есть все, чего требуют задачи для их решения, может быть тут.
🔶 В
JobCustomSingleStruct также должен быть метод инициализации, который знает как создавать данные для рефлексии.[BurstDiscard]
internal static void Initialize()
{
jobReflectionData.Data = JobsUtility.CreateJobReflectionData(typeof(JobCustomSingleStruct<TJob>),typeof(TJob), (ExecuteJobFunction)Execute);
}
Фактически тут надо передать тип нашей структуры-обработчика, тип интерфейса нашей джобы и указатель на метод Execute, чтобы он потом вызывался.
🔶 Чтобы зашедулить наши магические джобы, необходимо получить параметры для шедулинга:
var scheduleParams = new JobsUtility.JobScheduleParameters(UnsafeUtility.AddressOf(ref fullData),GetReflectionData<TJob>(), dependsOn, ScheduleMode.Single);
Где также указывается и режим выполнения. Он может быть Single - один поток (не обязательно main), Parallel - множество потоков, и Run - только тот поток, откуда вызываем джобу (опять же не обязательно main).
А дальше остается только вызывать Schedule и все, джоба улетела выполнять свои дела.
JobsUtility.Schedule(ref scheduleParams);
Совет: отправку джобы планировщику можно оформить в виде метода-расширения.
Ссылочки с примерами
- пример с single thread джобой вот тут
- пример с parallel thread джобой вот тут
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15👍9❤4
Небольшой пост заметка про виртуальную память в Unity.
Что такое виртуальная память?🔍 Если совсем кратко и грубо, то это память, которая зарезервирована процессом. Причем где конкретно эта память находится физически - нас не интересует, мы оперируем виртуальным адресным пространством, и можем даже выделить больше памяти, чем нам доступно на самом деле (например за счет файла подкачки).
Понимание того, как устроена оперативная память, как она выделяется, какие этапы выделения есть, нууу, наверное настолько узкоспециализированная информация, что в геймдеве особо и не задумываешься об этом. Пока не сталкиваешься с определенной "магией", после чего приходится разбираться, хотя бы кратко и поверхностно, чтобы еще один пазлик в голове начал складываться.😤
Вот и я столкнулась.😔 Выделяла себе кусочки памяти, где-то инициализировала, где-то нет. В целом никакого подвоха тут не ожидала. А что могло пойти не так? При аллоцировании мне Unity выдает кусочек из своего адресного пространства, скорее всего уже потроганную кем-то. Это все вроде как занимает доли миллисекунд.
Но настал тот момент, когда мне понадобилось выделить 2 Гб. Причем сразу, да и еще с частичной инициализацией. И получила я память, которая просто зарезервирована, но фактически еще не используется. И если malloc сам по себе быстрый, то вот любые операции с такой памятью - долго и больно.😧
Почему так? Потому что фактическое выделение памяти происходит именно во время первого обращения. То, что мы там зарезервировали кусок - он вроде бы и есть, а вроде бы и нет, такой кусок Шредингера. А вот начали писать - получите и распишитесь, вам прислали память. И все бы ничего, это даже хорошо, что нагрузка распределяется во времени, но не когда мы сразу выделяем и инициализируем эту память, получая спайку.
Как решать?
- подготавливать память заранее для конкретных задач, где это критично;
- не инициализировать память, если это возможно.
Note: На скриншоте 2.04 мс занимает memset, что очень печально, так как суммарные цифры достигают 20мс на кадр.
Что такое виртуальная память?
Понимание того, как устроена оперативная память, как она выделяется, какие этапы выделения есть, нууу, наверное настолько узкоспециализированная информация, что в геймдеве особо и не задумываешься об этом. Пока не сталкиваешься с определенной "магией", после чего приходится разбираться, хотя бы кратко и поверхностно, чтобы еще один пазлик в голове начал складываться.
Вот и я столкнулась.
Но настал тот момент, когда мне понадобилось выделить 2 Гб. Причем сразу, да и еще с частичной инициализацией. И получила я память, которая просто зарезервирована, но фактически еще не используется. И если malloc сам по себе быстрый, то вот любые операции с такой памятью - долго и больно.
Почему так? Потому что фактическое выделение памяти происходит именно во время первого обращения. То, что мы там зарезервировали кусок - он вроде бы и есть, а вроде бы и нет, такой кусок Шредингера. А вот начали писать - получите и распишитесь, вам прислали память. И все бы ничего, это даже хорошо, что нагрузка распределяется во времени, но не когда мы сразу выделяем и инициализируем эту память, получая спайку.
Как решать?
- подготавливать память заранее для конкретных задач, где это критично;
- не инициализировать память, если это возможно.
Note: На скриншоте 2.04 мс занимает memset, что очень печально, так как суммарные цифры достигают 20мс на кадр.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15❤5🔥3🤯2👏1
Давно не писала 🤦 Завалена багами, которые зачем-то наделала. Может кто-нибудь знает, зачем мы, программисты, делаем баги? Уже раздражает даже 🤪
Вот и сейчас, разбираюсь, почему у меня швы не бесшовные, хотя в коде выглядят именно такими😔
А так, что за последний месяц было сделано (пока без точных цифр):
- переписала генератор вокселей под Burst, стало существенно шустрее, спаек почти нет;
- перевела почти всю генерацию на новую систему;
- пофиксила множество багов и недоработок, остались швы только;
- воксельный мир стал проще, так как многие моменты были пересмотрены;
- воксели теперь генерируются на GPU, что уменьшило время генерации на 90% сразу)
Зачем все это? Затем, что я столкнулась с жесткими спайками - до 60мс на кадр! Основная проблема - неправильно выбранная архитектура генератора, из-за чего распределение задач генерации по чанкам не давала нужный эффект. Ну и вычислительная сложность высокая, поэтому частичный переход на GPU - стоит того.
Вот и сейчас, разбираюсь, почему у меня швы не бесшовные, хотя в коде выглядят именно такими
А так, что за последний месяц было сделано (пока без точных цифр):
- переписала генератор вокселей под Burst, стало существенно шустрее, спаек почти нет;
- перевела почти всю генерацию на новую систему;
- пофиксила множество багов и недоработок, остались швы только;
- воксельный мир стал проще, так как многие моменты были пересмотрены;
- воксели теперь генерируются на GPU, что уменьшило время генерации на 90% сразу)
Зачем все это? Затем, что я столкнулась с жесткими спайками - до 60мс на кадр! Основная проблема - неправильно выбранная архитектура генератора, из-за чего распределение задач генерации по чанкам не давала нужный эффект. Ну и вычислительная сложность высокая, поэтому частичный переход на GPU - стоит того.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12🔥8🥰1
Unity никогда не перестает удивлять, и иногда даже бесить)
Вот есть пакет Entities из DOTS, есть еще один пакет - Entities.Graphics, который предоставляет все необходимое для отрисовки через ECS без GameObject и компонентов MeshRenderer/MeshFilter.
Сама отрисовка происходит через самый обыкновенный BRG, с моей же стороны надо только предоставить информацию что рендерить и как это рендерить. Звучит просто, правда же?🔍
Если посмотреть документацию, то можно увидеть, что Unity предлагает использовать RenderMeshArray, если хочется в рантайме создавать сущности, без запечки. И раньше я так и делала, но так как этот компонент содержит два managed массива - один для мешей, другой для материалов, а чанки у меня постоянно создаются/удаляются/обновляются, то приходится пересоздавать эти массивы. Что очень дорого. Я уже не говорю про трекинг всех этих данных так, чтобы хотя бы индексы не менялись в этих массивах у тех чанков, которые не изменялись. А еще под Burst не запихнешь, и в Job System не используешь из-за managed кода. Короче, одни проблемы.😤
Почему я взялась искать другой путь? Потому что все эти managed массивы, пересчет хэша у RenderMeshArray, так как это SharedComponent, куча работы вокруг - это все дорого: 3-5-10 мс на обновление 30 чанков! И это за кадр! Интересно, откуда у меня спайки?🤔
Вначале я ввела spatial hashing для чанков, чтобы обновлять RenderMeshArray per chunk group, а не каждый раз для всех. Это сильно помогло, в худшем случае конечно все равно происходил перерасчет массивов и их хэша для всех групп, но в среднем - нагрузка снизилась в 10 раз.
Дальше мне подсказали, что если читать документацию не по диагонали, то можно заметить несколько строчек про компонент MaterialMeshInfo, который может принимать BatchMeshID и BatchMaterialID из BRG.🗣 То есть можно полностью избавиться от managed массивов и всего остального вокруг этого. Ура!
API тут простое:
- получаем систему EntitiesGraphicsSystem
- регистрируем наш меш через метод RegisterMesh и получаем BatchMeshID
- регистрируем материал через метод RegisterMaterial и получаем BatchMaterialID
- указываем эти id в компоненте MaterialMeshInfo и радуемся жизни
Что может пойти не так? Спросите вы. Тут 3, ладно, 4 строчки кода, если учитывать SetComponent, невозможно же запутаться? Но Unity имела на этот счет свое мнение🤭
Я использовала пакеты Entities, Entities.Graphics версии 1.0.16. И как минимум в этих версиях, данный функционал не работает! Я просто получала очень странную отрисовку мешей, чанки почему-то располагались в рандомных местах, все мигало, частично двигалось за камерой, а через несколько секунд Unity вылетала. По логам было понятно, что проблема в BRG - меши и материалы куда-то утекали, Id становились недействительными и все становилось грустно.
Решение? Хорошо, что есть новые версии пакетов) Решение оказалось простым - переход на версии 1.2.1, заодно и переход на Unity 2023.2.20. И все заработало как ожидалось, даже без изменений кода🥔
Что же касается циферок: медиана стала 0.03 мс против 3 мс ( в самом лучшем случае)! Минус еще одна проблема, маленькая победа)
Вот есть пакет Entities из DOTS, есть еще один пакет - Entities.Graphics, который предоставляет все необходимое для отрисовки через ECS без GameObject и компонентов MeshRenderer/MeshFilter.
Сама отрисовка происходит через самый обыкновенный BRG, с моей же стороны надо только предоставить информацию что рендерить и как это рендерить. Звучит просто, правда же?
Если посмотреть документацию, то можно увидеть, что Unity предлагает использовать RenderMeshArray, если хочется в рантайме создавать сущности, без запечки. И раньше я так и делала, но так как этот компонент содержит два managed массива - один для мешей, другой для материалов, а чанки у меня постоянно создаются/удаляются/обновляются, то приходится пересоздавать эти массивы. Что очень дорого. Я уже не говорю про трекинг всех этих данных так, чтобы хотя бы индексы не менялись в этих массивах у тех чанков, которые не изменялись. А еще под Burst не запихнешь, и в Job System не используешь из-за managed кода. Короче, одни проблемы.
Почему я взялась искать другой путь? Потому что все эти managed массивы, пересчет хэша у RenderMeshArray, так как это SharedComponent, куча работы вокруг - это все дорого: 3-5-10 мс на обновление 30 чанков! И это за кадр! Интересно, откуда у меня спайки?
Вначале я ввела spatial hashing для чанков, чтобы обновлять RenderMeshArray per chunk group, а не каждый раз для всех. Это сильно помогло, в худшем случае конечно все равно происходил перерасчет массивов и их хэша для всех групп, но в среднем - нагрузка снизилась в 10 раз.
Дальше мне подсказали, что если читать документацию не по диагонали, то можно заметить несколько строчек про компонент MaterialMeshInfo, который может принимать BatchMeshID и BatchMaterialID из BRG.
API тут простое:
- получаем систему EntitiesGraphicsSystem
- регистрируем наш меш через метод RegisterMesh и получаем BatchMeshID
- регистрируем материал через метод RegisterMaterial и получаем BatchMaterialID
- указываем эти id в компоненте MaterialMeshInfo и радуемся жизни
Что может пойти не так? Спросите вы. Тут 3, ладно, 4 строчки кода, если учитывать SetComponent, невозможно же запутаться? Но Unity имела на этот счет свое мнение
Я использовала пакеты Entities, Entities.Graphics версии 1.0.16. И как минимум в этих версиях, данный функционал не работает! Я просто получала очень странную отрисовку мешей, чанки почему-то располагались в рандомных местах, все мигало, частично двигалось за камерой, а через несколько секунд Unity вылетала. По логам было понятно, что проблема в BRG - меши и материалы куда-то утекали, Id становились недействительными и все становилось грустно.
Решение? Хорошо, что есть новые версии пакетов) Решение оказалось простым - переход на версии 1.2.1, заодно и переход на Unity 2023.2.20. И все заработало как ожидалось, даже без изменений кода
Что же касается циферок: медиана стала 0.03 мс против 3 мс ( в самом лучшем случае)! Минус еще одна проблема, маленькая победа)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20👏9😎5❤3😁1
Еще одна проблема решена: планирование задач в Job System занимало очень много времени.
Я планировала примерно по 150-200 задач на каждом шаге генерации за кадр. Это все занимало значительно время: от 1 мс до 3 мс на кадр за шаг. В совокупности - до 15-20 мс за кадр!🗣 Что конечно же вызывало спайки.
Как такое решать? Это же просто планирование задач!🤷
Решение: так как планирование задач нельзя запихнуть под Burst (не поддерживается), то остается только позапихивать все задачи в одну, то есть вместо планирование 32 задач одинакового типа, но с разными данными, планировать одну задачу, а данные объединить в массив данных. 32 - это количество чанков в шаге.
Но в данном решении есть несколько нюансов, из-за которых я сразу не сделала таким образом. Самый главный нюанс - консистентность хранилища состояния мира и сгенерированных данных для чанков. Каждый шаг - выполняется параллельно и не связан с другими шагами, пишет/читает разные данные, но хранилище то одно. А что делать, если реаллок? А что делать, если мы в каком-то шаге пишем в джобах параллельно, а в другом, параллельном шаге, читаем это хранилище в одном потоке?
Связывать шаги не хотелось, так как во-первых, тогда появляется проблема загрузки потоков Job System - я просто не загружу их адекватно при последовательном выполнении, а во-вторых, проблема не исчезает, если в первом шаге мы запланировали (и фактически запустили) джобы с параллельной записью, а во втором шаге только-только приступили к чтению хранилища в Main Thread.
Что же делать?🤔 Я пришла к идее разделения процессов записи и чтения. Учитывая, что каждый шаг читает/пишет данные по непересекающимся с другими шагами чанками, и это гарантируется на уровне моего планировщика чанков, то можно выделить локальный контекст под каждый шаг, в который мы спокойно можем писать наши данные так, как нам удобно: в джобах, параллельно, последовательно, в меин треде. А вот читать - из глобального контекста.
Остается только потом мигрировать данные из локальных контекстов всех шагов в глобальный контекст в главном потоке. Это происходит после выполнения всех шагов, когда максимально безопасно менять глобальный контекст.
Что дало такое усложнение? Я решила одну из проблем, по которой происходили спайки. Миграция занимает тысячные/сотые доли мс, и выполняется хоть и на Main Thread, но под Burst. А позволив перенести всю работу с хранилищем состояния мира генерации на Job System, я почти полностью освободила главный поток, заодно сверху как обычно Burst работает.
На скриншоте видно, на сколько уменьшилось время выполнения одного из шагов генерации.
Я планировала примерно по 150-200 задач на каждом шаге генерации за кадр. Это все занимало значительно время: от 1 мс до 3 мс на кадр за шаг. В совокупности - до 15-20 мс за кадр!
Как такое решать? Это же просто планирование задач!
Решение: так как планирование задач нельзя запихнуть под Burst (не поддерживается), то остается только позапихивать все задачи в одну, то есть вместо планирование 32 задач одинакового типа, но с разными данными, планировать одну задачу, а данные объединить в массив данных. 32 - это количество чанков в шаге.
Но в данном решении есть несколько нюансов, из-за которых я сразу не сделала таким образом. Самый главный нюанс - консистентность хранилища состояния мира и сгенерированных данных для чанков. Каждый шаг - выполняется параллельно и не связан с другими шагами, пишет/читает разные данные, но хранилище то одно. А что делать, если реаллок? А что делать, если мы в каком-то шаге пишем в джобах параллельно, а в другом, параллельном шаге, читаем это хранилище в одном потоке?
Связывать шаги не хотелось, так как во-первых, тогда появляется проблема загрузки потоков Job System - я просто не загружу их адекватно при последовательном выполнении, а во-вторых, проблема не исчезает, если в первом шаге мы запланировали (и фактически запустили) джобы с параллельной записью, а во втором шаге только-только приступили к чтению хранилища в Main Thread.
Что же делать?
Остается только потом мигрировать данные из локальных контекстов всех шагов в глобальный контекст в главном потоке. Это происходит после выполнения всех шагов, когда максимально безопасно менять глобальный контекст.
Что дало такое усложнение? Я решила одну из проблем, по которой происходили спайки. Миграция занимает тысячные/сотые доли мс, и выполняется хоть и на Main Thread, но под Burst. А позволив перенести всю работу с хранилищем состояния мира генерации на Job System, я почти полностью освободила главный поток, заодно сверху как обычно Burst работает.
На скриншоте видно, на сколько уменьшилось время выполнения одного из шагов генерации.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9👍2😱1
This media is not supported in your browser
VIEW IN TELEGRAM
Переношу (и заодно тестирую) шум на GPU. В качестве базы все также использую библиотеку FastNoise, но уже ее версию на HLSL. К этой библиотеке пришлось дописывать некоторые очень нужные функции (в основном касается примитивных шумов + фильтрации) + перенесла то, что у меня было на C#.
Так как шум (а потом и SDF) считается на GPU (ComputeShaders), то можно использовать уже посложнее алгоритмы☺️ Например, добавила поддержку генерации систем пещер 😧
#generation
Так как шум (а потом и SDF) считается на GPU (ComputeShaders), то можно использовать уже посложнее алгоритмы
#generation
Please open Telegram to view this post
VIEW IN TELEGRAM
❤11🔥5👍3
Внезапно обнаружила, что в библиотеке FastNoise, о которой рассказывала раньше, фрактальный шум (Fbm, Ridged, Ping-Pong) не совсем фрактальный на самом деле! 😧
Автор библиотеки допустил ошибку, и весь фрактальный шум генерируется на основе линейных параметров, не учитывая предыдущие шаги. Это дает совсем неинтересные результаты: фактически значение в каждой точке просто уточняется N итераций (октав). А как мы знаем, фракталы - это немного другое.
Дописать и решить эту проблему не сложно, в общем случае все сводится к добавлению множителя на каждом шаге, который рассчитывается вот так:
На скриншоте слева - обычный Fractal Ridged из Fast Noise, справа - Multi Fractal Ridged моей версии.🫥
#generation
Автор библиотеки допустил ошибку, и весь фрактальный шум генерируется на основе линейных параметров, не учитывая предыдущие шаги. Это дает совсем неинтересные результаты: фактически значение в каждой точке просто уточняется N итераций (октав). А как мы знаем, фракталы - это немного другое.
Дописать и решить эту проблему не сложно, в общем случае все сводится к добавлению множителя на каждом шаге, который рассчитывается вот так:
pow(lacunarity, -index * spectralExponent);
На скриншоте слева - обычный Fractal Ridged из Fast Noise, справа - Multi Fractal Ridged моей версии.
#generation
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥18❤5👍4
Наконец-то пофиксила все баги, которые наделала 🫥 , а также вернула почти весь функционал, чтобы можно было сравнить старую версию генератора с новой. 😧
Я не меняла алгоритмы генерации, только снизила нагрузку как общую, так и per frame.
Список изменений:
🔹 планирование генерации чанков стало Burst Compatible и полностью unmanaged;
🔹 планирование теперь предполагает подход Wave Front (волнами) по 32 чанка на волну;
🔹 были убраны отдельные Thread Worker для генерации, вместо них вся генерация была переписана на Job System, а также все максимально стало Burst Compatible;
🔹 расчет SDF для вокселей был полностью перенесен на GPGPU - это позволило считать SDF сразу для всех вокселей, что очень сильно упростило дальнейшие алгоритмы;
🔹 пост-процессинг сгенерированных данных был сильно упрощен, а также изменен в сторону like-ECS;
🔹 были переписаны структуры данных: где-то в угоду производительности (но больше использование памяти), где-то наоборот, но в целом они мало поменяли картину;
🔹 был упрощен подход с хранением типов вокселей: теперь есть общие наборы типов вокселей для всех биомов, биомы же только предоставляют веса для типов вокселей из сетов;
🔹 множество мелких исправлений и улучшений, что-то даже уже не помню🤭
Что касается цифр, было:
❌ были большие пики, абсолютные цифры также удручающие - свыше 600 мс на кадр❗️
❌ генерация всего мира занимала примерно 1 секунду, но ценой времени кадра (свыше 100 мс).
Стало:
✅ пики ушли, на графике есть локальные пики, которые не относятся к генерации и никак не влияют на время кадра;
✅ за кадр медиана 0,03мс на генерацию в Main Thread и примерно ~10 мс суммарно по всем Job System Workers;
✅ генерация всего мира занимает примерно 1 секунду, но время кадра почти не меняется (полезно будет при реализации телепортов/быстрого перемещения).
#generation
Я не меняла алгоритмы генерации, только снизила нагрузку как общую, так и per frame.
Список изменений:
🔹 планирование генерации чанков стало Burst Compatible и полностью unmanaged;
🔹 планирование теперь предполагает подход Wave Front (волнами) по 32 чанка на волну;
🔹 были убраны отдельные Thread Worker для генерации, вместо них вся генерация была переписана на Job System, а также все максимально стало Burst Compatible;
🔹 расчет SDF для вокселей был полностью перенесен на GPGPU - это позволило считать SDF сразу для всех вокселей, что очень сильно упростило дальнейшие алгоритмы;
🔹 пост-процессинг сгенерированных данных был сильно упрощен, а также изменен в сторону like-ECS;
🔹 были переписаны структуры данных: где-то в угоду производительности (но больше использование памяти), где-то наоборот, но в целом они мало поменяли картину;
🔹 был упрощен подход с хранением типов вокселей: теперь есть общие наборы типов вокселей для всех биомов, биомы же только предоставляют веса для типов вокселей из сетов;
🔹 множество мелких исправлений и улучшений, что-то даже уже не помню
Что касается цифр, было:
❌ были большие пики, абсолютные цифры также удручающие - свыше 600 мс на кадр❗️
❌ генерация всего мира занимала примерно 1 секунду, но ценой времени кадра (свыше 100 мс).
Стало:
✅ пики ушли, на графике есть локальные пики, которые не относятся к генерации и никак не влияют на время кадра;
✅ за кадр медиана 0,03мс на генерацию в Main Thread и примерно ~10 мс суммарно по всем Job System Workers;
✅ генерация всего мира занимает примерно 1 секунду, но время кадра почти не меняется (полезно будет при реализации телепортов/быстрого перемещения).
#generation
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥19❤7👏5👍2
ГД как часть разработки
Без грамотного описания игры разработка скорее всего не достигнет успеха. Потому что это будет не процесс разработки, а процесс нащупывания в темноте. ГД (геймдизайн) тут выступает в качестве факела, который освещает путь и разгоняет темноту☺️
Я конечно же тоже работаю над ГДД (геймдизайн документом). Это большая и сложная задача, к которой приходится подходить постепенно, шаг за шагом. И также как и с кодом, итеративно.
За последнюю неделю я описала еще несколько механик, ответила на часть вопросов, которые витали в воздухе. Переработала часть критических моментов, и даже вывела некоторые формулы для расчета параметров и характеристик.
Так что, не считая понедельника, всю неделю я не написала ни единой строчки кода😔 , зато написала сотни и даже тысячи строк текста 🤭
Также, я пришла к одной идее, которая возможно многим знакома, но для меня, как разработчицы в первую очередь, а потом уже геймдизайнера, это было небольшим открытием.
Идея
Идея заключается в том, чтобы все параметры, характеристики, термины, объекты заносить в таблицу. В тексте описания механик, вместо того, чтобы постоянно описывать какой-нибудь параметр и за что он отвечает, просто ссылаться на элемент таблицы.
Это правда очень сильно облегчает жизнь: получается прямая ссылка, название которой еще можно менять в одно действие. В таблице, для каждого элемента можно также завести описание, чтобы при клике на ссылку мы еще видели и описание.
В Notion, который я использую, это делается максимально просто:
🔹создается таблица;
🔹таблица заполняется нужными элементами;
🔹сослаться на элемент из таблицы в любом блоке текста можно через @ и в выпадающем списке выбрать конкретный элемент.
#game_design
Без грамотного описания игры разработка скорее всего не достигнет успеха. Потому что это будет не процесс разработки, а процесс нащупывания в темноте. ГД (геймдизайн) тут выступает в качестве факела, который освещает путь и разгоняет темноту
Я конечно же тоже работаю над ГДД (геймдизайн документом). Это большая и сложная задача, к которой приходится подходить постепенно, шаг за шагом. И также как и с кодом, итеративно.
За последнюю неделю я описала еще несколько механик, ответила на часть вопросов, которые витали в воздухе. Переработала часть критических моментов, и даже вывела некоторые формулы для расчета параметров и характеристик.
Так что, не считая понедельника, всю неделю я не написала ни единой строчки кода
Также, я пришла к одной идее, которая возможно многим знакома, но для меня, как разработчицы в первую очередь, а потом уже геймдизайнера, это было небольшим открытием.
Идея
Идея заключается в том, чтобы все параметры, характеристики, термины, объекты заносить в таблицу. В тексте описания механик, вместо того, чтобы постоянно описывать какой-нибудь параметр и за что он отвечает, просто ссылаться на элемент таблицы.
Это правда очень сильно облегчает жизнь: получается прямая ссылка, название которой еще можно менять в одно действие. В таблице, для каждого элемента можно также завести описание, чтобы при клике на ссылку мы еще видели и описание.
В Notion, который я использую, это делается максимально просто:
🔹создается таблица;
🔹таблица заполняется нужными элементами;
🔹сослаться на элемент из таблицы в любом блоке текста можно через @ и в выпадающем списке выбрать конкретный элемент.
#game_design
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9🔥7❤3😁1
Media is too big
VIEW IN TELEGRAM
Знаю знаю, что ландшафт цвета детской неожиданности совсем не впечатляет 🤭 (обещаю, что скоро это изменится), но все равно поделюсь поделюсь видео с первыми результатами переноса поддержки множества биомов на GPU.
Когда я считала воксели на Job System, то у меня уже была успешно реализована функция смешивания разных биомов. Сейчас же, в рамках задачи возвращения оставшихся функций на новый пайплайн генерации, переношу смешивание на HLSL.
Это оказалось не так просто, так как столкнулась с очень долгой компиляцией вычислительных шейдеров😔 Все потому, что весь код инлайнится, а алгоритмы шумов - большие и дорогие 😭 Но ничего, я уже успешно снизила время с 30 минут до 30 секунд, жить можно) А там еще что-нибудь придумаю 🥔
#generation
Когда я считала воксели на Job System, то у меня уже была успешно реализована функция смешивания разных биомов. Сейчас же, в рамках задачи возвращения оставшихся функций на новый пайплайн генерации, переношу смешивание на HLSL.
Это оказалось не так просто, так как столкнулась с очень долгой компиляцией вычислительных шейдеров
#generation
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥16👏7🤯3👍2❤1
Media is too big
VIEW IN TELEGRAM
Графический дебаг иногда бывает красивым, а иногда даже жутковатым 🤭
Сломала шейдинг, приходится искать проблему самым простым способом: выводить вертекс каналы в качестве Albedo. Это например веса нескольких материалов, жаль, что неправильные, интересно почему?🤔
#generation
Сломала шейдинг, приходится искать проблему самым простым способом: выводить вертекс каналы в качестве Albedo. Это например веса нескольких материалов, жаль, что неправильные, интересно почему?
#generation
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11❤3🔥3🙏2