Опубликован: 28.04.2009 | Доступ: свободный | Студентов: 1838 / 107 | Оценка: 4.36 / 4.40 | Длительность: 16:40:00
Специальности: Программист
Лекция 4:

Хранитель экрана

Теперь, давайте, интегрируем функциональность класса 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 за исключением пары нюансов:

  1. Удаление элементов из середины списка является очень дорогой операцией.
  2. Метод 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.
Андрей Леонов
Андрей Леонов

Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету.