Продолжаем работать над игрой. В этой главе мы сформируем новый проект, в котором выведем на экран три разных мячика, а потом будем перемещать их в пространстве. Заметьте, что подход в представлении и освещении исходных кодов всех оставшихся проектов с этой главы несколько меняется. За время чтения этой книги вы уже хорошо поднаторели в программировании игр, и разбирать каждую точку с запятой в коде программы смысла не имеет. Поэтому предлагается разделить все нововведения на разделы и рассматривать их в порядке убывания, а в конце всей главы изучать полный исходный код класса Game1, где, как всегда, все модификации кода выделены жирным шрифтом.
18.1. Задаем скорость движения модели
В классе ModelClass добавляется одна новая переменная speed, с помощью которой мы задаем скорость движения объектов в пространстве.
public float speed; … public ModelClass() { position = new Vector3(0, 0, 0); speed = 15.0f / 60.0f; }
Переменная speed инициализируется значением 15.0f/60.0f. В данном случае число 60 – это количество секунд в одной минуте. Такая конструкция выбора скорости очень часто встречается в играх. Для класса ModelClass это все нововведения. Переходим к исходному коду класса Game1, где нам необходимо создать массив объектов класса ModelClass, а затем реализовать механизм перемещения объектов сверху вниз.
18.2. Создаем массив данных
Сначала нужно объявить массив объектов ball класса ModelClass.
private ModelClass [] ball = new ModelClass[3]; Random rand = new Random();
Попутно происходит объявление объекта rand. Этот объект rand позволит в программе генерировать случайные числа. В двухмерной игре, как вы помните, мы также применяли данный объект. Далее в конструкторе класса Game1 происходит создание массива объектов ball при помощи цикла for.
for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass(); }
18.3. Инициализация и установка моделей на позиции
Теперь переходим к методу LoadGraphicsContent(), который отвечает за загрузку в игру различных графических данных.
Модель мячика, взятая с сайта Turbosquid.com, изначально имеет только два цвета – серый и синий. Чтобы разнообразить игру, я покрасил средствами 3ds Max шашки синего цвета в зеленый и красный цвета. Таким образом, у нас теперь имеется три разных мячика с тремя разными цветами и тремя разными названиями: * Мячик Soccerball – это базовая модель с синими шашками; * Мячик SoccerballGreen – эта модель с зелеными шашками; * Мячик SoccerballRed – этот мяч имеет красные шашки. В исходном коде метода LoadGraphicsContent() с помощью цикла for и метода rand.Next() генерируются случайные координаты в пространстве по всем трем осям для вывода мячей на игровые позиции. В этом блоке кода вас может заинтересовать выбор позиции по оси X.
Дело в том, что метод rand.Next() может генерировать только положительные числа, но нам необходимо иметь еще и отрицательные значения. Как мы помним, каждая модель в пространстве наделяется своей собственной локальной системой координат, где нулевая точка отсчета модели находится в центре экрана (при нулевых значениях координат по осям X и Y для точки просмотра сцены). Это значит, что движение модели в левую сторону – это движение модели в отрицательной плоскости оси X. Движение модели в правую сторону – это движение в положительной части оси Х. По оси Y для движения вверх нужно выбирать положительные значения, а для движения вниз – отрицательные значения. Выбирая позицию для мячика по оси Х, для минимального значения задается диапазон чисел от 0 до –60. Для максимального числа используем значения от 0 до 60, но уже в положительной плоскости оси Х. Дополнительно по оси Z устанавливается диапазон чисел от –20 до +100, что позволяет удалять или приближать мячики в пространстве в пределах этих заданных значений. Посмотрите на рис. 18.1, где показана техника выбора позиции мячей на экране.
Неплохо было бы вам на этом этапе добавить в код класс для работы с текстом и выводить на экран все установленные значения для мячей, камер, матриц и т. д., для того чтобы четко понимать, как происходят вычисления всех позиций в локальной системе координат модели.
18.4. Установка матриц
Матрицы преобразований остаются у нас прежними. Сначала задается точка просмотра сцены с помощью видовой матрицы.
После чего формируется цикл for, где устанавливается мировая матрица для трех мячиков, а все итоговые значения матриц передаются в качестве параметров в метод DrawModel().
Рис. 18.1. Выбор позиции на экране
for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world, view, proj); }
18.5. Формируем метод для перемещения моделей
Теперь пришло время для создания метода, который будет перемещать мячик в пространстве. Здесь алгоритм действий простой: необходимо с помощью ранее созданной переменной speed изменять позицию мячей по оси Y, так, как мы это делали в игре «Летящие в прерии».
void MoveBall() { for (int i = 0; ball.Length > i; i++) { ball[i].position.Y -= ball[i].speed; } }
Мячи в игре падают сверху вниз, а значит, нам нужно отнимать от текущих позиций объектов заданное количество пикселей (15.0f/60.0f). Метод MoveBall() вызывается в методе Update() класса Game1 на каждой новой итерации игрового цикла. Каждая новая позиция объекта, представленная переменной position, передается в мировую матрицу, где для движения объекта в работу включается механизм переноса всех вершин модели в пространстве (те самые мировые преобразования).
18.6. Случайный выбор позиции на экране
Движение объектов на экране происходит сверху вниз. Через определенный промежуток времени все мячики исчезнут с экрана; чтобы этого не происходило, добавим в исходный код простой метод под названием GamePadClick(). В этом методе щелчок правой кнопкой джойстика Triggers.Right в игре позволит нам выбрать для мячей новые позиции на экране телевизора.
Методика выбора позиции аналогична той методике, которую мы рассмотрели в начале этой главы. В дальнейшем мы модифицируем исходный код этого метода и будем его использовать уже для выстрелов по мячикам.
На данный момент метод GamePadClick() работает не идеально. Если вы нажмете на правую кнопку Triggers.Right и будете ее удерживать, то мячики постоянно будут изменять свое местоположение, поскольку мы не определили механизм обработки событий для нажатой, но еще не отпущенной кнопки. То есть фактически нажатие сейчас кнопки Triggers. Right запускает своего рода цикл, и пока кнопка нажата, мячики будут менять свое положение в пространстве. Чтобы этого не происходило, нужно создать механизм, который будет реагировать только на единичное нажатие кнопки. Сейчас подобный механизм нам пока не нужен, но уже в следующих главах мы рассмотрим и создадим исходный код для обработки таких игровых ситуаций.
//========================================================================= /// /// Листинг 18.1 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Проект: BallArray /// Класс Game1 /// Загружаем в игру три мячика /// //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace BallArray { public class Game1 : Microsoft.Xna.Framework.Game { private enum CurentGameState { … } CurentGameState gameState = CurentGameState.GameScreen; GraphicsDeviceManager graphics; ContentManager content; int screenWidth, screenHeight; Matrix view; Matrix proj; Matrix world; static float aspectRatio; static float FOV = MathHelper.PiOver4; static float nearClip = 1.0f; static float farClip = 1000.0f; private ModelClass[] ball = new ModelClass[3]; Random rand = new Random(); /// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass(); } } /// /// Инициализация /// protected override void Initialize() { aspectRatio = (float)screenWidth / (float)screenHeight; base.Initialize(); } /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { ball[0].Load(content, «Content\\Models\\Soccerball»); ball[1].Load(content, «Content\\Models\\SoccerballGreen»); ball[2].Load(content, «Content\\Models\\SoccerballRed»); for (int i = 0; ball.Length > i; i++) { ball[i].position.X = rand.Next(-(rand.Next(0, 60)), rand.Next(0, 60)); ball[i].position.Y = rand.Next(50, 80); ball[i].position.Z = -(rand.Next(20, 100)); } } } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.HelpScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { if(GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); MoveBall(); GamePadClick(); break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Update(gameTime); } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(new Color(159, 147, 207)); switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 150.0f), Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world, view, proj); } break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Draw(gameTime); } /// /// Движение мячей /// void MoveBall() { for (int i = 0; ball.Length > i; i++) { ball[i].position.Y -= ball[i].speed; } } /// /// Прицел /// void GamePadClick() { GamePadState currentState = GamePad.GetState(PlayerIndex.One); for (int i = 0; ball.Length > i; i++) { if (currentState.Triggers.Right > 0.5f) { ball[i].position.X = rand.Next(-(rand.Next(0, 60)), rand.Next(0, 60)); ball[i].position.Y = rand.Next(50, 80); ball[i].position.Z = -(rand.Next(20, 100)); } } } } }
Стреляем по целям
Наша задача на данном этапе заключается в добавлении в игру прицела и создании механизма обработки попаданий в цель. Как обычно, продолжаем усовершенствовать пример из предыдущей главы, и наш новый проект носит название CursorBall.
19.1. Класс для работы с двухмерными изображениями
В любой трехмерной игре обязательно применяются двухмерные изображения. Игра «Футбольный стрелок» в этом плане не исключение. Для представления на экране прицела мы будем использовать спрайт, нарисованный в виде окружности с рисками и вырезанным фоном (рис. 19.1). Для работы с прицелом в проект добавится новый класс Sprite, который мы создали за время работы над двухмерной игрой. Как видите, очень полезно создавать классы с фундаментальным подходом, для того чтобы в дальнейшем эти классы можно было использовать многократно, тем более что с каждой новой игрой этот класс можно усовершенствовать.
Рис. 19.1. Изображение прицела
19.2. Задаем радиус для мячей
Перейдем на время к классу ModelClass и добавим в его исходный код одну новую переменную radius.
public float radius; … public ModelClass() { position = new Vector3(0, 0, 0); speed = 15.0f / 60.0f; radius = 8.0f; }
Как видно из названия переменной, она предназначается для определения радиуса мячиков. Впоследствии для определения пересечения моделей и прицела будет использоваться класс BoundingSphere. Этот класс позволяет создавать ограничивающую сферу для моделей, и именно пересечение мушки с этой сферой будет означать, что мячик взят на прицел. Радиус в восемь единиц для мячика больше, чем это нужно на самом деле, но поскольку мячи постоянно находятся в движении, то попасть в цель будет не так просто. Поэтому мы немного помогаем игроку и увеличиваем зону пересечения прицела и модели.
19.3. Рисуем на экране прицел
Прицел в игре представлен классом Sprite, поэтому нужно объявить и создать объект этого класса, а заодно и не забываем о классе SpriteBatch, который необходим для работы с 2D-графикой.
SpriteBatch spriteBatch; Sprite cursor;
В конструкторе класса Game1 создаем объект cursor класса Sprite.
cursor = new Sprite();
В методе LoadGraphicsContent() загружаем в программу графическое изображение прицела из каталога проекта и папки Content\Textures.
spriteBatch = new SpriteBatch(graphics.GraphicsDevice); cursor.Load(content, «Content\\Textures\\cursor»);
В методе Draw() после вывода на экран мячей добавляем отрисовку на экране прицела.
И в этом месте очень важна очередность вывода графики на экран. Не забываем о том, что все то, что устанавливается для рисования на экране позже в исходном коде программы, соответственно и рисуется позже, поверх предыдущего графического контента.
19.4. Получаем координаты прицела
Основной проблемой в механизме обработки выстрелов по мячам является различие координат в положении на экране двухмерного прицела и трехмерной модели. Как вы знаете, двухмерная плоскость имеет свою систему координат, а трехмерная плоскость – свою. У нас в игре одним из основных механизмов игровой логики является обработка выстрелов в мячики, где нам необходимо сопоставлять текущие координаты спрайта с текущими координатами модели. И эти координаты будут абсолютно разными, поскольку системы отсчета у спрайтов и моделей разные. В связи с этим нам нужно создать механизм, который может перенести координаты спрайта в трехмерную плоскость. В справочной информации по студии XNA Game Studio Express имеется показательный пример получения и переноса двухмерных координат спрайта в трехмерную плоскость. Мы воспользуемся этим примером, который состоит из двух частей. Первая часть примера представлена методом GetPickRay(), где происходят получение текущих координат прицела и перенос их в трехмерное пространство.
Ray GetPickRay() { // задаем позицию прицела в центре изображения мушки float cursorX = cursor.spritePosition.X + cursor.spriteTexture.Width/2; float cursorY = cursor.spritePosition.Y + cursor.spriteTexture.Height/2; Vector3 nearsource = new Vector3(cursorX, cursorY, 0f); Vector3 farsource = new Vector3(cursorX, cursorY, 1f); // мировая матрица, все значения ставим в ноль world = Matrix.CreateTranslation(0, 0, 0); // матрица вида view = Matrix.CreateLookAt(camera, Vector3.Zero, Vector3.Up); // матрица проекции proj=Matrix.CreatePerspectiveFieldOfView(FOV,aspectRatio,nearClip,farClip); // ближняя точка Vector3 nearPoint = graphics.GraphicsDevice.Viewport.Unproject(nearsource, proj, view, world); // дальняя точка Vector3 farPoint = graphics.GraphicsDevice.Viewport.Unproject(farsource, proj, view, world); Vector3 direction = farPoint - nearPoint; direction.Normalize(); Ray pickRay = new Ray(nearPoint, direction); return pickRay; }
Сложный метод, но его можно рассматривать как часть объектно-ориентированного программирования, когда совсем не обязательно знать, как работает тот или иной метод или класс. Суть метода GetPickRay() заключается в переносе двухмерных координат прицела в трехмерное пространство посредством матричных преобразований, и здесь имеется один очень важный нюанс. Значения видовой и проекционной матриц для прицела должны быть идентичны значениям аналогичных матриц для моделей, с которыми в дальнейшем будут обрабатываться условия по совпадению координат. Если видовая и проекционная матрицы буду разными, то прицел и модели будут находиться в разных виртуальных измерениях! Поэтому как для мушки, так и для моделей значение видовой и проекционной матриц одинаково.
19.5. Целимся и стреляем
После того как вы сформировали метод GetPickRay() для переноса координат прицела из одной плоскости в другую, можно приступать к работе над вторым методом, который будет обрабатывать уже непосредственно столкновение объектов в пространстве. Для начала объявим в классе Game1 новый объект bb класса BoundingSphere.
public BoundingSphere[] bb = new BoundingSphere[3];
В отличие от класса BoundingBox, с которым мы имели дело во второй части этой книги, класс BoundingSphere создает не ограничивающий прямоугольник или куб, а ограничивающую сферу. Наши модели мячиков круглые, поэтому этот класс подходит для нас как нельзя кстати. Сейчас давайте «заберемся» прямо в исходный код метода GamePadClick(), который направлен на определение столкновений между прицелом и мячами, и по ходу его изучения прокомментируем суть работы этого метода.
В этом блоке кода мы обрабатываем выход прицела за пределы экрана со всех четырех сторон. Затем в теле метода GamePadClick() создается цикл, где на каждый мячик надевается своя сфера с радиусом, установленным в классе ModelClass.
for (int i = 0; bb.Length > i; i++) { bb[i].Center = ball[i].position; bb[i].Radius = ball[i].radius; }
Далее создается проверка условия для обработки нажатия правой кнопки Triggers.Right. Это условие в переводе на русский язык звучит так. Если правая кнопка Triggers.Right нажата и координаты прицела совпадают с координатами ограничивающей сферы одного из мячей, то происходит выстрел и попадание в цель, а мячик устанавливается на новую позицию на экране.
if (currentState.Triggers.Right > 0.5f) { Ray pickRay = GetPickRay(); Nullable result0 = pickRay.Intersects(bb[0]); if (result0.HasValue == true) {
В следующем блоке кода выбирается новая позиция на экране для всех мячей. Выбор позиции мячика аналогичен первоначальному выбору позиции при старте всей игры.
В идеале необходимо придумать и написать более сложную конструкцию кода, где мяч не просто устанавливается на новую позицию на экране телевизора, а именно подбивается вверх на 20–50 пикселей и затем вновь падает. Такой подход будет более реалистичным в игре, но эти действия выполняйте уже сами ;-) Обработку событий по пересечению сферы и прицела можно также поместить и в цикл, например следующим образом:
for (int i = 0; ball.Length > i; i++) { Nullable result = pickRay.Intersects(bb[i]); if (result.HasValue == true) { ball[i].position.X =rand.Next(-(rand.Next(0, 60)),rand.Next(0,60)); ball[i].position.Y = rand.Next(50, 80); ball[i].position.Z = -(rand.Next(20, 100)); } }
Но в дальнейшем мы будем вести подсчет попадания в мячи по каждому отдельному мячу, поэтому нам здесь цикл не подходит. В результате два рассмотренных метода представляют отлаженный механизм по переносу координт двухмерного прицела в трехмерную плоскость и обработке столкновений межу спрайтом и моделью. Этот механизм вам может понадобиться в различных играх, например в шутерах от первого или второго лица, где так же, как и нашей игре, присутствуют двухмерный прицел и множество различных трехмерных моделей.
//========================================================================= /// /// Листинг 19.1 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Проект: CursorBall /// Класс Game1 /// Подбиваем мячики /// //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace CursorBall { public class Game1 : Microsoft.Xna.Framework.Game { private enum CurentGameState { … } CurentGameState gameState = CurentGameState.GameScreen; GraphicsDeviceManager graphics; ContentManager content; int screenWidth, screenHeight; Matrix view; Matrix proj; Matrix world; static float aspectRatio; static float FOV = MathHelper.PiOver4; static float nearClip = 1.0f; static float farClip = 1000.0f; private ModelClass[] ball = new ModelClass[3]; Random rand = new Random(); SpriteBatch spriteBatch; Sprite cursor; public BoundingSphere[] bb = new BoundingSphere[3]; Vector3 camera = new Vector3(0.0f, 0.0f, 150.0f); /// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass(); } cursor = new Sprite(); } /// /// Инициализация /// protected override void Initialize() { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); aspectRatio = (float)screenWidth / (float)screenHeight; base.Initialize(); } /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { ball[0].Load(content, «Content\\Models\\Soccerball»); ball[1].Load(content, «Content\\Models\\SoccerballGreen»); ball[2].Load(content, «Content\\Models\\SoccerballRed»); for (int i = 0; ball.Length > i; i++) { ball[i].position.X = rand.Next(-(rand.Next(0, 60)), rand.Next(0, 60)); ball[i].position.Y = rand.Next(50, 80); ball[i].position.Z = -(rand.Next(20, 100)); } cursor.Load(content, «Content\\Textures\\cursor»); cursor.spritePosition = new Vector2(400, 300); } } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { … } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(new Color(159, 147, 207)); switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; view = Matrix.CreateLookAt(camera, Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world, view, proj); } spriteBatch.Begin(SpriteBlendMode.AlphaBlend); cursor.DrawSprite(spriteBatch); spriteBatch.End(); break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Draw(gameTime); } /// /// Движение мячей /// void MoveBall() { for (int i = 0; ball.Length > i; i++) { ball[i].position.Y -= ball[i].speed; } } /// /// Прицел /// void GamePadClick() { GamePadState currentState = GamePad.GetState(PlayerIndex.One); // GamePadThumbSticks Left if(currentState.ThumbSticks.Left.X < -0.65f) cursor.spritePosition.X -= 10; else if(currentState.ThumbSticks.Left.X > 0.65f) cursor.spritePosition.X += 10; if(currentState.ThumbSticks.Left.Y > 0.65f) cursor.spritePosition.Y -= 10; else if(currentState.ThumbSticks.Left.Y < -0.65f) cursor.spritePosition.Y += 10; // Обработка выхода прицела за пределы экрана if(cursor.spritePosition.X < 0) cursor.spritePosition.X = 0; else if(cursor.spritePosition.X > screenWidth – cursor.spriteTexture.Width) cursor.spritePosition.X = screenWidth - cursor.spriteTexture.Width; if(cursor.spritePosition.Y < 0) cursor.spritePosition.Y = 0; else if(cursor.spritePosition.Y > screenHight –cursor.spriteTexture. Hight) cursor.spritePosition.Y = screenHight - cursor.spriteTexture.Hight; // Задаем сферу для мячей for (int i = 0; bb.Length > i; i++) { bb[i].Center = ball[i].position; bb[i].Radius = ball[i].radius; } if (currentState.Triggers.Right > 0.5f) { Ray pickRay = GetPickRay(); Nullable result0 = pickRay.Intersects(bb[0]); if (result0.HasValue == true) { ball[0].position.X = rand.Next(-(rand.Next(0, 60)), rand.Next(0, 60)); ball[0].position.Y = rand.Next(50, 80); ball[0].position.Z = -(rand.Next(20, 100)); } Nullable result1 = pickRay.Intersects(bb[1]); if (result1.HasValue == true) { ball[1].position.X = rand.Next(-(rand.Next(0, 60)), rand.Next(0, 60)); ball[1].position.Y = rand.Next(50, 80); ball[1].position.Z = -(rand.Next(20, 100)); } Nullable result2 = pickRay.Intersects(bb[2]); if (result2.HasValue == true) { ball[2].position.X = rand.Next(-(rand.Next(0, 60)), rand.Next(0, 60)); ball[2].position.Y = rand.Next(50, 80); ball[2].position.Z = -(rand.Next(20, 100)); } } // Поставим мячи на новые позиции // Этот блок кода мы в дальнейшем удалим if (currentState.Triggers.Left > 0.5f) { for (int i = 0; ball.Length > i; i++) { ball[i].position.X = rand.Next(-(rand.Next(0, 60)), rand.Next(0, 60)); ball[i].position.Y = rand.Next(50, 80); ball[i].position.Z = -(rand.Next(20, 100)); } } } /// /// Преобразовываем координаты /// Ray GetPickRay() { float cursorX = cursor.spritePosition.X + cursor.spriteTexture.Width/2; float cursorY = cursor.spritePosition.Y + cursor.spriteTexture.Height/2; Vector3 nearsource = new Vector3(cursorX, cursorY, 0f); Vector3 farsource = new Vector3(cursorX, cursorY, 1f); world = Matrix.CreateTranslation(0, 0, 0); view = Matrix.CreateLookAt(camera, Vector3.Zero, Vector3.Up); proj=Matrix.CreatePerspectiveFieldOfView(FOV,aspectRatio,nearClip,farClip); Vector3 nearPoint = graphics.GraphicsDevice.Viewport.Unproject(nearsource, proj, view, world); Vector3 farPoint = graphics.GraphicsDevice.Viewport.Unproject(farsource, proj, view, world); Vector3 direction = farPoint - nearPoint; direction.Normalize(); Ray pickRay = new Ray(nearPoint, direction); return pickRay; } } }