Привет всем начинающим геймдевелоперам и просто хорошим людям. Сегодня, я хочу познакомить вас с замечательным фреймворком XNA (набор инструментов с управляемой средой времени выполнения dotNet). Программировать мы будем на C#.
Для того, чтобы познакомить вас с XNA ближе, я предлагаю написать простую «музыкальную» 2D игрушку. Остальное под катом.
Microsoft XNA (англ. XNA's Not Acronymed) — набор инструментов с управляемой средой времени выполнения (.NET), созданный Microsoft, облегчающий разработку и управление компьютерными играми. XNA стремится освободить разработку игр от написания «повторяющегося шаблонного кода»
1) Свежий DirectX (например Июнь 2010)
2) Microsoft Visual C# 2010 EXPRESS (бесплатная лицензия)
3) Microsoft XNA Game Studio 4.0
Механика игры проста до безумия. Смысл будет построен на музыке, в случае с этой игрой, будет использована композиция Исаака Шепарда — Leaves in the Wind. Нужно будет ловить мышкой «ноты», скорость и кол-во которых будут зависимы от текущей позиции в музыки, грубо говоря игровой «визуализатор». Для разнообразия существуют 5 тип нот: обычные, красные (враги), пурпурные (мощь), мигающие (превращает все в желтые), желтые (увеличивает скорость набора очков и размеры).
Собираем
Для начала ставим все необходимые компоненты по порядку, затем запускаем Microsoft Visual C# 2010 EXPRESS и создаем проект Windows Game (4.0) и называем его music_catch:
Создается пустой проект, который при компиляции только чистит «экран» приложения, давайте более подробно рассмотрим структуру нового проекта.
Проект music_catch — «логика» нашего приложения.
Game1.cs — главный класс приложения, унаследован он от Microsoft.Xna.Framework.Game
Program.cs — «точка входа» в приложение, он нам не интересен.
Проект music_catchContent — «контент» нашего приложения, туда мы будем складывать ресурсы.
Более подробно взглянем на Game1.cs
В нем можно выделить основные функции, такие как:
Game1() — конструктор класса.
Initialize() — инициализация приложения.
LoadContent() — загрузка контента.
UnloadContent() — выгрузка контента.
Update(GameTime gameTime) — обновление логики приложения (например физики, etc)
Draw(GameTime gameTime) — отрисовка игры. ВНИМАНИЕ, любые операции с рисованием нужно проводить тут и только тут.
Пустой проект собран, идем дальше, добавляем ресурсы в приложение, все нужные ресурсы «кидаем» в папку music_catch\music_catchContent. В нашем случае — пять PNG файлов и одно музыкальное сопровождение. Добавляем это все в проект:
Там же создаем шрифт, в теле SpriteFont1.spritefont указываем имя и размер:
Создаем переменные для будущего контента:
И грузим его в LoadContent():
Кстати, подгружается контент следующим образом: вызывается Content.Load<>(«asset»);
В треугольных скобках указывается процессор контента, в нашем случае это Texture2D, Song, SpriteFont. Можно использовать свои процессоры, об этом я расскажу как-нибудь потом.
Контент подгружен, идем в конструктор Game1() и пишем:
Приложение инициализировано.
Теперь нам нужно создать контроллер системы частиц и сами частицы (ноты), которые мы будем виртуозно ловить мышкой.
Создаем два класса: Catcher (сами частицы) и CatcherHolder (система частиц).
Листинг Catcher с комментариями:
Листинг CatcherHolder с комментариями:
Листинг Constant.cs:
Объясню, что за загадочный аккумулятор и зачем он нужен. Поговорим о «музыкальном» спектре.
Музыкальный сигнал – пища для аудиосистемы. Точнее – не так. Динамики музыку не слушают, ее восстанавливает наш мозг, получая сложный сигнал, содержащий множество частотных составляющих.
Дак вот, идея такая, слушать «частоты» каждый Update и записывать их в какой-нибудь, например, VisualizationData. Проще говоря, в массив из 128 элементов, которые изменяются от 0f до 1f.
Как этим можно воспользоваться?
Каждый Update: значения в массиве меняются в соответствии с музыкой, нам нужно проверить все 128 элементов, если значение элемента больше чем 0.6f, вызываем Beat-функцию и передаем ей Wave (индекс элемента массива, в котором произошло событие). Все бы хорошо, можно в Beat создавать частичку-ноту. Но представим, что у нас выполняется три Update'а подряд, в котором в одном и том же индексе — значение > 0.6f, как итог будет 100500 частичек за секунду. Чтобы таких вещей не происходило, можно использовать аккумулятор. Смысл его прост: при Beat'е у ячейки массива-аккумулятора соотвествующего индексу Wave отнимается константа BEAT_COST. Каждый Update ко всем элементам аккумулятора прибавляется ACCUMULATE_SPEED. Перед тем, как вызвать Beat проверяется выполняется ли условие — значение аккумулятора > ACCOMULATOR_REACTION, если да, то вызываем Beat. Это решает проблему.
Кстати, BEAT_REACTION — значение, после которых нужно проверять, стоит ли вызывать Beat.
Дальше приведу полный листинг GameLogic (Game1). Много кода, но постараюсь расписать в комментариях.
Вот такая простенькая игрушка получается. На конечной машине пользователя должен быть установлен XNA 4.0 и .NET;
Ссылки: сама игра (директ) | исходники (директ) | XNA Framework End-user
Скриншот:
P.S. Идея не моя, такая игра уже была выпущена под flash. Эта игра писалась исключительно для статьи, соответственно дальнейшего развития она не получит.
P.S.S. Так же помогу разобраться в XNA / уроке, для этого пишите мне в личку на хабре или на контакты, которые есть в профиле.
Для того, чтобы познакомить вас с XNA ближе, я предлагаю написать простую «музыкальную» 2D игрушку. Остальное под катом.
Краткое описание на википедии
Microsoft XNA (англ. XNA's Not Acronymed) — набор инструментов с управляемой средой времени выполнения (.NET), созданный Microsoft, облегчающий разработку и управление компьютерными играми. XNA стремится освободить разработку игр от написания «повторяющегося шаблонного кода»
Что для этого нам будет нужно?
1) Свежий DirectX (например Июнь 2010)
2) Microsoft Visual C# 2010 EXPRESS (бесплатная лицензия)
3) Microsoft XNA Game Studio 4.0
Что предполагается разобрать и сделать на этом уроке?
- Подключить сборки XNA Framework
- Создать пустое приложение с закрашиванием фона
- Научиться подгружать контент
- Научиться работать со звуком
- Научиться работать с графикой
Какую игру мы будем реализовывать?
Механика игры проста до безумия. Смысл будет построен на музыке, в случае с этой игрой, будет использована композиция Исаака Шепарда — Leaves in the Wind. Нужно будет ловить мышкой «ноты», скорость и кол-во которых будут зависимы от текущей позиции в музыки, грубо говоря игровой «визуализатор». Для разнообразия существуют 5 тип нот: обычные, красные (враги), пурпурные (мощь), мигающие (превращает все в желтые), желтые (увеличивает скорость набора очков и размеры).
Собираем вещи пустой проект
Для начала ставим все необходимые компоненты по порядку, затем запускаем Microsoft Visual C# 2010 EXPRESS и создаем проект Windows Game (4.0) и называем его music_catch:
Создается пустой проект, который при компиляции только чистит «экран» приложения, давайте более подробно рассмотрим структуру нового проекта.
Проект music_catch — «логика» нашего приложения.
Game1.cs — главный класс приложения, унаследован он от Microsoft.Xna.Framework.Game
Program.cs — «точка входа» в приложение, он нам не интересен.
Проект music_catchContent — «контент» нашего приложения, туда мы будем складывать ресурсы.
Более подробно взглянем на Game1.cs
В нем можно выделить основные функции, такие как:
Game1() — конструктор класса.
Initialize() — инициализация приложения.
LoadContent() — загрузка контента.
UnloadContent() — выгрузка контента.
Update(GameTime gameTime) — обновление логики приложения (например физики, etc)
Draw(GameTime gameTime) — отрисовка игры. ВНИМАНИЕ, любые операции с рисованием нужно проводить тут и только тут.
Пустой проект собран, идем дальше, добавляем ресурсы в приложение, все нужные ресурсы «кидаем» в папку music_catch\music_catchContent. В нашем случае — пять PNG файлов и одно музыкальное сопровождение. Добавляем это все в проект:
Там же создаем шрифт, в теле SpriteFont1.spritefont указываем имя и размер:
<FontName>Segoe UI Mono</FontName>
<Size>14</Size>
Создаем переменные для будущего контента:
private List<Texture2D> MelList;
private Texture2D mouse;
private Song song;
private SpriteFont font;
И грузим его в LoadContent():
MelList = new List<Texture2D>();
for(int a = 1; a <= 5; a++)
MelList.Add(Content.Load<Texture2D>("mel" + a));
mouse = Content.Load<Texture2D>("mouse");
song = Content.Load<Song>("Leaves_in_the_Wind");
font = Content.Load<SpriteFont>("SpriteFont1");
Кстати, подгружается контент следующим образом: вызывается Content.Load<>(«asset»);
В треугольных скобках указывается процессор контента, в нашем случае это Texture2D, Song, SpriteFont. Можно использовать свои процессоры, об этом я расскажу как-нибудь потом.
Контент подгружен, идем в конструктор Game1() и пишем:
graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth = 800; // ширина приложения
graphics.PreferredBackBufferHeight = 600; // высота приложения
graphics.IsFullScreen = false; // флаг полноэкранного приложения
graphics.ApplyChanges(); // применяем параметры
Content.RootDirectory = "Content";
Приложение инициализировано.
Пишем «игровую логику»
Теперь нам нужно создать контроллер системы частиц и сами частицы (ноты), которые мы будем виртуозно ловить мышкой.
Создаем два класса: Catcher (сами частицы) и CatcherHolder (система частиц).
Листинг Catcher с комментариями:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace MusicCatch
{
public class Catcher
{
public Texture2D Texture { get; set; } // Текстура частицы
public Vector2 Position { get; set; } // Позиция частицы
public Vector2 Velocity { get; set; } // Скорость частицы
public float Angle { get; set; } // Угол поворота частицы
public float AngularVelocity { get; set; } // Угловая скорость частицы
public Color Color { get; set; } // Цвет частицы
public float Size { get; set; } // Размер частицы
public int TTL { get; set; } // Время жизни частицы
private float RComponent; // Красный компонент RGB
private float GComponent; // Зеленый компонент RGB
private float BComponent; // Синий компонент RGB
public int type; // Тип частицы
private Random random; // Генератор случайных чисел
public Catcher(Texture2D texture, Vector2 position, Vector2 velocity,
float angle, float angularVelocity, int type, float size, int ttl)
{
// Установка переменных из конструктора
Texture = texture;
Position = position;
Velocity = velocity;
Angle = angle;
AngularVelocity = angularVelocity;
this.type = type;
Size = size;
TTL = ttl;
SetType(type); // Установка цвета под определенный тип
}
public void ApplyImpulse(Vector2 vector) // Добавление импульса (используется бонусом)
{
Velocity += vector;
}
public void Update() // Обновление единичной частички
{
TTL--;
Position += Velocity;
Angle += AngularVelocity;
if (type != -1)
{
Velocity = new Vector2(Velocity.X, Velocity.Y - .1f);
Size = (10 + Velocity.Y) / 20;
if(Size > 0.8f) Size = 0.8f;
}
if (type == 0)
{
GComponent -= 0.005f;
BComponent += 0.005f;
Color = new Color(RComponent, GComponent, BComponent);
}
else if (type == 4)
{
Color = new Color((float)(1f * random.NextDouble()), (float)(1f * random.NextDouble()), (float)(1f * random.NextDouble()));
}
}
public void Draw(SpriteBatch spriteBatch) // Прорисовка частички
{
Rectangle sourceRectangle = new Rectangle(0, 0, Texture.Width, Texture.Height);
Vector2 origin = new Vector2(Texture.Width / 2, Texture.Height / 2);
spriteBatch.Draw(Texture, Position, sourceRectangle, Color,
Angle, origin, Size, SpriteEffects.None, 0f);
}
public void SetType(int type) // Установка цвета частички
{
this.type = type;
Color StartColor = new Color(1f, 1f, 1f);
switch (type)
{
case 0: StartColor = new Color(0f, 1f, 0f); break; // Обычная
case 1: StartColor = new Color(1f, 0f, 0f); break; // Красная
case 2: StartColor = new Color(1f, 0f, 1f); break; // Пурпурная
case 3: StartColor = new Color(1f, 1f, 0f); break; // Желтая
case 4: random = new Random(); break; // Мигающая
}
RComponent = ((int)StartColor.R) / 255f;
GComponent = ((int)StartColor.G) / 255f;
BComponent = ((int)StartColor.B) / 255f;
Color = new Color(RComponent, GComponent, BComponent);
if (type == -1)
{
Color = new Color(1f, 1f, 1f, 0.1f);
}
}
}
}
Листинг CatcherHolder с комментариями:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace MusicCatch
{
class CatcherHolder
{
private Random random; // Генератор случайных чисел
public List<Catcher> particles; // Массив частичек (Catcher)
private List<Texture2D> textures; // Массив текстур
public List<float> accomulator { get; set; } // Массив float-значений, что такое и зачем нужен accomulator — объясню чуть позже.
public CatcherHolder(List<Texture2D> textures)
{
this.textures = textures;
this.particles = new List<Catcher>();
random = new Random();
accomulator = new List<float>(); // Инициализируем массив и записываем во все 128 ячеек — 1.0f
for (int a = 0; a < 128; a++)
{
accomulator.Add(1.0f);
}
}
// Генерация одной частички
// Wave - волна, число от 0f до ширины экрана.
private Catcher GenerateNewParticle(float Wave)
{
Texture2D texture = textures[random.Next(textures.Count)]; // Берем случайную текстуру из массива
Vector2 position = new Vector2(Wave, 0); // Задаем позицию
Vector2 velocity = new Vector2((float)(random.NextDouble() - 0.5), (float)(random.NextDouble() * 10)); // Случайное ускорение, 0.5f для X и 10f для Y
float angle = 0; // Угол поворота = 0
float angularVelocity = 0.05f * (float)(random.NextDouble()*2 - 1 ); // Случайная скорость вращения
Color color = new Color(0f, 1f, 0f); // Зеленый цвет (изменится цвет уже в самом Catcher)
float size = (float)random.NextDouble()*.8f + .2f; // Случайный размер
int ttl = 400; // Время жизни в 400 (400 актов рисования живет частица, т.е. 400 / 60 — 6 с лишним секунд.
int type = 0; — изначальный тип 0
// Вероятность появления
if (random.Next(10000) > 9900) // враг
type = 1;
else if (random.Next(10000) > 9950) // желтый
type = 3;
else if (random.Next(10000) > 9997) // пурпурный
type = 2;
else if (random.Next(10000) > 9998) // мигающий
type = 4;
return new Catcher(texture, position, velocity, angle, angularVelocity, type, size, ttl); // Создаем частичку и возвращаем её
}
// Генерация желтых частичек при касании с красной частичкой
public void GenerateYellowExplossion(int x, int y, int radius)
{
Texture2D texture = textures[random.Next(textures.Count)];
Vector2 direction = Vector2.Zero;
float angle = (float)Math.PI * 2.0f * (float)random.NextDouble();
float length = radius * 4f;
direction.X = (float)Math.Cos(angle);
direction.Y = -(float)Math.Sin(angle);
Vector2 position = new Vector2(x, y) + direction * length;
Vector2 velocity = direction * 4f;
float angularVelocity = 0.05f * (float)(random.NextDouble() * 2 - 1);
float size = (float)random.NextDouble() * .8f + .2f;
int ttl = 400;
int type = 3;
particles.Add(new Catcher(texture, position, velocity, 0, angularVelocity, type, size, ttl));
}
// "Музыкальный" импульс, создание частички
public void Beat(float Wave)
{
particles.Add(GenerateNewParticle(Wave));
}
public void Update() // Обновление всех частиц
{
for (int particle = 0; particle < particles.Count; particle++)
{
particles[particle].Update();
if (particles[particle].Size <= 0 || particles[particle].TTL <= 0)
{
// Если частичка дохлая или размер нуль или меньше, удаляем её
particles.RemoveAt(particle);
particle--;
}
}
// Обновляем аккумулятор, если значения ячейки меньше 1f, то добавляем значение, указанное в статическом классе Constants — ACCUMULATE_SPEED, листинг Constanst - ниже.
for (int a = 0; a < 128; a++)
if (accomulator[a] < 1.0f)
accomulator[a] += Constanst.ACCUMULATE_SPEED;
}
public void Draw(SpriteBatch spriteBatch)
{
// Прорисовываем все частички, важно указать BlendState.Additive, чтобы частички были более "мягкие".
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Additive);
for (int index = 0; index < particles.Count; index++)
{
particles[index].Draw(spriteBatch);
}
spriteBatch.End();
}
}
}
Листинг Constant.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MusicCatch
{
public class Constanst
{
public const float BEAT_COST = .4f; // "Стоимость" частички, отнимается у аккумулятора при генерации новой частички
public const float ACCUMULATE_SPEED = .01f; // Скорость аккумуляции
public const float BEAT_REACTION = .5f; // Значение реакции на "бит" в музыке
public const float ACCOMULATOR_REACTION = .5f; // Разрешает создавать новую частичку только тогда, когда значения больше реакции аккумулятора
}
}
Объясню, что за загадочный аккумулятор и зачем он нужен. Поговорим о «музыкальном» спектре.
Музыкальный сигнал – пища для аудиосистемы. Точнее – не так. Динамики музыку не слушают, ее восстанавливает наш мозг, получая сложный сигнал, содержащий множество частотных составляющих.
Дак вот, идея такая, слушать «частоты» каждый Update и записывать их в какой-нибудь, например, VisualizationData. Проще говоря, в массив из 128 элементов, которые изменяются от 0f до 1f.
Как этим можно воспользоваться?
Каждый Update: значения в массиве меняются в соответствии с музыкой, нам нужно проверить все 128 элементов, если значение элемента больше чем 0.6f, вызываем Beat-функцию и передаем ей Wave (индекс элемента массива, в котором произошло событие). Все бы хорошо, можно в Beat создавать частичку-ноту. Но представим, что у нас выполняется три Update'а подряд, в котором в одном и том же индексе — значение > 0.6f, как итог будет 100500 частичек за секунду. Чтобы таких вещей не происходило, можно использовать аккумулятор. Смысл его прост: при Beat'е у ячейки массива-аккумулятора соотвествующего индексу Wave отнимается константа BEAT_COST. Каждый Update ко всем элементам аккумулятора прибавляется ACCUMULATE_SPEED. Перед тем, как вызвать Beat проверяется выполняется ли условие — значение аккумулятора > ACCOMULATOR_REACTION, если да, то вызываем Beat. Это решает проблему.
Кстати, BEAT_REACTION — значение, после которых нужно проверять, стоит ли вызывать Beat.
Дальше приведу полный листинг GameLogic (Game1). Много кода, но постараюсь расписать в комментариях.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace MusicCatch
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
private List<Texture2D> MelList;
private Texture2D mouse;
private CatcherHolder m_cHolder;
MediaLibrary mediaLibrary; // Грубо говоря "проигрыватель"
Song song; // Сама музыка
VisualizationData visualizationData;
SpriteFont font;
private int scores = 0; // очки
private float self_size = 1f; // размер "игрока"
private int xsize = 1; // множитель очков
private float power = 0f; // переменная для пурпурного бонуса
private float activity = 0f; // переменная для активности игрока
public Game1()
{
graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth = 800;
graphics.PreferredBackBufferHeight = 600;
graphics.IsFullScreen = false;
graphics.ApplyChanges();
Content.RootDirectory = "Content";
// Создаем переменные
mediaLibrary = new MediaLibrary();
visualizationData = new VisualizationData();
scores = 0;
}
protected override void Initialize()
{
m_cHolder = new CatcherHolder(MelList);
MediaPlayer.Play(song); // начинаем играть музыку
MediaPlayer.IsVisualizationEnabled = true; // включаем визуализатор
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
MelList = new List<Texture2D>();
for(int a = 1; a <= 5; a++)
MelList.Add(Content.Load<Texture2D>("mel" + a));
mouse = Content.Load<Texture2D>("mouse");
song = Content.Load<Song>("Leaves_in_the_Wind");
font = Content.Load<SpriteFont>("SpriteFont1");
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
m_cHolder.Update();
MediaPlayer.GetVisualizationData(visualizationData); // получаем данные
// "Прогоняем" массив с частотами, выполняем условия
for (int a = 0; a < 128; a++)
{
if (visualizationData.Frequencies[a] > Constanst.BEAT_REACTION && m_cHolder.accomulator[a] > Constanst.ACCOMULATOR_REACTION)
{
m_cHolder.Beat(a * 3.125f * 2); // вызываем "бит", которые создает частичку.
m_cHolder.accomulator[a] -= Constanst.BEAT_COST; // убавляем аккумулятор
}
}
// проверяем, есть ли бонус, который тянет к игроку все ноты
if (power > 0f)
{
for (int particle = 0; particle < m_cHolder.particles.Count; particle++)
{
if (m_cHolder.particles[particle].type != 1) // если не враг, то тянем
{
float body1X = m_cHolder.particles[particle].Position.X;
float body1Y = m_cHolder.particles[particle].Position.Y;
float body2X = (float)Mouse.GetState().X;
float body2Y = (float)Mouse.GetState().Y;
float Angle = (float)Math.Atan2(body2X - body1X, body2Y - body1Y) - ((float)Math.PI / 2.0f); // находим угол к игроку
float Lenght = (float)(5000f * power) / (float)Math.Pow((float)Distance(body1X, body1Y, body2X, body2Y), 2.0f); // находим силу
m_cHolder.particles[particle].ApplyImpulse(AngleToV2(Angle, Lenght)); // даем пинка ноте
}
}
power -= 0.001f; // убавляем бонус
}
activity -= 0.001f; // убавляем активность игрока
if (activity < 0.0f)
activity = 0.0f;
else if (activity > 0.5f) activity = 0.5f;
// Держим активность игрока от 0f до .5f
// Проверяем столкновения двух кругов: игрока и нот
for (int particle = 0; particle < m_cHolder.particles.Count; particle++)
{
int x = (int)m_cHolder.particles[particle].Position.X;
int y = (int)m_cHolder.particles[particle].Position.Y;
int radius = (int)(16f * m_cHolder.particles[particle].Size);
if (circlesColliding(Mouse.GetState().X, Mouse.GetState().Y, (int)(16f * self_size), x, y, radius))
{
scores += (int)(10f * m_cHolder.particles[particle].Size * xsize); // добавляем очки, которые зависят от размера ноты и множителя
activity += 0.005f; // добавляем активность
int type = m_cHolder.particles[particle].type;
// выполняем всякие условия, которые возникают при коллизии
switch (type)
{
case 3: // желтый
self_size += 0.1f;
xsize += 1;
// увеличиваем множитель и размер игрока
if (self_size > 4.0f)
self_size = 4.0f;
break;
case 2: // пурпурный
power = 1f; // даем бонус игроку, который все ноты притягивает к себе
break;
case 4: // мигающий
for (int b = 0; b < m_cHolder.particles.Count; b++)
m_cHolder.particles[b].SetType(3); // устанавливает всем нотам тип — желтый
break;
case 1: // красный (враг)
for(int a = 1; a < xsize; a++)
m_cHolder.GenerateYellowExplossion(Mouse.GetState().X, Mouse.GetState().Y, (int)(16f * self_size));
xsize = 1;
self_size = 1f;
scores -= (int)(scores / 4);
break;
}
// удаляем частичку
m_cHolder.particles[particle].TTL = 0;
m_cHolder.particles.RemoveAt(particle);
particle--;
}
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
m_cHolder.Draw(spriteBatch); // рисуем CatcherHolder
spriteBatch.Begin();
Rectangle sourceRectangle = new Rectangle(0, 0, mouse.Width, mouse.Height); // размеры текстуры
Vector2 origin = new Vector2(mouse.Width / 2, mouse.Height / 2); // offset текстуры
Vector2 mouse_vector = new Vector2(Mouse.GetState().X, Mouse.GetState().Y); // вектор(позиция) мышки
string xtext = "x" + xsize.ToString(); // текст
Vector2 text_vector = font.MeasureString(xtext) / 2.0f; // вычисления offset'a текста
spriteBatch.Draw(mouse, mouse_vector, sourceRectangle, new Color(0.5f - power/2.0f + activity, 0.5f, 0.5f - power/2.0f), 0.0f, origin, self_size, SpriteEffects.None, 0f); // рисуем игрока
spriteBatch.DrawString(font, xtext, mouse_vector - text_vector, Color.White); // рисуем множитель
spriteBatch.DrawString(font, "Score: " + scores.ToString(), new Vector2(5, graphics.PreferredBackBufferHeight - 34), Color.White); // рисуем очки
spriteBatch.End();
base.Draw(gameTime);
}
// возвращает, столкнулись ли два круга или нет
bool circlesColliding(int x1, int y1, int radius1, int x2, int y2, int radius2)
{
int dx = x2 - x1;
int dy = y2 - y1;
int radii = radius1 + radius2;
if ((dx * dx) + (dy * dy) < radii * radii)
{
return true;
}
else
{
return false;
}
}
// функция перевода угла в вектор
public Vector2 AngleToV2(float angle, float length)
{
Vector2 direction = Vector2.Zero;
direction.X = (float)Math.Cos(angle) * length;
direction.Y = -(float)Math.Sin(angle) * length;
return direction;
}
// дистанция
public float Distance(float x1, float y1, float x2, float y2)
{
return (float)Math.Sqrt((float)Math.Pow(x2 - x1, 2) + (float)Math.Pow(y2 - y1, 2));
}
}
}
Вот такая простенькая игрушка получается. На конечной машине пользователя должен быть установлен XNA 4.0 и .NET;
Ссылки: сама игра (директ) | исходники (директ) | XNA Framework End-user
Скриншот:
P.S. Идея не моя, такая игра уже была выпущена под flash. Эта игра писалась исключительно для статьи, соответственно дальнейшего развития она не получит.
P.S.S. Так же помогу разобраться в XNA / уроке, для этого пишите мне в личку на хабре или на контакты, которые есть в профиле.