1. 3 г. назад
    3 г. назад исправил Zorko

    Здравствуйте, уважаемые коллеги.

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

    Как я понял, логика работы любой игры на Monkey/Mojo - это обновление экрана внутри OnRender каждые N мс (а GUI-приложения на Monkey работают подобным же образом, бешено перерисовывая одни и те же контролы стопсят раз в секунду?). Поправьте, если это не так. Есть OnRender, он вызывается самим классом App (вручную вызвать его из своих методов нельзя). Минимальная задержка для обновления экрана - SetUpdateRate(1) - 1 раз в секунду. Для многих игр, очень регулярно обновляющих содержимое экрана, такая логика вполне хороша. Но как быть со спокойными играми типа карточных, где нужно вывести всю графику и спокойно ждать реакции пользователя, перерисовывая некоторые элементы на экране частями, а не лихорадочно обновлять экран каждые 60 или больше раз в секунду. Это кажется излишне нагружающим процессор (на десктопе) и садящим заряд батареи (на планшете/смартфоне).

    Я было подумал, что можно использовать SetUpdateRate(0), устанавливая внутри своего метода SetUpdateRate(60) когда пора перерисовать экран, и снова возвращать SetUpdateRate(0) внутри OnRender. Но вот что пишут про SetUpdateRate(0):

    http://socoder.net/index2.php?topic=4047&seenpost=40229

    SetUpdateRate(0) will run OnUpdate and OnRender as fast as possible.

    Т.е. аргумент 0 вовсе не значит "обновлять только по необходимости", но "обновлять с максимальной частотой".

    Логика моей программы примерно такая (не событийная):

    Нарисовать что-то одно на части экрана (появилось на экране)
    Подождать секунду
    Нарисовать что-то другое на части экрана (появилось на экране)
    Опросить управление, отреагировать на управление, перерисовав что-то на части экрана
    (предполагается наличие буфера, в котором производятся графические операции над частями экрана)

    А не:

    Method OnRender ' С крайне высоким фпс! ;)
    Пришла пора срочно перерисовать весь экран, будьте любезны его заново сформировать
    End Method

    Как мне быть? Воспользоваться средним фпс, например, SetUpdateRate(15), переведя всё-таки логику работы на событийность (предвижу трудности)? Воспользоваться многозадачностью (например, с помощью Diddy), руля логикой в отдельном потоке, но каким-то образом отдавая OnRender'у команды на перерисовку графики? У кого были подобные трудности и нашлось ли решение? Примеры кода будут очень кстати. Спасибо.

    Извиняюсь за долгий ответ. По поводу Вашего примера - какую версию Monkey вы используете (номер версии)? Т.к. это похоже на баг, круг должен двигаться самостоятельно и во всех таргетах. Именно это у меня и происходит.

    @Zorko как определить - пора перерисовывать экран по требованию ОС (скажем, курсор мышки проехал по экрану и нужно восстановить изображение)

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

    @Zorko you might want to consider timestep-independent code. For this demo we assume fixed 60fps

    Здесь, скорее всего, имеется в виду SetUpdateRate(0), но не тот, который получился у Вас, а тот который должен быть на самом деле.

    @Zorko Вот игра JackHammer , типично тому, что я хочу сделать. Т.е. думаете, она перерисовывает одно и то же с постоянным фпс?

    Изучил исходник. В игре установлен UpdateRate=60:

    bb_app_SetUpdateRate(60);

    В OnRender не нашел условие выхода и ренедра, т.е. игра в идеальных условиях перерисовывается 60 раз в секунду.

    @Zorko Извиняюсь за некоторое упорство. Возможно, это связано с опытом работы с библиотекой SDL

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

    @Zorko Это интересно, хотелось бы примерчик.

    Хотел сделать пример с BeginRender и EndRender, но в итоге пример вырос до, возможно, подходящего Вам решения. Постарался в комментариях все объяснить

    Import mojo
    
    Class MyApp Extends App
    
    	Field buffer:Image	
    	Field pixels:Int[]
    	
    	Field dirty:Int
    
    	Method OnCreate:Int()
    		SetUpdateRate(15)
    		
    		'буфер для рендера
    		buffer = CreateImage(DeviceWidth(), DeviceHeight())
    		'кэш для пикселей
    		pixels = New Int[DeviceWidth()*DeviceHeight()]
    		Return 0
    	End Method
    	
    	Method OnUpdate:Int()
    		If (MouseHit())	
    			BeginRender() 'После вызова этой функции можем использовать функции рендера в любом месте
    			
    			'рисуем
    			Cls(0, 0, 0)
    			SetColor(255, 255, 255)
    			DrawRect(MouseX(), MouseY(), 50, 50)
    			
    			'записываем измененный регион в массив пикселей
    			ReadPixels(pixels, MouseX(), MouseY(), 50, 50)
    			
    			EndRender() 'завершаем рендер
    			
    			'записываем полученные пиксели в буфер
    			buffer.WritePixels(pixels, MouseX(), MouseY(), 50, 50)
    			dirty = 2 'хак, для таргетов с двойной буферизацией нам нужно нарисовать изображение два раза, чтобы оно попало в оба буфера, иначе получим мерцание
    		End If
    	End Method
    
    	Method OnRender:Int()
    		'Рисуем буфер, только в случае если он был изменен
    		If (Not dirty) Return
    		
    		DrawImage(buffer, 0, 0)		
    		dirty -= 1
    		
    		Return 0
    	End Method
    
    End Class
    
    Function Main:Int()
    	New MyApp
    	Return 0
    End Function
  2. devolonter

    6 Mar 2015 Администратор

    Приветствую!

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

    По поводу SetUpdateRate, хотелось бы заметить что это количество вызовов OnUpdate, а не OnRender, в случае если рендер занимает слишком много времени он начинает вызываться реже OnUpdate. Это не очевидно, потому что в идеальном случае количество вызовов OnRender действительно соответствует UpdateRate, но это не всегда так. SetUpdateRate(0) как раз отменяет эту коррекцию, т.е. в этом случае за OnUpdate всегда следует OnRender. При update rate = 0 вы сами вольны выбирать способ стабилизации FPS и по сути для этого он и был введен.

    Чтобы прервать рендер вы можете вызвать Return в OnRender, в этом случае рендер будет прерван. Также вы можете использовать BeginRender и EndRender для ручного запуска и завершения рендера.

    Но еще раз повторюсь, я бы выбрал вариант с update rate = 15

  3. 3 г. назад исправил Zorko

    Спасибо за Ваш ответ, Артур.

    Я подготовил такой примерчик, чтобы потестировать SetUpdateRate(0). И результат его работы расходится с форумным заверением , что SetUpdateRate(0) - это рендер с максимально возможным фпс.

    Import mojo
    
    Class MyApp Extends App
    
    Field x: Int
    
    Method OnCreate: Int ()
      SetUpdateRate(0)
      x = 0
      Return 0
    End Method
    
    Method OnRender: Int ()
      Cls(0, 0, 0)
      DrawCircle(x, 30, 30)
      x += 1
      Return 0
    End Method
    
    End Class
    
    Function Main: Int ()
      New MyApp
      Return 0
    End Function

    На таргете glfw круг едет вправо только если водить мышку над приложением (т.е. Windows не сохраняет картинку окна приложения и просит его перерисовать самого себя когда нужно восстановить изображение под курсором мышки). На таргете html5 круг вообще никак не движется.

    А как будет вести себя этот пример на Android и других таргетах? (не могу проверить, у меня не Pro-версия).

    В любом случае, мне нравится идея нерегулярного перерисовывания экрана только при реальной необходимости. Но непонятно как активировать перерисовку в случае SetUpdateRate(0), когда пришла пора обновлять экран.

    В случае объявления ненулевого аргумента (скажем, вызов OnRender 15 раз в секунду, но возвращение из него без реальной перерисовки) предвидится проблема: как определить - пора перерисовывать экран по требованию ОС (скажем, курсор мышки проехал по экрану и нужно восстановить изображение) или же пришла пора перерисовывать ибо изменилась логика работы приложения (скажем, количество набранных очков в игре). Не знаю способа как это сделать. При SetUpdateRate(0) метод OnRender вызывается именно так как мне и нужно, но только по требованию ОС. А как перерисовывать экран самостоятельно - неясно. При SetUpdateRate(15) же получается, что из этих 15 раз в секунду реально перерисовывать экран нужно, может быть, один раз, а может и ни разу, но нет возможности определить инициатора перерисовки (ОС просит восстановить изображение или же тикнул таймер SetUpdateRate(не_нуль) ). Такая дилемма. Вариант безусловной перерисовки 15 раз в сек тоже рассматривается, но как-то без энтузиазма.

    В уме нарисовалась такая логика OnRender (если получится вызывать его так, как хочется):

    1. Нарисовать картинку из графического буфера
    2. Возможно, сделать какие-то графические изменения на экране
    3. И если изменения были, то сохранить содержимое экрана опять в буфер

    При этом если инициатива перерисовки пришла от ОС, то пункт 1 к выполнению обязателен. А вот если по тику от таймера SetUpdateRate(не_нуль), то получается, что можно бы на этом и сэкономить, ведь на экране остаётся предыдущая картинка, она не изменилась.

    Я нашёл видео-урок по созданию игр на Monkey , в котором (позиция 8:56) утверждается следующее:

    you might want to consider timestep-independent code. For this demo we assume fixed 60fps

    Из чего можно сделать вывод, что нерегулярное (т.е. именно такое как мне нужно) независимое от шага времени обновление экрана возможно. Как же этого добиться?

    Вот игра JackHammer , типично тому, что я хочу сделать. Т.е. думаете, она перерисовывает одно и то же с постоянным фпс?

    Извиняюсь за некоторое упорство. Возможно, это связано с опытом работы с библиотекой SDL , и в ней такая логика, как мне нужно, легко достижима - рисуем, обновляем по частям, опрашиваем события когда это нам нужно и т.д. Mojo же устроена для меня несколько непривычно. :)

    @devolonter Чтобы прервать рендер вы можете вызвать Return в OnRender, в этом случае рендер будет прерван.

    Но прерывание рендера ведь имеет смысл только если мы уверены, что инициатива перерисовки происходит не от ОС (потому что испортилось изображение), а потому что подоспел тик таймера перерисовки? А я пока не умею определять это.

    @devolonter Также вы можете использовать BeginRender и EndRender для ручного запуска и завершения рендера.

    Это интересно, хотелось бы примерчик.

  4. devolonter

    8 Mar 2015 Администратор Ответ
    3 г. назад исправил devolonter

    Извиняюсь за долгий ответ. По поводу Вашего примера - какую версию Monkey вы используете (номер версии)? Т.к. это похоже на баг, круг должен двигаться самостоятельно и во всех таргетах. Именно это у меня и происходит.

    @Zorko как определить - пора перерисовывать экран по требованию ОС (скажем, курсор мышки проехал по экрану и нужно восстановить изображение)

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

    @Zorko you might want to consider timestep-independent code. For this demo we assume fixed 60fps

    Здесь, скорее всего, имеется в виду SetUpdateRate(0), но не тот, который получился у Вас, а тот который должен быть на самом деле.

    @Zorko Вот игра JackHammer , типично тому, что я хочу сделать. Т.е. думаете, она перерисовывает одно и то же с постоянным фпс?

    Изучил исходник. В игре установлен UpdateRate=60:

    bb_app_SetUpdateRate(60);

    В OnRender не нашел условие выхода и ренедра, т.е. игра в идеальных условиях перерисовывается 60 раз в секунду.

    @Zorko Извиняюсь за некоторое упорство. Возможно, это связано с опытом работы с библиотекой SDL

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

    @Zorko Это интересно, хотелось бы примерчик.

    Хотел сделать пример с BeginRender и EndRender, но в итоге пример вырос до, возможно, подходящего Вам решения. Постарался в комментариях все объяснить

    Import mojo
    
    Class MyApp Extends App
    
    	Field buffer:Image	
    	Field pixels:Int[]
    	
    	Field dirty:Int
    
    	Method OnCreate:Int()
    		SetUpdateRate(15)
    		
    		'буфер для рендера
    		buffer = CreateImage(DeviceWidth(), DeviceHeight())
    		'кэш для пикселей
    		pixels = New Int[DeviceWidth()*DeviceHeight()]
    		Return 0
    	End Method
    	
    	Method OnUpdate:Int()
    		If (MouseHit())	
    			BeginRender() 'После вызова этой функции можем использовать функции рендера в любом месте
    			
    			'рисуем
    			Cls(0, 0, 0)
    			SetColor(255, 255, 255)
    			DrawRect(MouseX(), MouseY(), 50, 50)
    			
    			'записываем измененный регион в массив пикселей
    			ReadPixels(pixels, MouseX(), MouseY(), 50, 50)
    			
    			EndRender() 'завершаем рендер
    			
    			'записываем полученные пиксели в буфер
    			buffer.WritePixels(pixels, MouseX(), MouseY(), 50, 50)
    			dirty = 2 'хак, для таргетов с двойной буферизацией нам нужно нарисовать изображение два раза, чтобы оно попало в оба буфера, иначе получим мерцание
    		End If
    	End Method
    
    	Method OnRender:Int()
    		'Рисуем буфер, только в случае если он был изменен
    		If (Not dirty) Return
    		
    		DrawImage(buffer, 0, 0)		
    		dirty -= 1
    		
    		Return 0
    	End Method
    
    End Class
    
    Function Main:Int()
    	New MyApp
    	Return 0
    End Function
  5. Артур, что Вы, никаких извинений, мы ведь здесь на добровольных началах, никто никому не обязан отвечать дельно, да ещё и быстро. :) Вы мне очень помогли. Решение весьма изящное. Этот код делает именно то, что мне было нужно - есть перерисовка по частям, работают все операции графической отрисовки, а также можно напрямую работать с пикселами, реализуя, например, скроллинг экрана прямым копированием пикселей в массиве.

    Слегка дополню Ваш код. На таргете Desktop Game (glfw) если свернуть приложение и потом развернуть - экран не перерисовывается. Опытным путём выяснилось: при развёртывании приложения метод OnResume вызывается не всегда, а OnResize всегда (даже при #GLFW_WINDOW_RESIZABLE=False). Поэтому дополним код:

    	Method OnResume: Int ()
    		dirty = 2
    	End Method
    
    	Method OnResize: Int ()
    		dirty = 2
    	End Method

    Теперь мне осталось решить всего одну проблему насчёт потока исполнения. Т.е. для логики приложения нужно захватить поток исполнения монопольно, на всё время, опрашивая управление и делая задержки самостоятельно. Это чтобы упростить работу по переносу кода, ибо здесь есть, например, опрос управления в виде процедуры и перенести его на события не очень тривиально.

    Вижу два варианта решения:

    1) простой; из OnCreate не возвращаемся никогда, а остальные события обрабатываем оттуда же с помощью аналога Дельфи Application.ProcessMessages (если такой конечно имеется в Monkey);

    2) на потоках (тредах, нитях). OnCreate запускает новую нить, которая делает всю работу, в т.ч. и по опросу событий и графической отрисовке.

    Прокомментируйте, пожалуйста. Если удастся решить и эту проблему, моя задумка (порт карточной игры) больше не упирается в логические препятствия. Впрочем, Monkey меня уже впечатлил: несмотря на скромность средств Mojo получается выстроить нужную логику перерисовки экрана с лёгкостью, и всё действительно работает как надо.

    @devolonter По поводу Вашего примера - какую версию Monkey вы используете (номер версии)? Т.к. это похоже на баг, круг должен двигаться самостоятельно и во всех таргетах. Именно это у меня и происходит.

    Использовал версию Monkey X - FREE, которая доступна сейчас с оф. сайта после регистрации. Это MonkeyX77a. В репозитории доступна версия v83a, но я пока не умею её собирать, а исполняемых файлов там нет. Можно ли откуда-то скачать собранную версию для Windows?

    Так что Ваш код я тестировал в Mungo. Кстати, действительно работает не так как в MonkeyX77a, а как Вы сказали. В Mungo всё собирается и работает отлично.

    Спасибо, Артур! :)

  6. devolonter

    12 Mar 2015 Администратор
    3 г. назад исправил devolonter

    @Zorko Прокомментируйте, пожалуйста. Если удастся решить и эту проблему, моя задумка (порт карточной игры) больше не упирается в логические препятствия.

    Не выходить из OnCreate у Вас, к сожалению, не получится, т.к. в этом случае будет сложно заставить все работать как задумано изначально, а Application.ProcessMessages в Monkey отсутствует. App класс Mojo имеет стэк вызовов и этого стэка следует придерживаться - логику обновляем в OnUpdate, рендер в OnRender. Потоки, применять в Monkey не советую, но если очень хочется можно воспользоваться недокументированным модулем brl.thread в связке с brl.asyncevent. Но если честно, боюсь, что это принесет немало головной боли.

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

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

    Import monkeytarget
    
    Class GameDelegate Extends BBGameDelegate
    
    	Method StartGame:Void()
    		Print "StartGame"
    		BBGame.Game().SetUpdateRate 60
    	End
    	
    	Method SuspendGame:Void()
    		Print "SuspendGame"
    	End
    	
    	Method ResumeGame:Void()
    		Print "ResumeGame"
    	End
    	
    	Method UpdateGame:Void()
    		Print "UpdateGame"
    	End
    	
    	Method RenderGame:Void()
    		Print "RenderGame"
    	End
    	
    	Method KeyEvent:Void( event:Int,data:Int )
    		Print "KeyEvent: event="+event+", data="+data
    	End
    	
    	Method MouseEvent:Void( event:Int,data:Int,x:Float,y:Float )
    		Print "MouseEvent: event="+event+", data="+data+", x="+x+", y="+y
    	End
    	
    	Method TouchEvent:Void( event:Int,data:Int,x:Float,y:Float )
    		Print "TouchEvent: event="+event+", data="+data+", x="+x+", y="+y
    	End
    
    	Method MotionEvent:Void( event:Int,data:Int,x:Float,y:Float,z:Float )
    		Print "MotionEvent: event="+event+", data="+data+", x="+x+", y="+y+", z="+z
    	End
    	
    	Method DiscardGraphics:Void()
    		Print "DiscardGraphics"
    	End
    
    End
    
    Function Main()
    
    	BBGame.Game().SetDelegate New GameDelegate
    
    End

    (код из стандартных примеров: mak/bbgametest/bbgametest.monkey)

    @Zorko Можно ли откуда-то скачать собранную версию для Windows?

    Из официальных источников, к сожалению, нет. Тут два выхода, или собирать самому из исходников на GitHub или использовать Mungo (он полностью совместим), правда последняя сборка Mungo основана на V82

  7. 3 г. назад исправил Zorko

    Благодарю за ответ. Буду пока пользоваться Mungo. (вот и ниша для Mungo, раз Марк раздаёт пока версию 77a, видимо, считая её стабильной?)

    У меня много вопросов, связанных с непониманием механизмов работы Monkey, например:

    1. Есть ли указатели на функции? на методы? Или их аналог.

    2. Вот в этом участке кода:

    Function Main:Int()
    	New MyApp
    	Return 0
    End Function

    очевидно, что основной поток исполнения всё время крутится внутри класса MyApp, а до строчки Return 0 дело доходит только после вызова EndApp() или ручного закрытия приложения. Как именно устроен диспетчер вызова сообщений - дело внутренностей класса App, однако же там явно находится код, ожидающий поступающих сообщений и обрабатывающий их. Вполне возможно, что сюда входят и сообщения от таймера, приводящие к вызову OnUpdate и OnRender.

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

    4. Очевидно, что событийная схема работы приложения диктуется устроением современных технологий, и она является вполне экономичной. Несобытийная схема активного опроса состояния событий - это, скорее, выходец из прошлого, однако старые игры для старых платформ (MSX, DOS, Spectrum) работают именно так. Я вижу возможность перенести их логику на событийность, однако это сложная работа, требующая указателей на методы/функции. Но я полагаю, это всё есть в Monkey. Вместе с тем заманчиво иметь готовый фреймворк (каркас), который бы реализовал несобытийную схему работы приложения, давая точку входа в метод, который бы владел единолично потоком исполнения, и давая аналог ProcessMessages и метод задержки исполнения на заданное время. В SDL это всё возможно (есть процедура SDL_Delay(msec) ).

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

    Какие интересуют платформы. Прежде всего конечно Android. Написать самому модуль, используя нативный код, можно, - очень хорошо, что Monkey позволяет это, но я слабо представляю себе как работает Android изнутри и, увы, слабо знаю язык Java.

  8. devolonter

    16 Mar 2015 Администратор

    @Zorko 1. Есть ли указатели на функции? на методы? Или их аналог.

    Указателей на функции и методы нет, но есть модуль reflection, который позволяет использовать отражение . См. стандартные примеры: reflectiontest/reflectiontest.monkey

    @Zorko очевидно, что основной поток исполнения всё время крутится внутри класса MyApp, а до строчки Return 0 дело доходит только после вызова EndApp() или ручного закрытия приложения.

    Нет, код следующий после New MyApp выполняется после создания сущности MyApp. Приложение не блокирует основной поток. Класс App является оберткой над BBGameDelegate (см. код выше), который в свою очередь наследует нативный класс. Нативный код таргета вызывает методы делегата реализующего BBGameDelegate. Т.е. ожидание системных событий происходит вне объекта класса App, этот объект является конечным звеном данной цепи. Реализация опроса событий, таймера и т.д. зависит от таргета. Где-то у нас есть определенная свобода, а где-то мы мало на что можем повлиять. Подводя итог, если Вы хотите поменять существующую модель приложения, Вам нужно реализовывать собственный таргет.

    @Zorko Методы OnUpdate и OnRender работают гарантированно в одном потоке или могут в разных?

    Гарантировано в одном потоке. Вероятность их одновременного вызова нулевая.

    @Zorko что будет с перерисовкой если OnUpdate задержит нить исполнения на большое время?

    Если OnUpdate задержит поток надолго, то система пометит этот процесс как зависший, т.к. ей перестанут поступать какие-либо сообщения. Если задержка в пределах допустимой нормы, но время выполнения OnUpdate или OnRender оказалось выше, чем расчетное для текущего update rate, то OnRender не будет вызываться до тех пор пока OnUpdate не догонит update rate, но не больше 4-х раз

    @Zorko Вместе с тем заманчиво иметь готовый фреймворк (каркас), который бы реализовал несобытийную схему работы приложения, давая точку входа в метод, который бы владел единолично потоком исполнения, и давая аналог ProcessMessages и метод задержки исполнения на заданное время. В SDL это всё возможно (есть процедура SDL_Delay(msec) ).

    К сожалению, подобный каркас нереализуем на HTML5 и Flash таргетах. По крайней мере я не вижу способа, как это можно реализовать.

    Возможно я не очень понимаю проблему. Но почему бы просто не рассматривать OnUpdate как тело вашего цикла? Ведь по сути так оно и есть.

или зарегистрируйтесь чтобы комментировать!