TGViewer
Channel Public Channel
SpellSinger - разработчик игр на Unity

SpellSinger - разработчик игр на Unity

@spellsingergames

Привет! Я Денис и я делаю игры на Unity. В этом блоге я делюсь своим опытом в геймдеве и не только.

Навигация в закрепе: https://t.me/spellsingergames/3

Написать в личку: @sinedsem
Subscribers
548
Photos
90
Videos
22
Links
105

Showing posts older than #64 · Back to latest

Older Posts 20 shown
Post #63 706
Посыпались первые проблемы. У 20 пользователей вылетел ArgumentNullException. Сейчас будем играть в угадайку, искать, где проблемное место, ведь в WebGL принято выкидывать stack trace эксепшенов ради скорости работы
  • 😐 1
Post #62 706
Post #61 641
Мы доделали первую версию Бурить-Копать!

Вчера мы наконец-то отправили на модерацию в Яндекс игры первую версию игры, которую мы начали делать ещё в сентябре прошлого года.

В релиз вошло далеко не всё, что мы бы хотели, а именно отсутствует часть удерживающих механик, таких как ачивки, но откладывать релиз ещё на несколько месяцев мы не хотим.

Буду писать здесь о успехах и неудачах. А пока делайте ставки, с какого раза мы пройдем модерацию?)

#бурить_копать
  • ❤ 7
  • 🔥 5
  • 🎉 2
  • 😎 1
Post #60 7.44K
Изображение, загруженное из интернета, размыто

Когда я сделал аватарки игроков для таблицы лидеров по уроку от Яндекса, у меня получились супер "мыльные" изображения. Мыльными они были не во всех разрешениях игры, а только на некоторых. То есть я запускаю игру в 1920х1080, и всё ок, а потом заливаю в Яндекс Игры, где размер области с игрой 1632х899 и качество становится сильно хуже.

Как я понял, это связано с тем, что для текстуры создаются Mip Map'ы, так как видимо метод UnityWebRequestTexture.GetTexture(url) был предназначен в первую очередь для загрузки текстур, а не спрайтов.

Соответственно, нам нужно взять RawData из загруженной текстуры, и создать другую текстуру, с необходимыми нам настройками (в примере я отключил создание Mip Maps):

[SerializeField] private RawImage bad;
[SerializeField] private RawImage good;

private IEnumerator Download(string url)
{
var request = UnityWebRequestTexture.GetTexture(url);
yield return request.SendWebRequest();

if (request.result == UnityWebRequest.Result.Success)
{
var downloaded = DownloadHandlerTexture.GetContent(request);

var converted = new Texture2D(downloaded.width, downloaded.height, downloaded.format, false);
converted.LoadRawTextureData(downloaded.GetRawTextureData());
converted.Apply(false, true);

bad.texture = downloaded;
good.texture = converted;
}
}


UPD: изначально у меня был пример с созданием Sprite из Texture2D, но, как верно заметили в комментариях, это не обязательно, можно воспользоваться RawImage. Сути проблемы это не меняет, всё равно нужно конвертировать текстуру. Изначальный пример оставил в комментариях.

#тайные_знания #ui #веб_игры
  • 🔥 7
  • 👍 4
Post #59 1.05K
Игра ломается в мобильном браузере, в первую очередь на iOS

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

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

Оказалось, что в мобильных телефонах довольно жесткие ограничения по памяти. В интернете пишут о лимите в 350 мб RAM. Согласно профайлеру, у меня как раз было потребление около 350 мб, из них графика и текстуры занимали около 280 мб.

Причем важно отметить, что на потребление оперативной памяти влияют разрешения текстур, а вот степень сжатия не влияет.

Хотелось отложить оптимизацию на потом, но, теперь вынуждены делать это прямо сейчас.

#веб_игры
  • 🔥 5
Post #58 1.49K
Можно ли в Unity отменить загрузку сцены?

Я столкнулся с одной интересной проблемой. Кажется, в Unity нельзя отменить загрузку сцены.

Я нахожусь на сцене главного меню. Я асинхронно (через Addressables) загружаю сцену с игрой, с флагом activateOnLoad=false.

Если сцена загрузилась, но не активирована, в ней обязательно выполнятся как минимум Awake на монобехах. Без вариантов. Это случится либо при переходе на любую другую сцену, либо при выходе из Play Mode, если игра запущена в редакторе.

