Чтобы облегчить жизнь разработчикам игр, создаются разнообразные фреймоворки, не только для С и С++, но и для С# и даже JavaScript. Одним из таких фреймворков является Microsoft XNA, использующий технологию Microsoft DirectX и позволяющий создавать игры для Xbox 360, Windows, and Windows Phone. Microsoft XNA сейчас уже более не развивается, однако в то же время сообщество Open Source предложило другой вариант – MonoGame. Познакомимся с этим фреймворком поближе на примере простой футбольной (к чему бы это?) игры.
Что такое MonoGame?
MonoGame – это open source реализация XNA API не только для Windows, но и для Mac OS X, Apple iOS, Google Android, Linux и Windows Phone. Это означает, что вы можете создавать игры сразу под все эти платформы с минимальными изменениями. Идеально для тех, кто строит планы захвата мира!
Вам даже не требуется Windows для разработки с MonoGame. Вы можете использовать MonoDevelop (open source кросс-платформенный IDE для языков Microsoft .NET) или кросс-платформенный IDE Xamarin Studio для работы на Linux и Mac.
Если вы являетесь разработчиком Microsoft .NET и ежедневно используете Microsoft Visual Studio, MonoGame можно установить и туда. На момент написания статьи последней стабильной версией MonoGame была 3.2, она устанавливается в Visual Studio 2012 и 2013.
Создаем первую игру
Чтобы создать первую игру, в меню шаблонов выберем MonoGame Windows Project. Visual Studio создаст новый проект со всеми необходимыми файлами и ссылками. Если вы запустите проект, то получите что-то такое:
Скучновато, не правда ли? Ничего, это только начало. Вы можете начинать разработку своей игры в этом проекте, но есть один нюанс. Вы не сможете добавлять какие-либо объекты (рисунки, спрайты, шрифты и т.д.) без преобразования их в совместимый с MonoGame формат. Для этого вам понадобится что-то из следующего:
- Установить XNA Game Studio 4.0
- Установить Windows Phone 8 SDK
- Использовать внешнюю программу вроде XNA content compiler
Итак, в Program.cs у вас находится функция Main. Она инициализирует и запускает игру.
static void Main() { using (var game = new Game1()) game.Run(); }
Game1.cs – ядро игры. У вас есть два метода, которые вызываются 60 раз в секунду: Update и Draw. При Update вы пересчитываете данные для всех элементов игры; Draw, соответственно, подразумевает отрисовку этих элементов. Заметьте, что времени на итерацию цикла дается совсем немного – всего 16.7 мс. Если времени на выполнение цикла не хватает, программа пропустит несколько методов Draw, что, естественно, будет заметно на картинке.
Для примера мы создадим футбольную игру «забей пенальти». Управляться удар будет нашим прикосновением, а компьютерный «вратарь» будет стараться поймать мяч. Компьютер выбирает случайные местоположение и скорость «вратаря». Очки считаются привычным нам способом.
Добавляем контент в игру
Первый шаг в создании игры – добавление контента. Начнем с фонового рисунка поля и рисунка мяча. Создадим два PNG рисунка: поля (внизу) и мяча (на КДПВ).
Чтобы использовать эти рисунки в игре, их нужно скомпилировать. Если вы используете XNA Game Studio или Windows Phone 8 SDK, вам нужно создать XNA контент проект. Добавьте рисунки в этот проект и соберите его. Затем зайдите в каталог проекта с копируйте получившиеся .xnb файлы в ваш проект. XNA Content Compiler не требует нового проекта, объекты в нем можно компилировать по мере необходимости.
Когда .xnb файлы готовы, добавьте их в папку Content вашей игры. Создадим два поля, в которых будем хранить текстуры мяча и поля:
private Texture2D _backgroundTexture; private Texture2D _ballTexture;
Эти поля загружаются в методе LoadContent:
protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. _spriteBatch = new SpriteBatch(GraphicsDevice); // TODO: use this.Content to load your game content here _backgroundTexture = Content.Load<Texture2D>("SoccerField"); _ballTexture = Content.Load<Texture2D>("SoccerBall"); }
Теперь нарисуем текстуры в методе Draw:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Green); // Set the position for the background var screenWidth = Window.ClientBounds.Width; var screenHeight = Window.ClientBounds.Height; var rectangle = new Rectangle(0, 0, screenWidth, screenHeight); // Begin a sprite batch _spriteBatch.Begin(); // Draw the background _spriteBatch.Draw(_backgroundTexture, rectangle, Color.White); // Draw the ball var initialBallPositionX = screenWidth / 2; var ínitialBallPositionY = (int)(screenHeight * 0.8); var ballDimension = (screenWidth > screenHeight) ? (int)(screenWidth * 0.02) : (int)(screenHeight * 0.035); var ballRectangle = new Rectangle(initialBallPositionX, ínitialBallPositionY, ballDimension, ballDimension); _spriteBatch.Draw(_ballTexture, ballRectangle, Color.White); // End the sprite batch _spriteBatch.End(); base.Draw(gameTime); }
Этот метод заливает экран зеленым, а затем рисует фон и мяч на точке пенальти. Первый метод spriteBatch Draw рисует фон, подогнанный к размеру окна, второй метод рисует мяч на точке пенальти. Движения здесь пока нет – надо его добавить.
Движение мяча
Чтобы мяч двигался, нужно пересчитывать его местоположение в каждой итерации цикла и рисовать его на новом месте. Высчитаем новую позицию в методе Update:
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); // TODO: Add your update logic here _ballPosition -= 3; _ballRectangle.Y = _ballPosition; base.Update(gameTime); }
Позиция мяча обновляется в каждом цикле путем вычитания 3 пикселей. Переменные _screenWidth, _screenHeight, _backgroundRectangle, _ballRectangle и _ballPosition инициализируются в методе ResetWindowSize:
private void ResetWindowSize() { _screenWidth = Window.ClientBounds.Width; _screenHeight = Window.ClientBounds.Height; _backgroundRectangle = new Rectangle(0, 0, _screenWidth, _screenHeight); _initialBallPosition = new Vector2(_screenWidth / 2.0f, _screenHeight * 0.8f); var ballDimension = (_screenWidth > _screenHeight) ? (int)(_screenWidth * 0.02) : (int)(_screenHeight * 0.035); _ballPosition = (int)_initialBallPosition.Y; _ballRectangle = new Rectangle((int)_initialBallPosition.X, (int)_initialBallPosition.Y, ballDimension, ballDimension); }
Этот метод сбрасывает все переменные, зависящие от размера окна. Он вызывается в методе Initialize.
protected override void Initialize() { // TODO: Add your initialization logic here ResetWindowSize(); Window.ClientSizeChanged += (s, e) => ResetWindowSize(); base.Initialize(); }
Этот метод вызывается в двух местах: в начале и каждый раз, когда изменяется размер окна. Если вы запустите программу, вы заметите, что мяч движется прямо, но не останавливается с окончанием поля. Нужно переместить мяч, когда он залетает в ворота с помощью следующего кода:
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); // TODO: Add your update logic here _ballPosition -= 3; if (_ballPosition < _goalLinePosition) _ballPosition = (int)_initialBallPosition.Y; _ballRectangle.Y = _ballPosition; base.Update(gameTime); }
Переменная _goalLinePosition также инициализируется в методе ResetWindowSize.
_goalLinePosition = _screenHeight * 0.05;
В методе Draw нужно еще убрать все вычисления:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Green); var rectangle = new Rectangle(0, 0, _screenWidth, _screenHeight); // Begin a sprite batch _spriteBatch.Begin(); // Draw the background _spriteBatch.Draw(_backgroundTexture, rectangle, Color.White); // Draw the ball _spriteBatch.Draw(_ballTexture, _ballRectangle, Color.White); // End the sprite batch _spriteBatch.End(); base.Draw(gameTime); }
Движение мяча перпендикулярно линии ворот. Если вы хотите перемещать мяч под углом, создайте переменную _ballPositionX и увеличивайте ее (для движения вправо) или уменьшайте (для движения влево). Еще лучший вариант – использовать Vector2 для положения мяча:
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); // TODO: Add your update logic here _ballPosition.X -= 0.5f; _ballPosition.Y -= 3; if (_ballPosition.Y < _goalLinePosition) _ballPosition = new Vector2(_initialBallPosition.X,_initialBallPosition.Y); _ballRectangle.X = (int)_ballPosition.X; _ballRectangle.Y = (int)_ballPosition.Y; base.Update(gameTime); }
Если вы запустите программу теперь, то увидите, что мяч летит под углом. Следующая задача – приделать управление пальцем.
Реализуем управление
В нашей игре управление осуществляется пальцем. Движение пальца задает направление и силу удара.
В MonoGame сенсорные данные получают с помощью класса TouchScreen. Вы можете использовать сырые данные или Gestures API. Сырые данные обеспечивают большую гибкость, поскольку вы получаете доступ ко всей информации, Gestures API трансформирует сырые данные в жесты, и вы можете отфильтровать только те, которые вам требуются.
В нашей игре нам нужен только щелчок и, поскольку Gestures API поддерживает такое движение, мы воспользуемся ей. Первым делом, обзначим, каким жестом мы будем пользоваться:
TouchPanel.EnabledGestures = GestureType.Flick | GestureType.FreeDrag;
Будут обрабатываться только щелчки и перетаскивания. Далее в методе Update обработаем жесты:
if (TouchPanel.IsGestureAvailable) { // Read the next gesture GestureSample gesture = TouchPanel.ReadGesture(); if (gesture.GestureType == GestureType.Flick) { … } }
Включим щелчок в методе Initialize:
protected override void Initialize() { // TODO: Add your initialization logic here ResetWindowSize(); Window.ClientSizeChanged += (s, e) => ResetWindowSize(); TouchPanel.EnabledGestures = GestureType.Flick; base.Initialize(); }
До сих пор мяч катился все время, пока игра выполнялась. Используйте переменную _isBallMoving, чтобы сообщить игре, когда мяч движется. В методе Update, когда будет обнаружен щелчок, установите _isBallMoving равным True, и движение начнется. Когда мяч перечет линию ворот, установите _isBallMoving в False и верните мяч в исходное положение:
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); // TODO: Add your update logic here if (!_isBallMoving && TouchPanel.IsGestureAvailable) { // Read the next gesture GestureSample gesture = TouchPanel.ReadGesture(); if (gesture.GestureType == GestureType.Flick) { _isBallMoving = true; _ballVelocity = gesture.Delta * (float)TargetElapsedTime.TotalSeconds / 5.0f; } } if (_isBallMoving) { _ballPosition += _ballVelocity; // reached goal line if (_ballPosition.Y < _goalLinePosition) { _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y); _isBallMoving = false; while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture(); } _ballRectangle.X = (int) _ballPosition.X; _ballRectangle.Y = (int) _ballPosition.Y; } base.Update(gameTime); }
Скорость мяча теперь уже не константа, программа использует переменную _ballVelocity для установки скорости по осям x и y. Gesture.Delta возвращает изменение движения со времени последнего обновления. Чтобы подсчитать скорость щелчка, умножьте этот вектор на TargetElapsedTime.
Если мяч движется, вектор _ballPosition изменяется исходя из скорости (в пикселях за фрейм) до тех пор, пока мяч не достигнет линии ворот. Следующий код останавливает мяч и удаляет все жесты из входной очереди:
_isBallMoving = false; while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture();
Теперь если вы запустите мяч, он полетит по направлению щелчка и с его скоростью. Однако тут есть одна проблема: программа не смотрит, где на экране произошел щелчок. Вы можете щелкнуть где угодно, и мяч начнет движение. Решение – использовать сырые данные, получить точку прикосновения и посмотреть, находится ли она около мяча. Если да, жест устанавливает переменную _isBallHit:
TouchCollection touches = TouchPanel.GetState(); if (touches.Count > 0 && touches[0].State == TouchLocationState.Pressed) { var touchPoint = new Point((int)touches[0].Position.X, (int)touches[0].Position.Y); var hitRectangle = new Rectangle((int)_ballPositionX, (int)_ballPositionY, _ballTexture.Width, _ballTexture.Height); hitRectangle.Inflate(20,20); _isBallHit = hitRectangle.Contains(touchPoint); }
Тогда движение начинается, только если _isBallHit равна True:
if (TouchPanel.IsGestureAvailable && _isBallHit)
Имеется еще одна проблема. Если вы ударите мяч слишком медленно или не в ту сторону, игра закончится, так как мяч не пересечет линию ворот и не вернется в исходную позицию. Необходимо установить предельное время движения мяча. Когда таймаут достигнут, игра начинается снова:
if (gesture.GestureType == GestureType.Flick) { _isBallMoving = true; _isBallHit = false; _startMovement = gameTime.TotalGameTime; _ballVelocity = gesture.Delta*(float) TargetElapsedTime.TotalSeconds/5.0f; } ... var timeInMovement = (gameTime.TotalGameTime - _startMovement).TotalSeconds; // reached goal line or timeout if (_ballPosition.Y <' _goalLinePosition || timeInMovement > 5.0) { _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y); _isBallMoving = false; _isBallHit = false; while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture(); }
Добавление вратаря
Наша игра работает – сделаем теперь ее посложнее, добавив вратаря, который будет двигаться после того, как мы ударим мяч. Вратарь представляет из себя рисунок в формате PNG, предварительно его откомпилируем.
Вратарь загружается в методе LoadContent:
protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. _spriteBatch = new SpriteBatch(GraphicsDevice); // TODO: use this.Content to load your game content here _backgroundTexture = Content.Load<Texture2D>("SoccerField"); _ballTexture = Content.Load<Texture2D>("SoccerBall"); _goalkeeperTexture = Content.Load<Texture2D>("Goalkeeper"); }
Отрисуем его в методе Draw
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Green); // Begin a sprite batch _spriteBatch.Begin(); // Draw the background _spriteBatch.Draw(_backgroundTexture, _backgroundRectangle, Color.White); // Draw the ball _spriteBatch.Draw(_ballTexture, _ballRectangle, Color.White); // Draw the goalkeeper _spriteBatch.Draw(_goalkeeperTexture, _goalkeeperRectangle, Color.White); // End the sprite batch _spriteBatch.End(); base.Draw(gameTime); }
_goalkeeperRectangle – прямоугольник вратаря в окне. Он изменяется в методе Update:
protected override void Update(GameTime gameTime) { … _ballRectangle.X = (int) _ballPosition.X; _ballRectangle.Y = (int) _ballPosition.Y; _goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY, _goalKeeperWidth, _goalKeeperHeight); base.Update(gameTime); }
Переменные _goalkeeperPositionY, _goalKeeperWidth и _goalKeeperHeight fields обновляются в методе ResetWindowSize:
private void ResetWindowSize() { … _goalkeeperPositionY = (int) (_screenHeight*0.12); _goalKeeperWidth = (int)(_screenWidth * 0.05); _goalKeeperHeight = (int)(_screenWidth * 0.005); }
Первоначальное положение вратаря – по центру окна перед линией ворот:
_goalkeeperPositionX = (_screenWidth - _goalKeeperWidth)/2;
Вратарь начинает движение вместе с мячом. Он совершает гармонические колебания из стороны в сторону:
X = A * sin(at + δ),
Где А – амплитуда колебаний (ширина ворот), t – период колебаний, а δ – случайная величина, чтобы игрок не мог предугадать движение вратаря.
Коэффициенты вычисляются в момент удара по мячу:
if (gesture.GestureType == GestureType.Flick) { _isBallMoving = true; _isBallHit = false; _startMovement = gameTime.TotalGameTime; _ballVelocity = gesture.Delta * (float)TargetElapsedTime.TotalSeconds / 5.0f; var rnd = new Random(); _aCoef = rnd.NextDouble() * 0.005; _deltaCoef = rnd.NextDouble() * Math.PI / 2; }
Коэффициент а – скорость вратаря, число между 0 и 0.005, представляющее скорость между 0 и 0.3 пикселя в секунду. Коэффициент дельта – число между 0 и π/2. Когда двигается мяч, позиция вратаря обновляется:
if (_isBallMoving) { _ballPositionX += _ballVelocity.X; _ballPositionY += _ballVelocity.Y; _goalkeeperPositionX = (int)((_screenWidth * 0.11) * Math.Sin(_aCoef * gameTime.TotalGameTime.TotalMilliseconds + _deltaCoef) + (_screenWidth * 0.75) / 2.0 + _screenWidth * 0.11); … }
Амплитуда движения – _screenWidth * 0.11 (ширина ворот). Добавьте (_screenWidth * 0.75) / 2.0 + _screenWidth * 0.11 к результату, чтобы вратарь двигался перед воротами.
Проверка, пойман ли мяч и добавление счета
Чтобы проверить, пойман мяч или нет, нужно посмотреть, пересекаются ли прямоугольники вратаря и мяча. Сделаем это в методе Update после вычисления позиций:
_ballRectangle.X = (int)_ballPosition.X; _ballRectangle.Y = (int)_ballPosition.Y; _goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY, _goalKeeperWidth, _goalKeeperHeight); if (_goalkeeperRectangle.Intersects(_ballRectangle)) { ResetGame(); }
ResetGame возвращает игру в исходное состояние:
private void ResetGame() { _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y); _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2; _isBallMoving = false; _isBallHit = false; while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture(); }
Теперь нужно проверить, попал ли мяч в ворота. Сделаем это, когда мяч пересекает линию:
var isTimeout = timeInMovement > 5.0; if (_ballPosition.Y < _goalLinePosition || isTimeout) { bool isGoal = !isTimeout && (_ballPosition.X > _screenWidth * 0.375) && (_ballPosition.X < _screenWidth * 0.623); ResetGame(); }
Чтобы добавить ведение счета, в игру нужно добавить новый объект – шрифт, которым будут писаться цифры. Шрифт – это XML файл, описывающий шрифт: вид, размер, начертание и т.д. В игре мы будем использовать такой шрифт:
<?xml version="1.0" encoding="utf-8"?> <XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics"> <Asset Type="Graphics:FontDescription"> <FontName>Segoe UI</FontName> <Size>24</Size> <Spacing>0</Spacing> <UseKerning>false</UseKerning> <Style>Regular</Style> <CharacterRegions> <CharacterRegion> <Start> </Star> <End></End> </CharacterRegion> </CharacterRegions> </Asset> </XnaContent>
Вы должны скомпилировать этот шрифт и добавить получившийся XNB файл в папку Content вашего проекта:
_soccerFont = Content.Load<SpriteFont>("SoccerFont");
В ResetWindowSize сбросим позицию счета:
var scoreSize = _soccerFont.MeasureString(_scoreText); _scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0);
Для хранения счета определим две переменные: _userScore и _computerScore. _userScore увеличивается, когда игрок забьет гол, _computerScore – когда игрок промахнется, истечет лимит времени или вратарь поймает мяч.
if (_ballPosition.Y < _goalLinePosition || isTimeout) { bool isGoal = !isTimeout && (_ballPosition.X > _screenWidth * 0.375) && (_ballPosition.X < _screenWidth * 0.623); if (isGoal) _userScore++; else _computerScore++; ResetGame(); } … if (_goalkeeperRectangle.Intersects(_ballRectangle)) { _computerScore++; ResetGame(); }
ResetGame пересоздает текст счета и устанавливает его позицию:
private void ResetGame() { _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y); _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2; _isBallMoving = false; _isBallHit = false; _scoreText = string.Format("{0} x {1}", _userScore, _computerScore); var scoreSize = _soccerFont.MeasureString(_scoreText); _scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0); while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture(); }
_soccerFont.MeasureString измеряет длину строки счета, эта величина используется для вычисления позиции. Счет рисуется в методе Draw:
protected override void Draw(GameTime gameTime) { … // Draw the score _spriteBatch.DrawString(_soccerFont, _scoreText, new Vector2(_scorePosition, _screenHeight * 0.9f), Color.White); // End the sprite batch _spriteBatch.End(); base.Draw(gameTime); }
Включение огней стадиона
В качестве последнего штриха добавим в игру включение огней стадиона, когда в помещении станет темно. Воспользуемся для этого датчиком освещенности, который есть сейчас во многих ультрабуках и трансформерах. Чтобы задействовать датчик, можно воспользоваться Windows API Code Pack для Microsoft .NET Framework, но мы пойдем другим путем: используем WinRT Sensor API. Хотя этот API написан для Windows 8, он может использоваться и в десктопных приложениях.
Выделите свой проект в Solution Explorer, нажмите правой кнопкой и выберите Unload Project. Затем еще раз нажмите правую кнопку – Edit project. В первой PropertyGroup добавьте тег TargetPlatFormVersion:
<PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> … <FileAlignment>512</FileAlignmen> <TargetPlatformVersion>8.0</TargetPlatformVersion> </PropertyGroup>
Нажмите еще раз правую кнопку и выберите Reload Project. Visual Studio перезагрузит проект. Когда выдобавите новую ссылку на проект, вы увидите вкладку Windows в Reference Manager, как показано на рисунке.
Добавьте ссылку на Windows в проект. Это также потребует добавления ссылки на System.Runtime.WindowsRuntime.dll.
Теперь напишем код детектирования датчика освещенности:
LightSensor light = LightSensor.GetDefault(); if (light != null) {
Если датчик присутствует, вы получите ненулевое значение, которое можно использовать для определения освещенности:
LightSensor light = LightSensor.GetDefault(); if (light != null) { light.ReportInterval = 0; light.ReadingChanged += (s,e) => _lightsOn = e.Reading.IlluminanceInLux < 10; }
Если показание менее 10, переменная _lightsOn равна True и фон будет рисоваться несколько по-другому. Если вы посмотрите на spriteBatch в методе Draw, то увидите, что ее третий параметр – цвет. Раньше мы всегда использовали белый, при этом цвет фона не меняется. Если использовать любой другой цвет, то цвет фона изменится. Выберем зеленый, когда огни выключены и белый, когда включены. Внесем изменения в метод Draw:
_spriteBatch.Draw(_backgroundTexture, rectangle, _lightsOn ? Color.White : Color.Green);
Теперь наше поле будет темно-зеленым, когда огни выключены и светло-зеленым, когда включены.
Разработка нашей игры закончена. Конечно, в ней надо кое-что доделать: придумать анимацию при забитом мяче, добавить отскок мяча от штанги и так далее. Пусть это будет вашим домашним заданием.
Оригинал статьи на сайте Intel Developer Zone
ссылка на оригинал статьи http://habrahabr.ru/company/intel/blog/227667/
Добавить комментарий