PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C#

Spacerujący skrzat Klasa "Sprite" Program w C# (skrzat) Latające spodki (program z defektem) Klasa "Sprite2" Program w C# (spodki, wersja  2) Program w C# (spodki, wersja 3) Poprzedni przykład Następny przykład Kontakt

Spacerujący skrzat

Zaprezentowana poniżej technika animacji polega na użyciu bitmapy będącej sekwencją prosto­kątnych klatek (ang. frame – kadr, ramka) reprezen­tujących kolejne obrazy porusza­jącgo się obiektu i proce­dury obsługi zdarzeń zegarowych kopiu­jącej klatkę po klatce do obszaru robo­czego okna z określoną szybkością. Bitmapa może być tworzona bezpo­średnio w programie C# za pomocą narzędzi obiektu Graphics bądź przygo­towana wcześniej w dowolnym edytorze obrazu i łado­wana do tego programu z odrębnego pliku binarnego lub jego zasobów. W programie przedsta­wiającym bajkowego skrzata spaceru­jącego na tle bajkowego krajobrazu używane będą dwie bitmapy:

Wszystkie klatki mają rozmiar 64x96 pikseli. Ich tło jest przezro­czyste, dzięki czemu tło animacji reprezen­towane przez drugą bitmapę nie będzie rujnowane. Obie bitmapy umieszczamy w zasobach nowo utworzo­nego programu o nazwie Skrzat, którego formu­larz zawiera proste menu z polece­niami Start, Stop (wyłączone) i Koniec oraz czaso­mierz do stero­wania animacją (rys.). Aby uniknąć problemów wynika­jących ze zmiany rozmiaru okna w trakcie animacji, właści­wości FormBorder­StyleMaxi­mizeBox formu­larza ustawiamy na Fixed­Single (poje­dyncza stała ramka) i False (blokada maksyma­lizacji). Faktyczny rozmiar okna zostanie ustalony w metodzie obsługi zdarzenia Load formu­larza po załado­waniu tła animacji.

W animacji opartej na komponencie Timer kluczową rolę odgrywa jego jedyne zdarzenie Tick wyzwalane cykli­cznie w odstępie czasu podawanym w milise­kundach we właści­wości Interval. Metoda obsługi tego zdarzenia będzie polegać na usunięciu bieżącej klatki z powierzchni okna i wyświe­tleniu kolejnej w nowym miejscu. Wydaje się, że interwał czasowy 50 ms jest odpowie­dni. Podwójny krok skrzata odtwa­rzany w sekwencji 16 klatek animacji będzie trwał wówczas około 0,8 sekundy (50·16=800 ms), sprawiając wrażenie naturalnego ruchu.

Klasa "Sprite"

Proces tworzenia animacji polegającej na wyświetlaniu małych bitmap (klatek) przedstawia­jących przesuwa­jącego się lub przesuwa­nego na ekranie duszka (ang. sprite) można usprawnić poprzez zdefinio­wanie typu obiekto­wego – klasy w języku C#. Dyspo­nując taką klasą animacji, można łatwo dla każdego duszka występu­jącego w programie utworzyć obiekt tej klasy lub obiekt klasy pochodnej i manipu­lować nim za pomocą jej metod składo­wych. Obraz duszka może się zmieniać w czasie (szereg klatek), jak np. w przy­padku skrzata, który przechodząc od jednego do drugiego brzegu okna przebiera nóżkami i rączkami, bądź może być stały (jedna klatka), jak w przy­padku karty przecią­ganej myszką podczas układania pasjansa. Przy projekto­waniu klasy duszka wypada uwzględnić oba przypadki, a wtedy będzie ona w miarę ogólna i użyteczna w wielu programach.

Definicję klasy umieszczamy w pliku Sprite.cs (pole­cenie Projekt Dodaj klasę...), w którym nazwę przestrzeni zmieniamy na Sprites i zestaw kilku dyrektyw using zastę­pujemy dwoma dyrekty­wami włącza­jącymi przestrzenie DrawingWindows.­Forms, a w kodzie formu­larza programu dopisujemy dyrektywę udostę­pniającą przestrzeń Sprites. Oczywistym jest, że pola klasy powinny umożliwiać określenie pozycji duszka na ekranie (na obszarze roboczym okna), szerokość i wysokość duszka, jego wygląd (aktualną klatkę) oraz tło animacji, zaś metody tworzenie i ew. niszczenie obiektu reprezen­tującego duszka, wyświe­tlanie go i ukry­wanie oraz przesu­wanie. Zadanie to można rozwiązać na wiele sposobów, wypada więc nadmienić, że podana poniżej propo­zycja jest ze względu na przejrzy­stość niezbyt efektywna, ale w prostszych zastoso­waniach wystarcza­jąca. Rozpoczy­namy od zadekla­rowania następu­jących pól klasy:

protected Form form;        // Okno animacji
protected Bitmap mtło;      // Aktualne tło okna (bitmapa robocza)
protected Bitmap mkla;      // Klatki duszka
protected Bitmap msav;      // Fragment tła zakryty przez duszka
protected Rectangle rect;   // Prostokąt duszka
protected Rectangle rkla;   // Prostokąt aktualnej klatki
protected int xmax;         // Współrzędna x ostatniej klatki
private bool wid;           // Duszek widoczny (true), ukryty (false)

Większość pól została zadeklarowana jako chroniona (modyfi­kator protected), by w razie potrzeby był do nich dostęp w klasach pocho­dnych. Przechowy­wana w polu form referencja umożliwi dostęp do narzędzi grafi­cznych klasy Graphics okna, na powierz­chni którego będzie reali­zowana animacja. Metody odpowia­dające za animację obrazu wywoły­wane przez metodę obsługi zdarzenia zegaro­wego będą za każdym razem przetwa­rzać stosowny fragment obrazu na bitmapie roboczej określonej przez refe­rencję zawartą w polu mtło i kopiować go na powierz­chnię okna. Takie manipu­lowanie bitmapą znajdującą się w pamięci opera­cyjnej prowadzi do zminima­lizowania efektu migotania ekranu, gdyż wymaga odświe­żania go tylko podczas kopiowania przetwo­rzonego fragmentu obrazu na powierz­chnię okna. Konstru­ktor klasy Sprite otrzymuje w argu­mentach referencję do okna, referencje do dwóch bitmap reprezen­tujących tło animacji i klatki duszka oraz liczbę wszystkich klatek i współ­rzędne pozycji początkowej (lewego górnego rogu) duszka w obszarze roboczym okna, na podstawie których inicja­lizuje wszystkie pola tworzo­nego, wstępnie niewidzial­nego obiektu:

public Sprite(Form okno, Bitmap tło, Bitmap klatki, int n = 1, int x = 0, int y = 0)
{
    form = okno;
    mtło = tło;
    mkla = klatki;
    if (n <= 0) n = 1;
    rect = new Rectangle(x, y, mkla.Width / n, mkla.Height);
    rkla = new Rectangle(0, 0, rect.Width, rect.Height);
    msav = new Bitmap(rect.Width, rect.Height);
    xmax = (n - 1) * rect.Width;
    wid = false;
}

Gdy liczba klatek nie jest podana jawnie, domyślnie jest nią 1, co oznacza, że obraz duszka nie zmienia się w czasie. Domyślną pozycją początkową duszka jest lewy górny narożnik powierz­chni roboczej okna. Utworzona dodatkowa bitmapa msav jest używana przy odtwa­rzaniu tła okna zasłania­nego przez duszka. Każdora­zowo gdy duszek ma być wyświe­tlony, odpowiedni fragment tła zostanie w niej zapamiętany, a gdy ma być ukryty lub przesu­nięty w inne miejsce, zasło­nięty przez niego fragment tła zostanie z niej odtworzony. Rzecz jasna nie powinno się zapomnieć o zniszczeniu tej bitmapy, by uniknąć wycieku pamięci. Odpowie­dnią metodą realizu­jącą tę operację jest destruktor, który zostanie wywołany automa­tycznie, gdy obiekt duszka przestanie być w programie potrzebny:

~Sprite()
{
    if (msav != null) msav.Dispose();
}

Oprócz konstruktora i destruktora klasa Sprite powinna, jak wspomniano powyżej, udostępniać metody wyświe­tlania, ukrywania i przesu­wania duszka. Pierwsza z tych metod najpierw kopiuje do bitmapy msav (rysuje na niej) prosto­kątny fragment rect bitmapy mtło tożsamy z fragmen­tem powierz­chni okna, na którym ma pojawić się duszek. Następnie kopiuje w to miejsce bitmapy mtło aktualną klatkę rkla bitmapy mkla i zaktua­lizowany w ten sposób fragment rect bitmapy mtło na powierz­chnię roboczą okna, a na koniec przypisuje zmiennej wid wartość true (duszek widoczny):

public void Show()              // Pokaż duszka
{
    using (Graphics g = Graphics.FromImage(msav))
        g.DrawImage(mtło, 0, 0, rect, GraphicsUnit.Pixel);
    using (Graphics g = Graphics.FromImage(mtło))
        g.DrawImage(mkla, rect, rkla, GraphicsUnit.Pixel);
    using (Graphics g = form.CreateGraphics())
        g.DrawImage(mtło, rect, rect, GraphicsUnit.Pixel);
    wid = true;
}

Użycie instrukcji using daje gwarancję, że po wykorzy­staniu obiektu Graphics utworzo­nego dla bitmapy za pomocą metody FromImage lub dla okna za pomocą metody Create­Graphics zasoby z nim związane zostaną zwolnione. Nieco prostsza jest metoda ukrywania duszka, która odtwarza zapamię­tany na bitmapie msav obraz poprzez skopio­wanie jej w miejsce prosto­kąta rect bitmapy mtło, a tego z kolei na powierz­chnię roboczą okna, po czym nadaje zmiennej wid wartość false (duszek niewi­doczny):