В моём случае это проблема, так как на загруженной сцене успевает проинициализироваться Zenject контекст, что приводит к неожиданным сайд-эффектам.

Я пробовал вызывать разные unload методы, но безуспешно.

Пока что вижу только одно решение - выносить содержимое сцены в отдельные бандлы (аддресабл группы), и загружать их отдельно - а по нажатию кнопки перехода в игру грузить пустую сцену с activateOnLoad=true.


В чате Unity Architectural Astronauts поделились ответом от разработчиков Unity по этому поводу:

I have asked our developers to take a look at this issue and they stated that this is by design. Scene loading, while async, is based around a first-in-first-out (FIFO) queue - loading the second scene does not begin until the first load operation is completed. Both loading and unloading happen on the same queue but unloading can't be queue unless the scene has been fully loaded - this is why SceneManager.UnloadSceneAsync returns null though the scene exists.

We apologize for the inconvenience.


#нерешенное
  • 🔥 7
Post #57 938
Огонь системой частиц

Делюсь видео уроком, по которому мы реализовали огонь турбин в нашей игре: https://www.youtube.com/watch?v=5Mw6NpSEb2o

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

Основное преимущество использовать Particle System вместо спрайтовой анимации заключается в том, что огонь реагирует на перемещения, а не привязан к локальным координатам бура.

#vfx #бурить_копать
  • 👍 3
  • 🍓 3
  • 🔥 2
Post #56 818
Как обновить pivot у всех спрайтов текстуры?

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

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

#selfmade
  • 👍 6
Post #55
Channel photo updated
Post #54 814
Explicit Interface Implementation

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

Оказывается, шарп позволяет по разному имплементировать методы с одинаковыми названиями, но объявленными в разных интерфейсах, и вызывать нужную имплементацию в зависимости от того, к переменной какого типа ты обращаешься. Это называется Explicit Interface Implementation, по ссылке можно подробно почитать про это.

Соответственно, проблема из предыдущего поста решается следующим образом:

public interface ICollectable
{
CollectableSkin Skin { get; }
}
public abstract class CollectableType<TSkin> : ScriptableObject, ICollectable where TSkin : CollectableSkin
{
[field: SerializeField] public TSkin Skin { get; private set; }
CollectableSkin ICollectable.Skin => Skin;
}


Это именно то, что мне было нужно. В тех местах, где у меня переменная базового типа ICollectable, я получу базовый класс CollectableSkin, а там, где будет какой-то конкретный тип CollectableType<TSkin>, я получу конкретный TSkin

Ставь 🔥 если не знал про такое, 🤡 если для тебя это было очевидно.
  • 🤡 9
  • 🔥 2
  • ⚡ 1
  • 🍓 1
  • 🙈 1
Post #53 648
В дополнение к предыдущему посту, реальный пример с реальной проблемой.

Есть сущность CollectableType, это базовый класс, SO, описывающий параметры предметов, которые можно найти на карте.
public interface ICollectable
{
CollectableSkin Skin { get; }
}
public abstract class CollectableType<TSkin> : ScriptableObject, ICollectable where TSkin : CollectableSkin
{
// Property 'Skin' cannot implement property from interface 'Game.SO.Gems.ICollectable'. Type should be 'Game.SO.CollectableSkin'.
[field: SerializeField] public TSkin Skin { get; private set; }
}


Есть сущность CollectableSkin, это SO, описывающий внешний вид предметов.
public class CollectableSkin : ScriptableObject
{
[field: SerializeField] public TileBase Tile { get; private set; }
}


Среди предметов, бывают, например, бомбы. Бомбы наследуются от CollectableType
public class BombType : CollectableType<BombSkin>
{
[field: SerializeField] public float Damage { get; private set; }
}


И используют свой дочерний класс внешнего вида, BombSkin, в котором есть поля, специфичные для бомб.
public class BombSkin : CollectableSkin
{
[field: SerializeField] public AnimationClip Explosion { get; private set; }
}


В тех местах, где я работаю с базовым типом CollectableType, мне достаточно только базовых полей CollectableSkin. Но там, где я работаю конкретно с бомбами, мне нужно, чтобы свойство Skin возвращало именно BombSkin, так как там есть конкретные поля для бомб.

