Суббота, 18.05.2024, 15:46
Приветствую Вас Гость | RSS
Форма входа
Поиск
Календарь
«  Май 2024  »
ПнВтСрЧтПтСбВс
  12345
6789101112
13141516171819
20212223242526
2728293031
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0

Глава 2: Рисование фигур и растровых изображений при помощи GDI+

И так — мы находимся в самом начале работы над игрой "Dungeon Crawler", которая находится в центре внимания большей части этой книги! Первая глава должна была ввести вас в курс дела касательно целей игры, а теперь — эта глава расскажет о том, как мы можем объединить графику с формами и элементами управления. Мы начнём изучение графических возможностей .NET Framework, которые сделают возможным создание комплексной игры. Вы узнаете, как отделить элементы управления от "Дизайнера форм" и просто создавать их во время исполнения программы. Несмотря на то, что в будущих главах — по-прежнему будет необходимо использование форм и элементов управления, код графики не будет зависеть от управляющих элементов, например, таких как PictureBox. .NET Framework абстрагирует классы вокруг Windows Graphics Interface (GDI), так что мы можем создать поверхности для рисования и отображать на них фигуры, используя такие классы как Graphics и Bitmap в сочетании с управляющим элементом PictureBox. Мы будем просто создавать то, что необходимо во время исполнения.

Вот что кроется в этой главе:

  • Рисование линий
  • Рисование прямоугольников
  • Отображение текста
  • Загрузка растровых изображений
  • Рисование растровых изображений
  • Вращение и переворот растровых изображений
  • Доступ к пикселям растровых изображений
  • Создание структур для многоразового использования

Рисование линий

Линии и другие векторные фигуры, сами по себе, могут быть не очень интересными, но мы собираемся использовать рисование линий в качестве отправной точки для изучения программирования графики с .NET Framework и GDI+. Код графики, который мы будем рассматривать, дает результат, показанный на рисунке 2.1.

Рисунок 2‑1 Рисование линий при помощи управляемых GDI+ объектов.

PictureBox — наш друг

Для наших целей в этой главе, мы рассмотрим функции характерные для программирования 2D-графики с использованием свойств класса Image, элемента управления PictureBox. PictureBox может быть добавлен в форму вручную (при помощи конструктора форм), но проще использовать глобальный элемент управления PictureBox и просто создать его во время выполнения в функции Form1_Load. На самом деле, мы просто изменим код формы, на столько хорошо, что ручное редактирование свойств и не потребуется. Любое свойство которое, вы видите в окне "Свойства" конструктора форм, может быть изменено в коде программы, и это — проще сделать напрямую в коде. И так, в раздел глобальных переменных, публичного частичного класса Form1, давайте добавим несколько переменных включая и элемент управления PictureBox:

PictureBox pb;
Timer timer;
Random rand;

В Form1_Load, мы создаём новый PictureBox и добавляем его к форме. Родительское свойство используется для прикрепления элемента управления к форме Form1 (далее с этим ключевым словом, которое относится к текущей форме). DockStyle.Fill заставляет PictureBox заполнить всю форму, так что мы можем изменить размер формы и PictureBox будет изменяться вместе с ним.

pb = new PictureBox();
pb.Parent = this;
pb.Dock = DockStyle.Fill;
pb.BackColor = Color.Black;

Пока мы работаем с Form1_Load, давайте просто продвинемся вперед и установим настройки формы. Опять же, сделаем это напрямую в коде, в то время как это же можно сделать с помощью окна свойств в конструкторе форм.

// Создаем форму
this.Text = "Line Drawing Demo";
this.FormBorderStyle =
 System.Windows.Forms.FormBorderStyle.FixedSingle;
this.MaximizeBox = false;
this.Size = new Size(600, 500);
// Создаём генератор случайных чисел
rand = new Random();

Поверхности и устройства

Вернемся в раздел глобальных переменных в верхней части кода, нам необходимы два новых объекта: Bitmap и объект Graphics.

Bitmap surface;
Graphics device;