public void Hide()              // Ukryj duszka
{
    using (Graphics g = Graphics.FromImage(mtło))
        g.DrawImage(msav, rect, 0, 0, Width, Height, GraphicsUnit.Pixel);
    using (Graphics g = form.CreateGraphics())
        g.DrawImage(mtło, rect, rect, GraphicsUnit.Pixel);
    wid = false;
}

Bardziej złożona jest metoda przesuwania duszka z jednego miejsca powierz­chni roboczej okna do drugiego. Łącznie metoda wykonuje cztery operacje kopio­wania (ryso­wania) prosto­kątnych fragmentów bitmap, przy czym trzy z nich są realizo­wane w pamięci komputera, a tylko ostatnia na powierz­chni okna:

public void Move(int x, int y)  // Przesuń duszka
{
    if (wid)
    {
        using (Graphics g = Graphics.FromImage(mtło))
            g.DrawImage(msav, rect, 0, 0, Width, Height, GraphicsUnit.Pixel);
        Rectangle rnew = new Rectangle(x, y, Width, Height);
        using (Graphics g = Graphics.FromImage(msav))
            g.DrawImage(mtło, 0, 0, rnew, GraphicsUnit.Pixel);
        if (xmax > 0)
            rkla.X = (rkla.X < xmax) ? rkla.X + rkla.Width : 0;
        using (Graphics g = Graphics.FromImage(mtło))
            g.DrawImage(mkla, rnew, rkla, GraphicsUnit.Pixel);
        Rectangle ru = Rectangle.Union(rect, rnew);
        using (Graphics g = form.CreateGraphics())
            g.DrawImage(mtło, ru, ru, GraphicsUnit.Pixel);
    }
    rect.X = x;
    rect.Y = y;
}

Zmiany obrazu, jakie mają nastąpić na powierzchni okna, są przygoto­wywane na bitmapie mtło i dotyczą najmniej­szego prosto­kąta ru obejmu­jącego prosto­kąty rectrnew określa­jące pozycję duszka przed przesu­nięciem i po przesu­nięciu go do nowego miejsca. Metoda reali­zuje następu­jące operacje:

Naturalnie operacje graficzne są wymagane tylko wtedy, gdy duszek jest widoczny. Gdy nie jest widoczny, metoda jedynie aktua­lizuje jego pozycję, przypi­sując polom reprezen­tującym lewy górny narożnik prosto­kąta rect współrzędne określone w argu­mentach x i y.

Poniższy listing przedstawia pełny kod klasy Sprite uzupeł­niony o właści­wości udostę­pniające szerokość i wysokość duszka, jego aktualną pozycję i widoczność, a także o metody Collision (kolizja) i Contains (zawiera). Pierwsza zwraca wartość true, gdy prostokąt duszka jest w kolizji z danym prosto­kątem, lub false, gdy takiej kolizji nie ma, natomiast druga zwraca true, gdy prostokąt duszka zawiera punkt o podanych współ­rzędnych, lub false, gdy punkt ten leży poza prosto­kątem duszka. Ponadto omówione powyżej metody Show (pokaż), Hide (ukryj) i Move (przesuń) zostały zdefinio­wane jako wirtualne (ang. virtual), dzięki czemu będzie je można redefi­niować (przykrywać, przesła­niać) w klasach pochodnych.

using System.Drawing;
using System.Windows.Forms;

namespace Sprites
{
    public class Sprite             // Duszek
    {
        protected Form form;        // Okno animacji
        protected Bitmap mtło;      // Aktualne tło okna (bitmapa robocza)
        protected Bitmap mkla;      // Klatki duszka
        protected Bitmap msav;      // Fragment tła zakryty przez duszka
        protected Rectangle rect;   // Prostokąt duszka
        protected Rectangle rkla;   // Prostokąt aktualnej klatki
        protected int xmax;         // Współrzędna x ostatniej klatki
        private bool wid;           // Duszek widoczny (true), ukryty (false)

        public int Width            // Szerokość klatki
        {
            get { return rect.Width; }
        }

        public int Height           // Wysokość klatki
        {
            get { return rect.Height; }
        }

        public int X                // Aktualna współrzędna x duszka
        {
            get { return rect.Left; }
        }

        public int Y                // Aktualna współrzędna y duszka
        {
            get { return rect.Top; }
        }

        public bool Visible         // Duszek widoczny (true), ukryty (false)
        {
            get { return wid; }
        }

        public Sprite(Form okno, Bitmap tło, Bitmap klatki,
                      int n = 1, int x = 0, int y = 0)
        {
            form = okno;
            mtło = tło;
            mkla = klatki;
            if (n <= 0) n = 1;
            rect = new Rectangle(x, y, mkla.Width / n, mkla.Height);
            rkla = new Rectangle(0, 0, rect.Width, rect.Height);
            msav = new Bitmap(rect.Width, rect.Height);
            xmax = (n - 1) * rect.Width;
            wid = false;
        }

        ~Sprite()                               // Destruktor
        {
            if (msav != null) msav.Dispose();
        }