P. S. Мы же не всерьёз считаем принудительный каст безопасным решением проблемы?

P. P. S. Залетайте на стрим, обсудим проблему вживую, покажу, как я её решил.
  • 🤡 4
  • 🍓 4
  • 👍 3
  • 🤔 1
Post #52 549
В чём Java удобнее, чем C#?

В Java, при реализации интерфейса, я могу использовать дочерний тип в возвращаемом значении, а в C# не могу.

Java
class Fruit {
}
class Apple extends Fruit {
}
interface IBasket {
Fruit getFruit();
}
class Basket implements IBasket {
private Apple fruit;
@Override
public Apple getFruit() {
return fruit;
}
}


C#
class Fruit
{
}
class Apple : Fruit
{
}
interface IBasket
{
Fruit GetFruit();
}
class Basket: IBasket
{
private Apple fruit;
// Method 'GetFruit' cannot implement method from interface 'IBasket'. Return type should be 'Fruit'.
public Apple GetFruit()
{
return fruit;
}
}
  • 🤡 13
  • 👍 3
  • 🍓 3
  • 💩 1
Post #51 558
Моё первое знакомство с UniTask

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

Опытные товарищи посоветовали использовать UniTask. Мне понравилось.

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

public class FrenzyState
{
private CancellationTokenSource cancelStopToken;

public void Start()
{
// some useful code
cancelStopToken?.Cancel();
cancelStopToken = new CancellationTokenSource();
Stop(cancelStopToken.Token).Forget();
}

private async UniTaskVoid Stop(CancellationToken cancellationToken)
{
await UniTask.Delay(TimeSpan.FromSeconds(10), cancellationToken: cancellationToken);
// some useful code
}
}
GitHub GitHub - Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity. Provides an efficient allocation free async/await integration for Unity. - Cysharp/UniTask
  • 🔥 5
  • 👎 1
Post #50 556
SpellSinger - разработчик игр на Unity Моя боль от дженериков в C# – отсутствие wildcards Почему, ну почему в C# нет возможности писать <? extends T>? Это же супер удобно в Java. Приведу пример, который работает в Java, но который я не могу повторить на C#: abstract class Factory<TProduct> { …
Зрители на стриме помогли найти целых 2 решения проблемы:
1) Многословное
abstract class Factory<TProduct, TItem, TFactory>
where TProduct : Product<TProduct, TItem, TFactory>
where TItem : Object
where TFactory : Factory<TProduct, TItem, TFactory>
{
public int counter;
public abstract TProduct Make();
}
class Product<TProduct, TItem, TFactory>
where TProduct : Product<TProduct, TItem, TFactory>
where TItem : Object
where TFactory : Factory<TProduct, TItem, TFactory>
{
private readonly TFactory factory;
protected Product(TFactory factory)
{
this.factory = factory;
factory.counter++;
}
public void Destroy()
{
factory.counter--;
}
}
class FloatFactory : Factory<FloatProduct, Object, FloatFactory>
{
public override FloatProduct Make()
{
return new FloatProduct(this);
}
}
class FloatProduct : Product<FloatProduct, Object, FloatFactory>
{
public FloatProduct(FloatFactory factory) : base(factory)
{
}
}


2) с ковариантом
interface IFactory<out TProduct>
{
int Counter { get; set; }
TProduct Make();
}

class Product<TItem>
{
private readonly IFactory<Product<TItem>> factory;

protected Product(IFactory<Product<TItem>> factory)
{
this.factory = factory;
factory.Counter++;
}

public void Destroy()
{
factory.Counter--;
}
}

class FloatFactory : IFactory<FloatProduct>
{
public int Counter { get; set; }

public FloatProduct Make()
{
return new FloatProduct(this);
}
}

class FloatProduct : Product<float>
{
public FloatProduct(FloatFactory factory) : base(factory)
{
}
}
  • 🔥 7
Post #49 511
Моя боль от дженериков в C# – отсутствие wildcards