Bitmap представляет собой рисуемую поверхность и на самом деле является просто указателем на данные в памяти. После отображения чего-либо с помощью объекта Graphics (в PictureBox.Image), мы устанавливаем переменную Bitmap (которая является указателем), равной PictureBox.Image, и этот Bitmap может теперь рассматриваться как независимая поверхность, которая может быть скопирована куда угодно, сохранена в файл, или ещё что-нибудь. Bitmap должен быть создан с теми же размерами, как и контролл PictureBox. Этот код располагается в Form1_Load:

// Создаем графическое устройство
surface = new Bitmap(this.Size.Width, this.Size.Height);
pb.Image = surface;
device = Graphics.FromImage(surface);

Есть немало версий Graphics.DrawLine() с различными вариациями параметров, которые используют Points, X, Y координаты — на основе float и int, и разными режимами рисования. Версия, которую Я использую, будет использовать класс Pen с определенным цветом и толщиной линии. Функция DrawLine() создает ручку со случайным цветом и случайным размером линии, и двумя случайными точками на концах линии, которые вписываются в размеры формы. После вызова DrawLine(), происходит обновление PictureBox.Image.

public void drawLine()
{
// Выбрать случайный цвет
int A = rand.Next(0, 255);
int R = rand.Next(0, 255);
int G = rand.Next(0, 255);
int B = rand.Next(0, 255);
Color color = Color.FromArgb(A, R, G, B);
// Создать Pen определенного цвета
int width = rand.Next(2, 8);
Pen pen = new Pen(color, width);
// Задаем случайные значения концов линии
int x1 = rand.Next(1, this.Size.Width);
int y1 = rand.Next(1, this.Size.Height);
int x2 = rand.Next(1, this.Size.Width);
int y2 = rand.Next(1, this.Size.Height);
// рисуем линию
device.DrawLine(pen, x1, y1, x2, y2);
// Обновляем поверхность рисования
pb.Image = surface;
}

4D программирование с классом Timer

Мы так же можем создать Timer напрямую в коде без использования конструктора форм. Необходим только один дополнительный шаг, после чего новый Timer будет работать как обычно, при установке его свойств, мы должны создать обработчик событий.

Timer timer;
Объект Таймер создаётся в Form1_Load:
// Установим таймер
timer = new Timer();
timer.Interval = 20;
timer.Enabled = true;
timer.Tick += new System.EventHandler(TimerTick);

Когда новый обработчик события будет создан, то он становится "видимым" в системе обработчиков событий в Visual C #, и может быть использован в качестве триггера событий, даже когда мы пишем функцию сами (вместо тех что Visual C# создает для нас). В этом примере я хочу запускать функцию DrawLine() каждые 20 миллисекунд, что составляет 50 кадров в секунду (50 Гц).

public void TimerTick(object source, EventArgs e)
{
drawLine();
}

И последнее: мы должны освободить память после того, как мы закончим работать с объектами, созданными в наших программах. Visual C# (а, точнее, среда) будет освобождать объекты автоматически, в большинстве случаев, но хороший стиль программирования заключается в том — чтобы освобождать память, которую вы использовали. Лучше всего это делать в событии Form1_FormClosed. В определенный момент, становится слишком сложно управлять всем в коде; некоторые события, подобные этому, лучше оставить для обработчика событий в Свойствах формы. Для вызова событий, откройте Form1.cs в режиме конструктора, а затем в окне "Свойства" нажмите кнопку "События" (который выглядит как молния). Вы увидите все события, как показано на рисунке 2.2. Я бы не сказал, что освобождение памяти имеет решающее значение, но это хорошая идея.

Рисунок 2‑2 Список событий формы.

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
device.Dispose();
surface.Dispose();
timer.Dispose();
}

Рисование прямоугольников

Как только мы получим основу для рисования линий, появится много других векторных фигур, которые можно сделать с помощью всего нескольких незначительных изменений в коде. Одна из таких фигур представляет собой прямоугольник, который мы и рассмотрим далее. Результат показан на Рисунке 2.3.

Рисунок 2‑3 Рисование прямоугольников при помощи управляемых GDI+.

Для справки, мы пройдёмся по всему листингу кода (который все еще довольно короткий). Сначала идут глобальные переменные, Form1_Load, который инициализирует программу, и Form1_FormClosed, который освобождает память.

using System;
using System.Drawing;
using System.Windows;
using System.Windows.Forms;
public partial class Form1 : Form
{
PictureBox pb;
Timer timer;
Bitmap surface;
Graphics device;
Random rand;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
// Создаем форму
this.Text = "Rectangle Drawing Demo";
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.
FixedSingle;
this.MaximizeBox = false;
this.Size = new Size(600, 500);
// Создаем новый PictureBox
pb = new PictureBox();
pb.Parent = this;
pb.Dock = DockStyle.Fill;
pb.BackColor = Color.Black;
// Создаем графическое устройство
surface = new Bitmap(this.Size.Width, this.Size.Height);
pb.Image = surface;
device = Graphics.FromImage(surface);
// Создаем генератор случайных чисел
rand = new Random();
// Определяем таймер
timer = new Timer();
timer.Interval = 20;
timer.Enabled = true;
timer.Tick += new EventHandler(timer_Tick);
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
device.Dispose();
surface.Dispose();
timer.Dispose();
}

В конце, у нас есть событие timer_Tick и DrawRect() функция, которая делает фактическую растеризацию прямоугольника. Опять же, есть несколько версий функции Graphics.DrawRectangle(), и я только что выбрал самый простой, но есть и другие, которые позволяют использовать объект Point в качестве координат, а не отдельные X и Y значения.

private void drawRect()
{
// Создать цвет случайным образом
int A = rand.Next(0, 255);
int R = rand.Next(0, 255);
int G = rand.Next(0, 255);
int B = rand.Next(0, 255);
Color color = Color.FromArgb(A, R, G, B);
/ / создать Pen и назначить цвет
int width = rand.Next(2, 8);
Pen pen = new Pen(color, width);
/ / концы линий заданные случайным образом
int x = rand.Next(1, this.Size.Width — 50);
int y = rand.Next(1, this.Size.Height — 50);
Rectangle rect = new Rectangle(x, y, 50, 50);
/ / Рисуем прямоугольник
устройствDrawRectangle (ручка, прямоугольник);
/ / Обновляем поверхность для рисования
pb.Image = surface;
}
private void timer_Tick(object source, EventArgs e)
{
drawRect();
}
}

Отображение текста

Нам необходимо будет нарисовать текст на экране игры с помощью любого нужного шрифта, и класс Graphics дает нам такую возможность, через функцию DrawString(). Есть несколько версий функции с различными наборами параметров, но мы будем использовать простейший вариант, которому требуется обычный String (для слов, которые мы хотим отобразить), самодельный объект Font, цвет и координаты. Рисунок 2.4 показывает результат этого примера.

using System;
using System.Drawing;
using System.Windows.Forms;
public partial class Form1 : Form
{
string[] text = {
"АВАТАР!",
"Знай, что Британия вступила в новую эпоху",
"просветления! Знай, что наконец пришло время",
"истинного повелителя Бриттании, чтобы занять свое место»,
"и возглавить свой народ. Под моим руководством, Бриттания",
"будет процветать. И все народы будут",
"радоваться и отдавать дань уважения новому... защитнику!"
"Знай, что ты тоже должен встать на колени передо мной, Аватар".,
"Ты тоже скоро признаешь мою власть. И Я -",
"стану твоим спутником... твоим проводником... и твоим",
"повелителем!", "",
"Ultima VII: The Black Gate",
"Copyright 1992 года Electronic Arts"
};
PictureBox pb;
Bitmap surface;
Graphics device;
Random rand;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
/ / инициализация
this.Text = "Text Drawing Demo";
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.
FixedSingle;
this.MaximizeBox = false;
this.Size = new Size(600, 500);
rand = new Random();
/ / создаем новый PictureBox
pb = new PictureBox();
pb.Parent = this;
pb.Dock = DockStyle.Fill;
pb.BackColor = Color.Black;
/ / создаем графическое устройство
surface = new Bitmap(this.Size.Width, this.Size.Height);
pb.Image = surface;
device = Graphics.FromImage(surface);
/ / создаем новый шрифт
Font font = new Font("Times New Roman", 26, FontStyle.Regular,
GraphicsUnit.Pixel);
// рисуем текст
for (int n = 0; n < text.Length; n++)
{
device.DrawString(text[n], font, Brushes.Red, 10, 10 + n*28);
}
/ / Обновить поверхность рисования
pb.Image = surface;
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
device.Dispose();
surface.Dispose();
}
}

Рисунок 2‑4 Отображение текста выбранным шрифтом и заданным цветом.

Есть и другие формы помимо линий, прямоугольников и текста, которые класс Graphics может нарисовать. Теперь у вас есть основа, посмотрите на то как вы можете изменить программу, чтобы использовать любую из следующих функций:

  • DrawArc
  • DrawBezier
  • DrawCurve
  • DrawEllipse
  • DrawPie
  • DrawPolygon

Хитрости

Для упрощения кода в этом проекте C#, я убрал отображающееся по умолчанию пространство имен которые Visual C# автоматически добавляется в новый проект. В более крупном проекте, со множеством файлов исходных кодов и библиотек, мы должны будем использовать пространство имен, но в простых примерах, подобных этому, пространство имен можно пропустить.

Разбираемся с растровыми изображениями

Изучение того, как рисовать растровые изображения — является первым шагом на пути создания 2D игры, такой как наша будущая игра Dungeon Crawler. После того как мы овладеем умением нарисовать одно растровое изображение, мы сможем расширить его до анимации, рисуя один кадр за другим во временной последовательности и — вуаля, спрайтовая анимация становится реальностью! Мы сосредоточимся на спрайтовой анимации в главе 3, поэтому работа над основами рисования растровой графики в настоящий момент является в просто предпосылкой.

Опираясь на код мы узнали ранее в этой главе что, объект Bitmap, PictureBox, и объект Graphics работают в тандеме, представляя собой графическое устройство, способное рисовать векторные формы так же, как и растровые изображения. Ещё раз, в качестве ссылок, мы должны объявить две переменные:

Bitmap surface;
Graphics device;

А затем, предполагая, что у нас есть элемент управления PictureBox, который называется pictureBox1, создаем объекты. Контрол PictureBox может быть создан во время выполнения или мы можем просто добавить его в форму вручную.

surface = new Bitmap(this.Size.Width, this.Size.Height);
pictureBox1.Image = surface;
device = Graphics.FromImage(surface);

И так, мы уже знаем этот код, и (для того, чтобы заложить основу) он должен располагаться в самом начале, в качестве графического устройства для рисования растрового изображения.

Загрузка растровых файлов

Мы можем загрузить картинку в C# с помощью класса Bitmap. Но в ней нет функции Bitmap.Load() (к сожалению!) поэтому мы должны использовать конструктор, для передачи имени растрового файла при создании объекта.

Bitmap bmp;
bmp = new Bitmap("image.bmp");

Определение

Конструктор — это функция класса (так называемый — метод), срабатывающая когда объект создается впервые. В нём инициализируются переменные класса (также называемые свойствами). Деструктор — функция класса, которая выполняется, когда объект разрушается: с помощью object.Dispose() или object = NULL.

Несмотря на то что оба метода работают, и мы можем даже передать строку вместо жесткого кодирования файла, существует очень серьезная проблема связанная с обработкой ошибок: если файл не существует, исключение приведет к краху программы. Недостача файлов встречается достаточно часто (как правило, в связи с их присутствием не в той папке), и мы хотим, чтобы отображалось дружественное сообщение об ошибке, а не ожидалось аварийное завершение программы. Решение состоит в том, чтобы обернуть код загрузки растра в блок try. . .catch. Например так:

try
{
bmp = new Bitmap(filename);
}
catch (Exception ex) { }

Этот код не рухнет, если файл отсутствует или если возникнет какая-то другая ошибка при чтении файла. Итак, поместите это в повторно используемую функцию, которая возвращает Bitmap если файл существует или "ничего" (null), если она не сработала. Одно замечание: не забудьте освободить память использующуюся для графики, когда программа закончится.

public Bitmap LoadBitmap(string filename)
 {
 Bitmap bmp = null;
 try
 {
 bmp = new Bitmap(filename);
 }
 catch (Exception) { }
 return bmp;
 }

Если файл не существует, то LoadBitmap() будет возвращать "ничто", как указатель на объект, а не аварийное завершение с кодом исключения. Эта маленькая функция очень удобна! И она демонстрирует силу повторного использования кода и модификации — все необходимые нам функции, которые отсутствуют в SDK или библиотеках, мы можем просто написать сами. Можно даже пойти ещё дальше и написать свои собственные новые обертки класса Bitmap (называющиеся чем-то вроде CBitmap?) с функцией Load(). Вы можете легко сделать это самостоятельно, с тем небольшим количеством кода, который мы использовали до сих пор. Тем не менее, я хочу пропустить этот шаг и добавить растровые нагрузка в классе Sprite, когда мы доберемся до него в главе 3.

Подсказка

Чтобы убедиться, что созданные объекты должным образом утилизированы, после того как программа завершится, я рекомендую расположить Form1_FormClosed() функцию в верхней части исходного кода, прямо под объявлением переменных, где она будет быстро и легко писать код, необходимый для бесплатного объект. Всегда пишите код создания/удаления в паре, чтобы избежать утечки памяти!

Рисование растрового изображения

Есть несколько версий функции Graphics.DrawImage() на "языке ООП" альтернативные версии называются — перегруженными функциями. Простейший вариант вызова функции содержит только Bitmap или Image параметр, а затем X и Y координаты. Например, эта строка

device.DrawImage(bmp, 0, 0);

будет рисовать растровые *.bmp в пиксельные координаты 0,0. Рисунок 2.5 показывает этот пример.

Рисунок 2‑5 Отображение рисунка загруженного из растрового файла.

Мы можем дополнительно использовать структуру Point, в которой X и Y координаты объединены в один объект, либо использовать переменные Single с плавающей запятой. Существуют также функции масштабирования, которые позволяют изменить размер изображения. По мимо дополнительных параметров ширины и высоты, мы можем определить новые размеры для изображения. Рисунок 2.6 показывает другой пример с добавлением этой линии, которая рисует еще одну копию растрового изображения уменьшенного размера.

device.DrawImage(planet, 400, 10, 64, 64);

Рисунок 2‑6 Отображение масштабированного растрового рисунка.

Поворот и Отражение растрового изображения

Bitmap класс имеет некоторые вспомогательные функции для манипулирования изображением и даже его отдельными пикселями. Bitmap.RotateFlip() функция поворота растрового изображения в 90-градусный шагом (90, 180 и 270 градусов), а также позволяет отразить изображение вертикально, горизонтально, или всё сразу. Вот пример, который вращает растр на 90 градусов:

planet.RotateFlip(RotateFlipType.Rotate90FlipNone);

RotateFlipType имеет следующие значения:

  • Rotate180FlipNone
  • Rotate180FlipX
  • Rotate180FlipXY
  • Rotate180FlipY
  • Rotate270FlipNone
  • Rotate270FlipX
  • Rotate270FlipXY
  • Rotate270FlipY
  • Rotate90FlipNone
  • Rotate90FlipX
  • Rotate90FlipXY
  • Rotate90FlipY
  • RotateNoneFlipX
  • RotateNoneFlipXY
  • RotateNoneFlipY

Bitmap Drawing демо имеет несколько кнопок на форме, которые позволяют вам изучить вращение и отражение картинки различными способами, как вы можете видеть на рисунке 2.7. В дополнение к вызову RotateFlip(), мы тем не менее должны нарисовать изображение еще раз и обновить PictureBox, как обычно:

image.RotateFlip(RotateFlipType.Rotate180FlipNone);
device.DrawImage(planet, 0, 0);
pictureBox1.Image = surface;

Рисунок 2‑7 Поворот и отражение рисунка.

Доступ к пикселям растрового изображения

Мы также можем исследовать и модифицировать буфер пикселей точечного рисунка напрямую, с помощью функций класса Bitmap. Bitmap.GetPixel() извлекает пиксель растрового изображения по заданным X, Y координатам, возвращая его в качестве переменной Color. Кроме того, Bitmap.SetPixel() будет изменять цвет пикселя в заданных координатах. Следующий пример читает каждый пиксель из рисунка Планеты и меняет его на зеленый, установив красную и синюю компоненты цвета равную нулю, оставляя только зеленый цвет в остатке. Рисунок 2.8 показывающий пример "Bitmap Drawing" с изменением пикселей — не очень эффектен, но он хорошо иллюстрирует то, что вы можете сделать с этой возможностью.

Рисунок 2‑8 Изменение цвета пикселей на рисунке.

for (int x = 0; x < image.Width — 1; x++)
{
for (int y = 0; y < image.Height — 1; y++)
{
Color pixelColor = image.GetPixel(x, y);
Color newColor = Color.FromArgb(0, pixelColor.G, 0);
image.SetPixel(x, y, newColor);
}
}

Вот исходный код "Bitmap Drawing demo". В нём есть форма с элементами управления, поэтому вы должны просто открыть проект для того, чтобы запустить его, в исходном коде не нужно строить пользовательский интерфейс, как мы это делали в некоторых наших простых примерах.

using System;
using System.Drawing;
using System.Windows.Forms;
public partial class Form1 : Form
{
Bitmap surface;
Graphics device;
Bitmap image;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
// Создаем форму
this.Text = "Bitmap Drawing Demo";
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.
FixedSingle;
this.MaximizeBox = false;
// Создаем графическое устройство
surface = new Bitmap(this.Size.Width, this.Size.Height);
pictureBox1.Image = surface;
device = Graphics.FromImage(surface);
// Загружаем картинку
image = LoadBitmap("skellyarcher.png");
// Рисуем растровое изображение
device.DrawImage(image, 0, 0);
}

public Bitmap LoadBitmap(string filename)
{
Bitmap bmp = null;
try
{
bmp = new Bitmap(filename);
}
catch (Exception ex) { }
return bmp;
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
device.Dispose();
surface.Dispose();
image.Dispose();
}

private void button9_Click(object sender, EventArgs e)
{
image.RotateFlip(RotateFlipType.Rotate90FlipNone);
device.DrawImage(image, 0, 0);
pictureBox1.Image = surface;
}

private void button10_Click(object sender, EventArgs e)
{
image.RotateFlip(RotateFlipType.Rotate180FlipNone);
device.DrawImage(image, 0, 0);
pictureBox1.Image = surface;
}
private void button11_Click(object sender, EventArgs e)
{
image.RotateFlip(RotateFlipType.Rotate270FlipNone);
device.DrawImage(image, 0, 0);
pictureBox1.Image = surface;
}
private void button12_Click(object sender, EventArgs e)
{
image.RotateFlip(RotateFlipType.RotateNoneFlipX);
device.DrawImage(image, 0, 0);
pictureBox1.Image = surface;
}

private void button13_Click(object sender, EventArgs e)
{
image.RotateFlip(RotateFlipType.RotateNoneFlipY);
device.DrawImage(image, 0, 0);
pictureBox1.Image = surface;
}

private void button14_Click(object sender, EventArgs e)
{
image.RotateFlip(RotateFlipType.RotateNoneFlipXY);
device.DrawImage(image, 0, 0);
pictureBox1.Image = surface;
}

private void button15_Click(object sender, EventArgs e)
{
Color white = Color.FromArgb(255, 255, 255);
Color black = Color.FromArgb(0, 0, 0);
for (int x = 0; x < image.Width — 1; x++)
{
for (int y = 0; y < image.Height — 1; y++)
{
if (image.GetPixel(x,y) == white)
image.SetPixel(x, y, black);
}
}
device.DrawImage(image, 0, 0);
pictureBox1.Image = surface;
}

private void button16_Click(object sender, EventArgs e)
{
for (int x = 0; x < image.Width — 1; x++)
{
for (int y = 0; y < image.Height — 1; y++)
{
Color pixelColor = image.GetPixel(x, y);
Color newColor = Color.FromArgb(0, pixelColor.G, 0);
image.SetPixel(x, y, newColor);
}
}
device.DrawImage(image, 0, 0);
pictureBox1.Image = surface;
}
}

Создание многоразового Framework'а

У нас теперь достаточно кода, чтобы с этого момента начать конструировать игровой фреймворк — основу для наших будущих C# проектов. Цель фреймворка состоит в том, чтобы позаботиться о повторяющемся кода. Любые переменные и функции, которые постоянно необходимы, могут быть перемещены в Game класс, как свойства и методы, где они будут удобно располагаться и легко доступны. Во-первых, мы создадим новый файл с исходным кодом называемый Game.cs, который будет содержать исходные код класса Game. Затем, мы будем копировать этот Game.cs файл в папку любого нового проекта, который мы создадим, и добавим его к этому проекту. Цель состоит в том, чтобы упростить весь процесс создания нового игрового проекта и сделать большую часть нашего C# кода пригодным для повторного использования. Давайте начнём:

using System;
using System.Drawing;
using System.Diagnostics;
using System.Windows;
using System.Windows.Forms;
public class Game
{
private Graphics p_device;
private Bitmap p_surface;
private PictureBox p_pb;
private Form p_frm;

Вы можете узнать первые три свойства класса из предыдущих примеров. Они имеют "p_" в начале их имени, а это позволяет с первого взгляда сказать, что они являются приватными членами класса (в отличие, скажем, от параметров функции). Четвертое свойство, p_frm, является ссылкой на главную форму проекта, которая будет задана при создании объекта. Да, наш Game класс может даже изменить его форму, поэтому мы не должны делать ничего больше, чем оставить форму в классе.

Подсказка

Класс представляет собой план, написанный на исходном коде, того как объект должен вести себя во время выполнения. Подобно тому, как объект не существует во время компиляции (то есть, когда мы редактируем исходный код и собираем проект), класс не существует во время выполнения. Объект создается из плана класса.

Конструктор класса Game

Конструктор является первым методом, который запускается, когда создается экземпляр класса в объекте. Мы можем добавить параметры в конструктор, чтобы отправить информацию для объекта во время выполнения, важные вещи вроде формы, или, может быть, имя файла, или все то, что вы захотите.

Определение

"Реализация" это процесс создания объекта из плана, определенного в классе. Когда это происходит, объект создается и конструктор исполняется. Аналогично, когда объект уничтожается, срабатывает деструктор. Эти методы определены в классе.

Вот конструктор для класса Game. Это всего лишь предварительная версия, так как со временем будет добавлено больше кода. Как вы видите, это не новый код, это просто код, который мы видели раньше при создании Graphics и Bitmap объектов, необходимых для отображения в PictureBox. Который, кстати, создается во время выполнения этой функции и устанавливается на заполнение всей формы (Dock = DockStyle.Fill). Чтобы определить, что эти объекты используются, переменная Graphics называется p_device — что технически не верно, но адекватно передает замысел. Чтобы проиллюстрировать когда конструктор срабатывает, во всплывающем окне появляется сообщение, которое вы можете удалить после того, как он выполнится.

public Game(Form1 form, int width, int height)
{
Trace.WriteLine("Game class constructor");
/ / Задаем свойства формы
p_frm = form;
p_frm.FormBorderStyle = FormBorderStyle.FixedSingle;
p_frm.MaximizeBox = false;
p_frm.Size = new Size(width, height);
/ / Создаем PictureBox
p_pb = new PictureBox();
p_pb.Parent = p_frm;
p_pb.Dock = DockStyle.Fill;
p_pb.BackColor = Color.Black;
/ / Создаем графическое устройство
p_surface = new Bitmap(p_frm.Size.Width, p_frm.Size.Height);
p_pb.Image = p_surface;
p_device = Graphics.FromImage(p_surface);
}

Деструктор класса Game

Mетод "деструктор" вызывается автоматически, когда объект вот-вот будет удален из памяти (т. е. уничтожен). В C#, или, более конкретно, в .NET, имя деструктора — Finalize(), но мы создаем деструктор подкласса использующий значок тильды (~) перед именем класса. Таким образом, если имя нашего класса — Game, то деструктор будет — ~Game(). В этом методе, мы снова отправляем сообщение на консоль, используя System.Diagnostics.Trace.WriteLine(). Не стесняйтесь использовать Trace, в любое время вы должны видеть отладочную информацию, так как она будет отображаться в окне вывода. Обратите внимание, что p_frm не уничтожен, оставим это так, потому что это — просто ссылка на текущую Windows форму.

~Game()
{
Trace.WriteLine("Game class destructor");
p_device.Dispose();
p_surface.Dispose();
p_pb.Dispose();
}

Загрузка Bitmap

Наш первый многоразовый метод для Game класса LoadBitmap:

public Bitmap LoadBitmap(string filename)
{
Bitmap bmp = null;
try
{
bmp = new Bitmap(filename);
}
catch (Exception ex) { }
return bmp;
}

Обновление класса Game

Нам, вероятно, не понадобится функции Update() на ранней стадии, но здесь она представлена, как вариант, если вы захотите использовать его для обновления PictureBox, в любое время рисование происходит на "устройстве". В свое время, эта функция будет расширена, чтобы она могла делать намного больше, чем скудный однострочный код, представленный в настоящее время. Также, здесь показан "атрибут" называемый Device. Атрибут позволяет писать код, который выглядит как простое свойство класса и используется (например, p_device), когда действительно происходит вызов функции.

public Graphics Device
{
get { return p_device; }
}
public void Update()
{
/ / обновить поверхность рисования
p_pb.Image = p_surface;
}
}

Так, например, если мы хотим получить значение, возвращаемое свойством Device, мы можем сделать это вот так:

Graphics G = game.Device;

Обратите внимание, что я не включил скобки в конце Device. Это потому, что оно не рассматривается как метод, хотя мы в состоянии сделать что-нибудь с данными перед его возвращением. Ключом к свойству является его get и set члены. Так как я не хочу, чтобы кто-либо изменял переменную p_device за пределами класса, я сделал свойство доступным только для чтения с помощью get без соответствующего set элемента. Если я захочу сделать p_device изменяемым, я буду использовать set.

Свойства очень полезны, поскольку они позволяют защитить данные в классе! Вы можете предотвратить изменение переменной убедившись, что изменённое значение лежит в допустимом диапазоне, прежде чем разрешить изменение — так что, в этом смысле, свойства выглядят как "переменные с привилегиями".

Демонстрация Framework-а

Код в этой программе (Framework demo) воспроизводит в большей степени тот же результат, который мы уже видели ранее в этой главе (рисунок фиолетовой планеты). Разница в том, что благодаря новому классу Game, исходный код гораздо, гораздо короче! Взгляните:

using System;
using System.Drawing;
using System.Windows.Forms;
public partial class Form1 : Form
{
public Game game;
public Bitmap planet;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
// cоздаем форму
this.Text = "Framework Demo";
// создаем объект Game
game = new Game(this, 600, 500);
// загрузка растровых изображений
planet = game.LoadBitmap("planet.bmp");
if (planet == null)
{
MessageBox.Show("Error loading planet.bmp");
Environment.Exit(0);
}
// рисуем растровые изображения
game.Device.DrawImage(planet, 10, 10);
game.Device.DrawImage(planet, 400, 10, 100, 100);
game.Update();
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
/ / удаляем объект Game
game = null;
}

}

Устранение любых многократно повторяющихся участков исходного кода, путем перемещения его в файл поддержки, это — как сокращение математической формулы, когда пересчет новой формулы оказывается более эффективным, чем это было раньше. Любой код, который не должен быть написан — увеличивает вашу производительность в качестве программиста. Таким образом, ищите любую возможность, чтобы чисто и эффективно использовать повторяющийся код, но не сокращайте только ради повторного использования кода — убедитесь, что вы размещаете переменные и функции вместе, которые логически связанны друг с другом, а не мешанину всего и вся.

Поднимаем уровень!

Эта глава дала нам возможность воплотить систему отображения в коде и обойти "Конструктор форм", путем создания контролов во время исполнения программы, а не во время дизайна интерфейса. Используя эту технику, мы создали PictureBox для использования в визуализации. Также мы узнали, как работать с растровыми изображениями и манипулировать ими различными способами, которые будут очень полезны в игре. Мы уже узнали достаточно о программировании 2D-графики, чтобы в следующей главе начать работать со спрайтами!


© Jonathan S. Harbour "Visual C# Game Programming for Teens" 2012