        public virtual void Show()              // Pokaż duszka
        {
            using (Graphics g = Graphics.FromImage(msav))
                g.DrawImage(mtło, 0, 0, rect, GraphicsUnit.Pixel);
            using (Graphics g = Graphics.FromImage(mtło))
                g.DrawImage(mkla, rect, rkla, GraphicsUnit.Pixel);
            using (Graphics g = form.CreateGraphics())
                g.DrawImage(mtło, rect, rect, GraphicsUnit.Pixel);
            wid = true;
        }

        public virtual void Hide()              // Ukryj duszka
        {
            using (Graphics g = Graphics.FromImage(mtło))
                g.DrawImage(msav, rect, 0, 0, Width, Height, GraphicsUnit.Pixel);
            using (Graphics g = form.CreateGraphics())
                g.DrawImage(mtło, rect, rect, GraphicsUnit.Pixel);
            wid = false;
        }

        public virtual void Move(int x, int y)  // Przesuń duszka
        {
            if (wid)
            {
                using (Graphics g = Graphics.FromImage(mtło))
                    g.DrawImage(msav, rect, 0, 0, Width, Height, GraphicsUnit.Pixel);
                Rectangle rnew = new Rectangle(x, y, Width, Height);
                using (Graphics g = Graphics.FromImage(msav))
                    g.DrawImage(mtło, 0, 0, rnew, GraphicsUnit.Pixel);
                if (xmax > 0)
                    rkla.X = (rkla.X < xmax) ? rkla.X + rkla.Width : 0;
                using (Graphics g = Graphics.FromImage(mtło))
                    g.DrawImage(mkla, rnew, rkla, GraphicsUnit.Pixel);
                Rectangle ru = Rectangle.Union(rect, rnew);
                using (Graphics g = form.CreateGraphics())
                    g.DrawImage(mtło, ru, ru, GraphicsUnit.Pixel);
            }
            rect.X = x;
            rect.Y = y;
        }

        public bool Collision(Rectangle r)      // Kolizja duszka z r
        {
            return rect.IntersectsWith(r);
        }

        public bool Contains(int x, int y)      // Zawiera punkt (x, y)
        {
            return rect.Contains(x, y);
        }
    }
}

Program w C# (skrzat)

Kod źródłowy C# klasy formularza zawarty w pliku Form1.cs programu wyświetla­jącego spaceru­jącego skrzata jest przedsta­wiony na poniższym listingu. Na początku w metodzie obsługi zdarzenia Load formu­larza tworzone są dwie mapy bitowe z zasobów PejzażSkrzat programu przedsta­wiające tło animacji i klatki skrzata, ustalany jest rozmiar obszaru robo­czego okna i tworzony obiekt klasy Sprite reprezen­tujący wstępnie niewidzial­nego skrzata usytuo­wanego tuż za prawym brzegiem tego obszaru w odle­głości 6/11 jego wysokości od górnego brzegu. Obie bitmapy są niszczone dopiero po zamknięciu okna – w metodzie obsługi zdarzenia FormClosed formu­larza. Animację uruchamia po wyświe­tleniu skrzata lub wznawia po jej zatrzy­maniu polecenie Start menu, a zatrzy­muje polecenie Stop. Obsługa zdarzenia Tick genero­wanego przez zegar polega na przesu­nięciu skrzata za pomocą metody Move klasy Sprite. Jeżeli obiekt ten nie zniknął po lewej stronie obszaru robo­czego okna, zostaje przesu­nięty o 4 piksele w lewo, zaś w przeci­wnym razie zostaje jak na początku wykonania programu ustawiony tuż za prawym brzegiem obszaru robo­czego, by ponownie odbył wędrówkę w poprzek okna.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Sprites;

namespace Skrzaty
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private Bitmap Tło = null;          // Tło okna
        private Bitmap Klatki = null;       // Kolejne klatki skrzata
        private Sprite skrzat = null;       // Obiekt skrzata

        private void Form1_Load(object sender, EventArgs e)
        {
            Tło = new Bitmap(Properties.Resources.Pejzaż);
            Klatki = new Bitmap(Properties.Resources.Skrzat);
            ClientSize = Pejzaż.Size;
            skrzat = new Sprite(this, Tło, Klatki, 16, Tło.Width, 6 * Tło.Height / 11);
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (Klatki != null) Klatki.Dispose();
            if (Tło != null) Tło.Dispose();
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(Tło, 0, 0);
        }

        private void startToolStripMenuItem_Click(object sender, EventArgs e)
        {
            startToolStripMenuItem.Enabled = false;
            stopToolStripMenuItem.Enabled = true;
            if (!skrzat.Visible) skrzat.Show();
            timer1.Start();
        }

        private void stopToolStripMenuItem_Click(object sender, EventArgs e)
        {
            startToolStripMenuItem.Enabled = true;
            stopToolStripMenuItem.Enabled = false;
            timer1.Stop();
        }

        private void koniecToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (skrzat.X > -skrzat.Width)
                skrzat.Move(skrzat.X - 4, skrzat.Y);
            else
                skrzat.Move(Tło.Width, skrzat.Y);
        }
    }
}

Poniższy rysunek przedstawia widok okna programu uchwycony po ok. 3 sekundach od chwili urucho­mienia animacji.

Latające spodki (program z defektem)

Inna prezentowana poniżej przykładowa animacja przedstawia dwa latające spodki na tle gwieździ­stego nieba. Podobnie jak poprzednio jej działanie polega na wyświe­tlaniu sekwencji prosto­kątnych klatek duszka w jedna­kowych odstępach czasu odmie­rzanych przez kompo­nent zegarowy klasy Timer. W programie o niemal identy­cznym formu­larzu różniącym się jedynie tytułem użyjemy klasy Sprite i dwóch map bitowych umieszczo­nych w zasobach:

Przyjmiemy, że ruch spodków będzie odbywał się w prostoką­tnym zakresie ograni­czonym przez punkty p1p2 (lewy górny i prawy dolny narożnik), a kierunki tego ruchu będą określały pola v1v2 typu Point (przesu­nięcie pierwszego i drugiego obiektu mierzone w pikselach wzdłuż poziomej i pionowej osi). Gdy spodek odleci w lewo lub w prawo poza wyzna­czony zakres, składowa X jego kierunku zostanie odwró­cona na przeciwną, a gdy w górę lub w dół, odwrócona zostanie składowa Y. Efektem tych zmian przy zakresie w miarę dobrze przekra­czającym obszar roboczy okna powstanie wrażenie zniknięcia obiektu z pola widzenia obserwa­tora oraz ponownego jego przylotu. Pozycje i kierunki spodków inicjali­zujemy z niewielką dozą przypadko­wości w metodzie obsługi zdarzenia Load formu­larza, korzy­stając z genera­tora liczb losowych, przez co ich poja­wienie się i pene­tracja okolicy staną się nieco ekscy­tujące.

Oczywiście operacje przesuwania obiektów do nowego miejsca i ewentu­alnej korekty kierunków ich lotu precyzujemy w metodzie obsługi zdarzenia Tick kompo­nentu zegaro­wego, którego właści­wości Interval przypisu­jemy wartość 20. Nie oznacza to jednak, że obydwa będą przesuwane co 20 ms, gdyż ze względu na przyjęte rozwią­zania w nastę­pnych wersjach tego programu będą przesuwane na przemian, czyli każdy co 40 ms. Jako przełą­cznika użyjemy zmiennej logicznej pierwszy przyjmu­jącej naprze­mienne wartości truefalse wskazujące, który obiekt ma zmienić pozycję w kolejnym kroku animacji. Przez analogię do poprze­dniego programu przedsta­wione w ogólnym zarysie rozumo­wanie prowadzi do następu­jącego kodu źródło­wego:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Sprites;

namespace Ufo1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private Bitmap niebo = null;        // Tło okna (nocne niebo)
        private Bitmap spodek = null;       // Klatki spodka
        private Sprite obiekt1 = null;      // Pierwszy spodek
        private Sprite obiekt2 = null;      // Drugi spodek
        private Point p1, p2;               // Zakres ruchu spodków
        private Point v1, v2;               // Kierunki ruchu spodków
        private bool pierwszy = true;       // Ruch pierwszego/drugiego

        private void Form1_Load(object sender, EventArgs e)
        {
            niebo = new Bitmap(Properties.Resources.niebo);
            spodek = new Bitmap(Properties.Resources.spodek);
            ClientSize = niebo.Size;
            p1 = new Point(-spodek.Width / 2, -spodek.Height);
            p2 = new Point(niebo.Width + spodek.Width / 6, niebo.Height + menuStrip1.Height);
            Random gen = new Random();
            obiekt1 = new Sprite(this, niebo, spodek, 3, p1.X, gen.Next(niebo.Height));
            obiekt2 = new Sprite(this, niebo, spodek, 3, gen.Next(niebo.Width), p1.Y);
            v1 = new Point(gen.Next(2, 4), gen.Next(1, 3));
            v2 = new Point(gen.Next(2, 4), gen.Next(1, 3));
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (spodek != null) spodek.Dispose();
            if (niebo != null) niebo.Dispose();
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(niebo, 0, 0);
        }

        private void startToolStripMenuItem_Click(object sender, EventArgs e)
        {
            startToolStripMenuItem.Enabled = false;
            stopToolStripMenuItem.Enabled = true;
            if (!obiekt2.Visible) obiekt2.Show();
            if (!obiekt1.Visible) obiekt1.Show();
            timer1.Start();
        }

        private void stopToolStripMenuItem_Click(object sender, EventArgs e)
        {
            startToolStripMenuItem.Enabled = true;
            stopToolStripMenuItem.Enabled = false;
            timer1.Stop();
        }

        private void koniecToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (pierwszy)
            {
                obiekt1.Move(obiekt1.X + v1.X, obiekt1.Y + v1.Y);
                if (obiekt1.X < p1.X || obiekt1.X > p2.X) v1.X = -v1.X;
                if (obiekt1.Y < p1.Y || obiekt1.Y > p2.Y) v1.Y = -v1.Y;
            }
            else
            {
                obiekt2.Move(obiekt2.X + v2.X, obiekt2.Y + v2.Y);
                if (obiekt2.X < p1.X || obiekt2.X > p2.X) v2.X = -v2.X;
                if (obiekt2.Y < p1.Y || obiekt2.Y > p2.Y) v2.Y = -v2.Y;
            }
            pierwszy = !pierwszy;
        }
    }
}