Почему, ну почему в C# нет возможности писать <? extends T>? Это же супер удобно в Java. Приведу пример, который работает в Java, но который я не могу повторить на C#:
abstract class Factory<TProduct> {
public int counter;
public abstract TProduct make();
}
class Product<TItem> {
private final Factory<? extends Product<TItem>> factory;
protected Product(Factory<? extends Product<TItem>> factory) {
this.factory = factory;
factory.counter++;
}
public void destroy() {
factory.counter--;
}
}
class FloatFactory extends Factory<FloatProduct> {
@Override
public FloatProduct make() {
return new FloatProduct(this);
}
}
class FloatProduct extends Product<Float> {
protected FloatProduct(FloatFactory factory) {
super(factory);
}
}

На С# не получается:

abstract class Factory<TProduct>
{
public int counter;
public abstract TProduct Make();
}
class Product<TItem>
{
private readonly Factory<Product<TItem>> factory;
protected Product(Factory<Product<TItem>> factory)
{
this.factory = factory;
factory.counter++;
}
public void Destroy()
{
factory.counter--;
}
}
class FloatFactory : Factory<FloatProduct>
{
public override FloatProduct Make()
{
return new FloatProduct(this);
// Argument type 'ConsoleApp1222.FloatFactory' is not assignable to parameter type 'ConsoleApp1222.Factory<ConsoleApp1222.Product<float>>'
}
}
class FloatProduct : Product<float>
{
public FloatProduct(FloatFactory factory) : base(factory)
{
}
}


#нерешенное
  • 🤝 2
Post #48 632
Post #47 1.17K
Как выполнить код перед билдом Addressables?

Есть интерфейс IPreprocessBuildWithReport, имплементировав который можно выполнить свой код перед сборкой игры. Однако, сборка Addressables происходит до вызова этого интерфейса.

Если нужно как-то поработать с Addressable ассетами перед билдом, можно унаследовать класс BuildPlayerProcessor – он вызывается до билда аддрессаблов.

Пример:
public class MyBuildPlayerProcessor : BuildPlayerProcessor
{
public override int callbackOrder => 0;

public override void PrepareForBuild(BuildPlayerContext buildPlayerContext)
{
Debug.Log("BuildPlayerProcessor");
}

}

public class MyPreprocessBuildWithReport : IPreprocessBuildWithReport
{
public int callbackOrder => 0;

public void OnPreprocessBuild(BuildReport report)
{
Debug.Log("IPreprocessBuildWithReport");
}
}


#тайные_знания
  • 👍 3
  • 🔥 2
Post #46 745
Использование System.Drawing (libgdiplus) на Mac OS c процессорами Apple

Сегодня появилась возможность поработать в Unity на макбуке с процессором M1. Хотел импортировать анимацию своим инструментом, но он не сработал, вылезла вот такая ошибка:

DllNotFoundException: libgdiplus.dylib assembly:<unknown assembly> type:<unknown type> member:(null)


В интернете находится следующее решение:
Открыть конфиг для используемой версии Unity Editor, например
/Applications/Unity/Hub/Editor/2022.3.10f1/Unity.app/Contents/MonoBleedingEdge/etc/mono/config

И дописать в него строчки
<dllmap dll="gdiplus" target="/Library/Frameworks/Mono.framework/Versions/6.12.0/lib/libgdiplus.dylib" />
<dllmap dll="gdiplus.dll" target="/Library/Frameworks/Mono.framework/Versions/6.12.0/lib/libgdiplus.dylib" />

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

Тогда я установил mono-libgdiplus с помощью brew
brew install mono-libgdiplus

и в конфиге Unity указал путь до libgdiplus.dylib, который установился через brew
<dllmap dll="gdiplus" target="/opt/homebrew/lib/libgdiplus.dylib" />
<dllmap dll="gdiplus.dll" target="/opt/homebrew/lib/libgdiplus.dylib" />

Перезапустил Unity и ошибка ушла, всё заработало.

#bugs_and_fixes #тайные_знания
  • 👍 5
  • 🔥 1
Post #45 838
Мы сделали редактор шаблонов для нашей игры

Сейчас мы делаем игру Бурить-Копать! (ссылка на черновик). В ней дороги процедурно генерируются. Но, чтобы разнообразить рандом, мы используем заранее придуманные шаблоны.

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

Ссылка на редактор: https://saranai.ru/drill/
А вот примеры того, что уже придумали зрители:
Пример 1
Пример 2

#бурить_копать
  • 👍 3
  • 🔥 3
Post #44 799
Older posts →
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →