|
Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету. |
Хранитель экрана
Теперь, давайте, интегрируем функциональность класса Firework в главную форму приложения (листинг 4.3).
public partial class FullscreenForm : Form
{
Firework firework = null;
public FullscreenForm()
{
InitializeComponent();
}
private void FullscreenFormLoad(object sender, EventArgs e)
{
SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
MinimumSize = SizeFromClientSize(new Size(1, 1));
try
{
firework = new Firework(Handle, 0.0f, 0.0f);
}
catch (FireworkException ex)
{
MessageBox.Show(ex.Message, "Критическая ошибка", MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
Application.Idle += new EventHandler(ApplicationIdle);
}
void Application_Idle(object sender, EventArgs e)
{
if (firework == null)
{
Close () ; return;
}
firework.Update();
Invalidate() ;
}
private void FullscreenFormFormClosed(object sender, FormClosedEventArgs e)
{
if (firework != null)
{
firework.Dispose(); firework =
null;
}
}
private void FullscreenFormPaint(object sender, PaintEventArgs e)
{
if (firework == null)
return;
firework.Paint();
}
}
Листинг
4.3.
В заключение необходимо развернуть форму на весь экран и убрать заголовок. Для этого присвойте свойству FormBorderStyle значение None, а свойству WindowState значение Maximized. После запуска полученного приложения экран окрасится в черный цвет, а в центре появится маленький вращающийся пестрый диск.
4.2. Фейерверк искр
На следующем этапе мы добавим в наше приложение снопы искр, вылетающие из крутящегося диска. Траектория движения искры, напоминающая спираль, будет складываться как композиция движения искры из центра диска по прямой с постепенным замедлением и движения по окружности вокруг диска с постоянно уменьшающейся угловой скоростью.
Начальный цвет искры будет совпадать с цветом диска в окрестностях появившейся искры. С течением времени искра постепенно затухает посредством уменьшения коэффициента непрозрачности, пока он не достигнет нуля, после чего искра считается потухшей. Всю эту функциональность разумно инкапсулировать в отдельном классе, вложенном в класс Firework (листинг 4.4).
class Scintilla
{
// Время жизни частицы
const float StartTime = 6.0f;
// Диапазон скоростей прямолинейного движения частицы
const float MinSpeed = 0.3f;
const float MaxSpeed = 0.45f;
// Коэффициент замедления прямолинейного движения частицы
const float tSlowing = 0.105f;
// Коэффициент замедления угловой скорости вращательного движения частицы
const float rSlowing = 0.25f;
// Цвет частицы
public Color color;
// Текущие угол поворота частицы вокруг круга и расстояние от центра круга (т.е. координаты
// частицы задаются в сферической системе координат)
public float angle;
public float distance;
// Оставшееся время жизни частицы
public float time;
// Текущая скорость прямолинейного движения
public float tSpeed;
// Текущая скорость вращательного движения
public float rSpeed;
// Возвращает структуру с информацией о частице
public VertexPositionColor Vertex
{
get
{
VertexPositionColor vertex;
// Рассчитываем декартовые координаты частицы
vertex.Position.X = distance * (float)Math.Sin(angle);
vertex.Position.Y = distance * (float)Math.Cos(angle);
vertex.Position.Z = 0;
// Вычисляем новый коэффициент непрозрачности частицы
vertex.Color = new Color(color.R, color.G, color.B,
(byte)(255f * time /
StartTime));
return vertex;
}
}
// Рассчитывает параметры новой частицы. diskAngle – угол поворота диска, используемый для
// синхронизации цвета частицы с цветом диска в окрестностях вершины public void
Init(float diskAngle)
{
// Вычисляем начальное расстояние вершины от центра диска (вершина должна находиться в центре
// диска). Генератор случайных чисел rnd объявлен в классе
Firework
distance = (float)rnd.NextDouble() * Firework.diskRadius;
// Вычисляем начальный угол поворота искры вокруг диска
angle = (float)Firework.rnd.NextDouble() * 2.0f * (float)Math.PI;
// Определяем начальную скорость прямолинейного движения искры
tSpeed = MinSpeed + (float)Firework.rnd.NextDouble() *
(MaxSpeed - MinSpeed);
// Угловая скорость движения вершины всегда в 4 раза меньше
скорости диска rSpeed = Firework.diskSpeed / 4. 0f;
// Рассчитываем начальный цвет искры (он должен совпадать с цветом диска)
byte red = (byte)(255 * Math.Abs(Math.Sin((angle - diskAngle) * 3)));
byte
green = (byte)(255 * Math.Abs(Math.Cos((angle - diskAngle) * 2)));
color =
new Color(red, green, 128, 255);
// Задаем оставшееся время жизни искры time
= StartTime;
}
// Обновляет состояние искры. delta - время, прошедшее с момента
последнего вызова метода
// Update
public void Update(float delta)
{
// Если искра еще не потухла if (time gt;
0.0f)
{
// Уменьшаем оставшееся время жизни искры
time = time - delta;
// Корректируем скорости прямолинейного и вращательного движений
tSpeed = Math.Max(tSpeed - tSlowing * delta, 0.0f); rSpeed =
Math.Max(rSpeed - rSlowing * delta, 0.0f);
// Корректируем положение искры в пространстве distance += tSpeed *
delta;
angle += rSpeed * delta;
}
}
}
Листинг
4.4.
Пользователь сможет управлять видом фейерверка искр посредством двух параметров:
- Размер искр.
- Максимальное количество искр, появляющихся каждые 5 миллисекунд (дискретный шаг, с которым выполняется моделирование логики работы хранителя экрана).
Так как количество искр на экране постоянно меняется, встает вопрос хранения информации об искрах. В принципе, для этой цели вполне подходит обобщенный класс List за исключением пары нюансов:
- Удаление элементов из середины списка является очень дорогой операцией.
- Метод DrawUserPrimitives может визуализировать вершины исключительно из массивов. Однако преобразование списка List в массив посредством метода ToArray() сопряжено с выделением памяти, что в свою очередь может привести к частым вызовам сборщика мусора и, соответственно, провалам производительности.
Первый недостаток мы обойдем достаточно хитро. Потухшие искры не будут удаляться из списка - вместо этого информация о новых искрах будет просто заноситься в элементы списка с потухшими искрами, затирая их, и лишь при отсутствии таковых добавляться в конец списка. Для борьбы со вторым недостатком мы будем самостоятельно копировать содержимое списка в массив, при этом выделение памяти для массива будет производиться лишь при недостаточном размере целевого массива. В листинге 4.5 приведены фрагменты обновленного класса Firework с учетом вышеприведенных требований.
class Firework : IDisposable
{
// Дискретный шаг времени, с которым выполняется моделирование логики работы хранителя экрана
const float timeStep = 0.005f;
// Максимальное время между вызовами Update не должно превышать 1 секунды. Большие временные
// интервалы игнорируются
const float maxDelta = 1.0f;
// Максимальное количество искр, которое может появиться за один дискретный шаг времени (5
// миллисекунд)
int maxScintillaCount;
// Вероятность появления следующей искры
const float scintillaProbability = 0.3f;
// Размер искры
float scintillaSize;
...
// Время, прошедшее с момента запуска приложения
double lastTime = 0;
// Список искр
List<Scintilla> scintillas;
// Массив вершин для визуализации искр
VertexPositionColor[] scintillasVertices = null;
// Количество вершин, хранящихся в массиве
int scintillasVertexNount = 0;
// Генератор случайных чисел, используемый классами Firework и Scintilla
public static Random rnd = new Random();
// Конструктор
public Firework(IntPtr hWnd, float scintillaSize, float scintillaInterval)
{
this.hWnd = hWnd;
// Запоминаем пользовательские настройки
this.scintillaSize = scintillaSize;
this.maxScintillaCount = maxScintillaCount;
...
// Создаем список вершин
scintillas = new List<Scintilla>(16);
// Создаем массив вершин
scintillasVertices = new VertexPositionColor[16];
...
}
// Обновляет сцену
public void Update()
{
double currentTime = (float)stopwatch.ElapsedTicks / (float)Stopwatch.Frequency;
// Максимальный временной интервал не должен превышать maxDelta if (currentTime –
lastTime > maxDelta) lastTime = currentTime - maxDelta;
// Количество дискретных шагов timeStep, которые необходимо выполнить
int stepCount = (int)Math.Floor((currentTime - lastTime) / timeStep);
// Интервал между двумя вызовами метода Update, с учетом дискретности времени float
delta = stepCount * timeStep;
// Поворачиваем диск
diskAngle += diskSpeed * delta;
// Корректируем положение вершин диска
...
// Моделируем движение искр с дискретным шагом времени for (int i =
0; i < stepCount; i++)
{
lastTime += timeStep;
// Счетчик количества не потухших искр scintillasVertexСount = 0;
// Количество новых искр, которые могут появиться на данном шаге
int scintillaCount = rnd.Next(maxScintillaCount + 1);
// Перебираем все искры
for (int j = 0; j < scintillas.Count; j++)
{
// Обновляем состояние текущей искры
scintillas[j].Update(delta);
// Если искра не потухла
if (scintillas[j].time gt; 0)
{
// Увеличиваем счетчик не потухших искр
scintillasVertexСount++;
}
else
// Если искра является потухшей
{
// Пока не исчерпан лимит новых искр
while (scintillaCount gt; 0)
{
// Пробуем добавить новую искру, поэтому уменьшаем счетчик искр
scintillaCount--;
// Генерируем новую искру с вероятностью scintillaProbability
if ((Firework.rnd.NextDouble() lt; scintillaProbability))
{
// Инициализируем текущую искру
scintillas[j].Init(diskAngle);
// Увеличиваем счетчик искр
scintillasVertexСount++; break;
}
}
}
}
// Если необходимо создать еще несколько новых искр while
(scintillaCount gt; 0)
{
scintillaCount--;
if ((Firework.rnd.NextDouble() lt; scintillaProbability))
{
// Добавляем в список информацию о новой искре
scintillas.Add(new Scintilla());
scintillas[scintillas.Count - 1].Init(diskAngle);
scintillasVertexNount++;
}
}
}
// Если число искр превышает размер массива вершин
if (scintillasVertexNount > scintillasVertices.Length)
{
// Удваиваем размер массива. Если размер удвоенного массива недостаточен, используем в
// качестве размера массива текущее количество вершин (на всякий случай перестраховываемся)
scintillasVertices = new VertexPositionColor[Math.Max(scintillasVertexNount,
scintillasVertices.Length * 2)];
}
// Копируем информацию о искрах в массив вершин int
k = 0;
for (int i = 0; i < scintillas.Count; i++)
// Учитывает только не потухшие искры if
(scintillas[i].time > 0)
{
scintillasVertices[k] = scintillas[i].Vertex; k++;
}
}
// Визуализация сцены public
void Paint()
{
device.RenderState.CullMode = CullMode.None;
// Задаем режим смешения пикселей для моделирования полупрозрачности
device.RenderState.AlphaBlendEnable = true;
device.RenderState.BlendFunction = BlendFunction.Add;
device.RenderState.SourceBlend = Blend.SourceAlpha;
device.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
// Задаем размер точек (искр)
device.RenderState.PointSize = scintillaSize;
// Визуализируем массив искр
if (scintillasVertexNount > 0)
{
device.DrawUserPrimitives(PrimitiveType.PointList, scintillasVertices, 0,
scintillasVertexNount);
}
// Визуализируем вращающийся диск
device.DrawUserPrimitives(PrimitiveType.TriangleFan, diskVertices, 0,
4> diskVertices.Length - 2);
}
Обработчик события Load формы также нуждается в косметической правке:
private void FullscreenFormLoad(object sender, EventArgs e)
{
// Задаем размеры искр и максимальное количество искр, генерируемое каждые 5 миллисекунд
firework = new Firework(Handle, 3.0f, 10f);
}
Листинг
4.5.