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.