Uruchomienie animacji skutkuje pojawieniem się dwóch latających spodków na tle gwieździ­stego nieba (rys.). Pierwszy nadlatuje z lewej strony obszaru roboczego okna na przypad­kowej wysokości, drugi z góry obszaru na przypad­kowej szerokości. Gdy spodek zniknie, przekra­czając granicę obszaru roboczego, pojawia się ponownie po chwili w pobliżu, by kontynu­ować swoją misję eksplora­cyjną.

Niestety, program zawiera defekt ujawniający się, gdy spodki znajdą się na kursie kolizyjnym. Po kolizji kontynuują swój lot w zamie­rzonych kierunkach, jakby do niej nie doszło. Ślady wypadku jednak pozostają i sprawiają wrażenie, że trzeci spodek rozpadł się na kilka części (rys.). Przyczyną defektu jest nieuwzglę­dnienie zmiany fragmentu tła zakrytego przez pierwszy spodek (bitmapa msav) zacho­dzącej, gdy w trakcie kolizji drugi spodek zmieni swoją pozycję.

Zauważmy, że w metodzie obsługi polecenia Start najpierw wyświe­tlony zostaje drugi spodek (refe­rencja obiekt2), a nastę­pnie pierwszy (refe­rencja obiekt1). Możemy więc przyjąć, że drugi spodek porusza się na orygi­nalnym tle, zaś pierwszy jakby nieco bliżej obserwa­tora na tle zawiera­jącym drugi spodek (rys. obok). Zatem gdy w trakcie kolizji drugi spodek zostanie przesu­nięty za pomocą metody Move do nowego miejsca, fragment tła zakryty przez pierwszy spodek ulegnie zmianie, która nie zostaje odnoto­wana na jego bitmapie msav. Gdy więc w nastę­pnym kroku przesuwany jest pierwszy spodek, odtwa­rzany przezeń fragment tła sprzed zmiany nie pasuje do zaistnia­łego stanu animacji. Oczywiście zmiana kolej­ności wyświe­tlania spodków nie poprawi sytuacji.

Klasa "Sprite2"

Problem kolizji rozwiązujemy, posługując się klasą pochodną Sprite2 wywodzącą się od klasy bazowej Sprite. Jej dodatkowym składni­kiem będzie pole obiekt zawiera­jące refe­rencję do obiektu klasy Sprite. Załóżmy, że najpierw wyświe­tlany jest duszek klasy Sprite2 (nr 2 na rys. powyżej), a następnie duszek klasy Sprite (nr 1 na rys. powyżej). Zakryta przez duszka nr 1 część tła przecho­wywana na bitmapie msav zostanie zauktua­lizowana podczas ruchu duszka nr 2 przez metody nowej klasy, dzięki czemu obraz animacji pozostanie niezaśmie­cony. Oznacza to, że metody Show (pokaż), Hide (ukryj) i Move (przesuń) klasy pochodnej wymagają przesło­nienia (ang. override, nadpi­sanie), gdyż ich działanie będzie w przy­padku kolizji obiektu tej klasy z obiektem wskazy­wanym przez refe­rencję obiekt inne niż w klasie bazowej.

Nową klasę umieszczamy w pliku Sprite2.cs w prze­strzeni Sprites. Jej składni­kami oprócz nowego pola obiekt typu Sprite powinien być konstru­ktor Sprite2 inicjali­zujący wszystkie pola tworzo­nego obiektu i trzy wyszczegól­nione powyżej metody przesła­niające metody wirtualne klasy bazowej. Postępując analogi­cznie jak w przy­padku definio­wania klasy pochodnej Okrag wywodzącej się od klasy bazowej Punkt (zob. program od punktu do fontanny), otrzymujemy następu­jącą definicję klasy Sprite2:

using System.Drawing;
using System.Windows.Forms;

namespace Sprites
{
    class Sprite2 : Sprite
    {
        private Sprite obiekt;                  // Obiekt kolizyjny

        public Sprite2(Form okno, Bitmap tło, Bitmap klatki, Sprite obiekt,
                       int n = 1, int x = 0, int y = 0) : base(okno, tło, klatki, n, x, y)
        {
            this.obiekt = obiekt;
        }

        public override void Show()             // Pokaż duszka
        {
            bool kolizja = (obiekt != null) && obiekt.Visible && obiekt.Collision(rect);
            if (kolizja) obiekt.Hide();
            base.Show();
            if (kolizja) obiekt.Show();
        }

        public override void Hide()             // Ukryj duszka
        {
            bool kolizja = (obiekt != null) && obiekt.Visible && obiekt.Collision(rect);
            if (kolizja) obiekt.Hide();
            base.Hide();
            if (kolizja) obiekt.Show();
        }

        public override void Move(int x, int y) // Przesuń duszka
        {
            Rectangle ru = Rectangle.Union(rect, new Rectangle(x, y, Width, Height));
            bool kolizja = Visible && (obiekt != null) && obiekt.Visible && obiekt.Collision(ru);
            if (kolizja) obiekt.Hide();
            base.Move(x, y);
            if (kolizja) obiekt.Show();
        }
    }
}

Podana w nagłówku klasy Sprite2 nazwa Sprite poprze­dzona dwukro­pkiem określa klasę bazową, od której wywodzi się klasa pochodna, a wystę­pujące w nagłówku konstru­ktora po dwukropku wywołanie ze słowem kluczowym base w roli nazwy metody jest wywołaniem konstru­ktora klasy bazowej inicjali­zującym odziedzi­czone po niej pola. Słowo to jest również używane jako refe­rencja w metodach Show, HideMove klasy pochodnej w celu uzyskania dostępu do metod klasy bazowej. Wszystkie trzy wspomniane metody działają według tego samego schematu:

Wyjaśnijmy na koniec, że nie ma znaczenia, który z pary obiektów zostanie wyświe­tlony jako pierwszy. Istotnie, gdyby wbrew przyjętemu wcześniej założeniu najpierw wyświetlony został obiekt wskazy­wany przez referencję obiekt, to w przy­padku kolizji przy wyświe­tlaniu obiektu klasy Sprite2 zostałby przez jej metodę Show ukryty, a po wyświe­tleniu go wyświe­tlony jako drugi.

Program w C# (spodki, wersja 2)

Druga wersja programu animacji przedstawiającej dwa latające spodki jest niemal taka sama jak pierwsza. Program dodatkowo korzysta z klasy Sprite2, tworząc w metodzie obsługi zdarzenia Load formu­larza obiekt tej klasy zamiast obiektu klasy Sprite i przypi­sując jego refe­rencję zmiennej obiekt2:

using System;
...               // Pozostałe dyrektywy using
using Sprites;

namespace Ufo2
{
    public partial class Form1 : Form
    {
        ...       // Konstruktor Form1 i deklaracje pól

        private void Form1_Load(object sender, EventArgs e)
        {
            niebo = new Bitmap(Properties.Resources.niebo);
            spodek = new Bitmap(Properties.Resources.spodek);
            ClientSize = niebo.Size;
            p1 = new Point(-spodek.Width / 2, -spodek.Height);
            p2 = new Point(niebo.Width + spodek.Width / 6, niebo.Height + spodek.Height / 2);
            gen = new Random();
            obiekt1 = new Sprite(this, niebo, spodek, 3, p1.X, gen.Next(niebo.Height));
            obiekt2 = new Sprite2(this, niebo, spodek, obiekt1, 3, gen.Next(niebo.Width), p1.Y);
            vx1 = gen.Next(2, 4); vy1 = gen.Next(1, 3);
            vx2 = gen.Next(2, 4); vy2 = gen.Next(1, 3);
        }

        ...       // Pozostałe metody klasy Form1
    }
}

Tym razem lecące na kursie kolizyjnym spodki nie zaśmie­cają powierz­chni animacji, generują jednak rozbłyski będące efektem migotania ekranu. Operacja Move obiektu klasy Sprite2 wymaga bowiem w przy­padku kolizji z obiektem klasy Sprite określonym przez refe­rencję obiekt aż trzykro­tnego kopio­wania prosto­kątnych fragmentów obrazu na powierz­chnię okna. Niewąt­pliwie jest to defekt programu, lecz jest on o wiele mniej istotny od poprze­dniego. Ba, rozbłyski te można nawet odczytywać jako sygnały wysyłane przez przelatu­jące obok siebie obiekty.

Program w C# (spodki, wersja 3)

W trzeciej wersji programu animacji przelatujące obok siebie spodki nie będą genero­wały rozbłysków. Projekt formularza i jego kod źródłowy jest taki sam jak w drugiej wersji programu, modyfi­kacje dotyczą kodu klas SpriteSprite2. Zaczynamy od przebu­dowy klasy Sprite. Bazując na jej pierwotnej wersji, możemy łatwo wydzielić do pomocni­czych metod operacje przygoto­wywania zmienia­jącego się fragmentu obrazu animacji na robo­czej bitmapie mtło (metody InitShow, InitHide, InitMove) i kopiowania z niej tego fragmentu na powierz­chnię okna (metoda FinOper), po czym wykorzystać je w metodach Show, HideMove:

using System.Drawing;
using System.Windows.Forms;

namespace Sprites
{
    public class Sprite             // Wersja 2 klasy Sprite
    {
        ...                         // Składniki poprz. wersji (bez Show, Hide, Move)

        public virtual void Show()              // Pokaż duszka
        {
            InitShow();
            FinOper(rect);
            wid = true;
        }

        public virtual void Hide()              // Ukryj duszka
        {
            InitHide();
            FinOper(rect);
            wid = false;
        }

        public virtual void Move(int x, int y)  // Przesuń duszka
        {
            if (wid)
            {
                InitMove(x, y);
                FinOper(Rectangle.Union(rect, new Rectangle(x, y, Width, Height)));
            }
            rect.X = x;
            rect.Y = y;
        }

        public void InitHide()                  // Zacznij ukrywanie
        {
            using (Graphics g = Graphics.FromImage(mtło))
                g.DrawImage(msav, rect, 0, 0, Width, Height, GraphicsUnit.Pixel);
        }

        public void InitShow()                  // Zacznij pokazywanie
        {
            using (Graphics g = Graphics.FromImage(msav))
                g.DrawImage(mtło, 0, 0, rect, GraphicsUnit.Pixel);
            using (Graphics g = Graphics.FromImage(mtło))
                g.DrawImage(mkla, rect, rkla, GraphicsUnit.Pixel);
        }

        protected void InitMove(int x, int y)   // Zacznij przesuwanie
        {
            using (Graphics g = Graphics.FromImage(mtło))
                g.DrawImage(msav, rect, 0, 0, Width, Height, GraphicsUnit.Pixel);
            Rectangle rnew = new Rectangle(x, y, Width, Height);
            using (Graphics g = Graphics.FromImage(msav))
                g.DrawImage(mtło, 0, 0, rnew, GraphicsUnit.Pixel);
            if (xmax > 0)
                rkla.X = (rkla.X < xmax) ? rkla.X + rkla.Width : 0;
            using (Graphics g = Graphics.FromImage(mtło))
                g.DrawImage(mkla, rnew, rkla, GraphicsUnit.Pixel);
        }

        protected void FinOper(Rectangle r)     // Zakończ operację
        {
            using (Graphics g = form.CreateGraphics())
                g.DrawImage(mtło, r, r, GraphicsUnit.Pixel);
        }
    }
}

Sama przebudowa klasy Sprite wydaje się bezsen­sowna, gdyż zwiększa jedynie stopień kompli­kacji klasy poprzez wprowa­dzenie czterech metod pomocni­czych. Metody te stają się jednak bardzo przydatne w kontekście klasy Sprite2, ponieważ umożli­wiają sformuło­wanie jej metod Show, HideMove z użyciem tylko jednej operacji kopiowania przygoto­wanego w pamięci opera­cyjnej fragmentu obrazu na powierz­chnię okna:

using System.Drawing;
using System.Windows.Forms;

namespace Sprites
{
    class Sprite2 : Sprite                      // Wersja 2 klasy Sprite2
    {
        private Sprite obiekt;                  // Obiekt kolizyjny

        public Sprite2(Form okno, Bitmap tło, Bitmap klatki, Sprite obiekt,
                       int n = 1, int x = 0, int y = 0) : base(okno, tło, klatki, n, x, y)
        {
            this.obiekt = obiekt;
        }

        public override void Show()             // Pokaż duszka
        {
            if ((obiekt != null) && obiekt.Visible && obiekt.Collision(rect))
            {
                obiekt.InitHide();
                InitShow();
                obiekt.InitShow();
                Rectangle ro = new Rectangle(obiekt.X, obiekt.Y, obiekt.Width, obiekt.Height);
                FinOper(Rectangle.Union(rect, ro));
            }
            else
                base.Show();
        }

        public override void Hide()             // Ukryj duszka
        {
            if ((obiekt != null) && obiekt.Visible && obiekt.Collision(rect))
            {
                obiekt.InitHide();
                InitHide();
                obiekt.InitShow();
                Rectangle ro = new Rectangle(obiekt.X, obiekt.Y, obiekt.Width, obiekt.Height);
                FinOper(Rectangle.Union(rect, ro));
            }
            else
                base.Hide();
        }

        public override void Move(int x, int y) // Przesuń duszka
        {
            Rectangle ru = Rectangle.Union(rect, new Rectangle(x, y, Width, Height));
            if (Visible && (obiekt != null) && obiekt.Visible && obiekt.Collision(ru))
            {
                obiekt.InitHide();
                InitMove(x, y);
                obiekt.InitShow();
                Rectangle ro = new Rectangle(obiekt.X, obiekt.Y, obiekt.Width, obiekt.Height);
                FinOper(Rectangle.Union(ru, ro));
                rect.X = x;
                rect.Y = y;
            }
            else
                base.Move(x, y);
        }
    }
}

Zaprezentowany proces tworzenia programu animacji przedstawia­jącej bezkoli­zyjny lot dwóch spodków na tle nocnego nieba uzmysławia, że przy większej liczbie porusza­jących się duszków progra­mista napotka znacznie większe trudności.


Opracowanie przykładu: maj 2020