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

Przykład C#

Układ planetarny Prawo powszechnej grawitacji Model komputerowy układu planetarnego Program w C# Rozwiązanie obiektowe Program w C# (wersja 2) Poprzedni przykład Następny przykład Program w C++ Kontakt

Układ planetarny

Naszym zadaniem jest opracowanie programu okienko­wego w języku C#, który czyta dane dotyczące hipotety­cznego układu planetar­nego z pliku tekstowego o podanej z klawia­tury nazwie, a nastę­pnie prezentuje za pomocą animacji graficznej zachowanie się tego układu w czasie. Zakładamy, że w pierwszym wierszu pliku jest podana liczba ciał układu (nie mniejsza od 2), a w każdym następnym po sześć wartości (pięć liczb rzeczy­wistych i numer lub symbol koloru) oddzie­lonych spacjami i stano­wiących parametry kolej­nego ciała (rys.):

Podobnie jak w przypadku analogi­cznego programu w C++ przyjmiemy, że numer koloru jest liczbą całko­witą od 0 do 15 określa­jącą kolor z zestawu 16 kolorów wybranych z pełnej gamy kolorów trybu RGB:

Nazwa koloru Numer Opis Nazwa koloru Numer Opis
Gold 0         – złoty Khaki 8         – khaki
Blue 1         – niebieski CornflowerBlue 9         – chabrowy
Green 2         – zielony Lime 10         – jasnozielony (limonka)
DarkCyan 3         – ciemny cyan Cyan 11         – cyan
Red 4         – czerwony Tomato 12         – koralowy
Magenta 5         – magenta Violet 13         – fioletowy
Chocolate 6         – czekoladowy Yellow 14         – żółty
LightGray 7         – jasnoszary White 15         – biały

Zbiór ten przypomina zestawy kolorów stosowane w biblio­tece WinBGI C++ i aplika­cjach konso­lowych C#. Istotne różnice dotyczą kolorów przypi­sanych numerom 0 (złoty zamiast czarnego) i 8 (khaki zamiast szarego). Uzasadnie­niem tych zmian jest czarne tło animacji, na którym czarna orbita ciała byłaby niewi­doczna, a szara ledwie dostrze­galna. Kolor można również określać za pomocą kodu HTML, np. zapis #ff6347 lub #FF6347 oznacza kolor pomarań­czowy (Orange). Zestaw wybranych kolorów wygodnie jest umieścić w jednowy­miarowej tablicy o elementach typu Color:

Color[] Kolor = {
    Color.Gold,                 //  0 - złoty
    Color.Blue,                 //  1 - niebieski
    Color.Green,                //  2 - zielony
    Color.DarkCyan,             //  3 - ciemny cyan
    Color.Red,                  //  4 - czerwony
    Color.Magenta,              //  5 - magenta
    Color.Chocolate,            //  6 - czekoladowy
    Color.LightGray,            //  7 - jasnoszary
    Color.Khaki,                //  8 - khaki
    Color.CornflowerBlue,       //  9 - chabrowy
    Color.Lime,                 // 10 - jasnozielony (limonka)
    Color.Cyan,                 // 11 - cyan
    Color.Coral,                // 12 - koralowy
    Color.Violet,               // 13 - fioletowy
    Color.Yellow,               // 14 - żółty
    Color.White};               // 15 - biały

Wprowadzanie parametrów ciał układu planetar­nego z klawia­tury w trakcie wykonania programu byłoby uciążliwe, lepiej użyć pliku tekstowego. Dodatkowym atutem takiego rozwią­zania jest możliwość łatwej modyfi­kacji danych, która przy dużej niestabil­ności układu wynika­jącej z nieauten­tyczności danych jest bardzo pożądana. Współ­rzędne pozycji ciał i składowe ich prędkości są dostoso­wane do płaskiego obszaru roboczego okna o rozmiarze 800x600 pikseli. Założenie dwuwymia­rowości przestrzeni nie jest pozbawione sensu, gdyż orbity planet Układu Słone­cznego leżą niemal w jednej płaszczyźnie.

Prawo powszechnej grawitacji

Niech ciało o masie m1 znajduje się w punkcie (x1,y1), a ciało o masie m2 w punkcie (x2,y2). Prawo powsze­chnego ciążenia Newtona, zwane również prawem powsze­chnej grawi­tacji, orzeka, że siła działająca pomiędzy tymi ciałami jest siłą przycią­gającą, skierowaną wzdłuż prostej łączącej te punkty, i ma wartość

gdzie G jest stałą uniwer­salną, a r jest odległo­ścią pomiędzy tymi punktami:

Prawo powsze­chnego ciążenia możemy przedstawić w postaci wekto­rowej, która określa składowe Fx i Fy siły F w układzie współrzę­dnych x i y. Miano­wicie składowe siły przycią­gania, jakiej ciało m1 doznaje ze strony ciała m2, mają wartości:

Nietrudno uogólnić te wzory dla dowolnej liczby n≥2 ciał o masach m1, m2, ..., mn. Zakładamy, że znajdują się one na wspólnej płaszczy­źnie w punktach (x1,y1), (x2,y2), ..., (xn,yn). Składowe FxpFyp siły wypad­kowej Fp, jakiej doznaje ciało mp ze strony wszystkich pozosta­łych ciał, wyrażają się wzorami:

w których

Sile Fp przyciągania ciała mp przez wszystkie pozostałe ciała towarzyszy przyśpie­szenie wyrażające zmianę prędkości tego ciała w czasie. Oznaczmy go przez ap. Związek pomiędzy działa­jącą na ciało siłą a przyśpie­szeniem i masą określa druga zasada dynamiki Newtona:

Stąd i podanych wyżej wzorów określa­jących składowe siły przycią­gania wynika, że składowe axp i ayp przyśpie­szenia ap wynoszą:

Model komputerowy układu planetarnego

Niech ciało mp porusza się z prędko­ścią vp o skła­dowych vxp i vyp. Jeżeli dt jest niewielkim odstępem czasowym, to po jego upływie nową pozycię ciała można wyznaczyć w sposób przybliżony według schematu:

Równie prosto można obliczyć nowe składowe prędkości ciała po upływie odstępu dt, której zmianę w czasie określa przyśpie­szenie:

Wzory te posłużą do zobrazowania ruchu ciał na ekranie monitora kompute­rowego. Przyjmiemy, że uniwer­salna stała grawi­tacji G jest równa 1. Takie założenie nie ogranicza istoty problemu, a jedynie ułatwia opraco­wanie programu, ponieważ nie trzeba wtedy używać dodatko­wego współczyn­nika skalowania dla danych dopaso­wanych do rozmiaru powierzchni ryso­wania. Powinniśmy również podjąć decyzję odnośnie ich reprezen­tacji. Narzuca­jącym się rozwią­zaniem jest implemen­tacja tablicowa, wygodna ze względu na obecność indeksów w powyższych wzorach:

private int n = 0;              // Liczba ciał
private double[] m;             // Masy
private double[] x, y;          // Współrzędne pozycji
private double[] vx, vy;        // Składowe prędkości
private Color[] kolor;          // Kolory

Efektem wyznaczania pozycji ciała w kolej­nych momentach przy niewielkim odstępie czasowym dt (stała DT równa 0.1) i zazna­czania ich koloro­wymi pikselami na czarnej bitmapie nieba będzie krzywa wyobraża­jąca orbitę tego ciała, a oznaczanie małym obrazkiem (białym okręgiem wypeł­nionym kolorem orbity) bieżącej pozycji ciała na powierz­chni obszaru roboczego okna sprawi wrażenie jego ruchu po tej orbicie. Stałe określa­jące parametry bitmap i zmienne zawiera­jące refe­rencje do nich można zadekla­rować następu­jąco:

private const int NW = 800;     // Szerokość bitmapy nieba
private const int NH = 600;     // Wysokość bitmapy nieba
private const int CW = 5;       // Rozmiar bitmapki ciała
private const int PW = CW / 2;  // Przesunięcie jej środka

private Bitmap[] ciało;         // Bitmapki ciał
private Bitmap niebo = null;    // Bitmapa nieba

Metody rysowania ciała o indeksie p na czarnej bitmapie nieba i powierz­chni obszaru roboczego okna reprezen­towanej przez obiekt g klasy Graphics i przesu­wania go po niej mogą mieć postać:

private void RysujObiekt(int p, bool punkcik, Graphics g)
{
    int a = (int)x[p], b = (int)y[p];
    if (punkcik)
    {
        if (a >= 0 && b >= 0 && a < niebo.Width && b < niebo.Height)
            niebo.SetPixel(a, b, kolor[p]);
        g.DrawImage(ciało[p], a - PW, b - PW);
    }
    else
    {
        Rectangle r = new Rectangle(a - PW, b - PW, CW, CW);
        g.DrawImage(niebo, r.Left, r.Top, r, GraphicsUnit.Pixel);
    }
}

private void PrzesunObiekt(int p, Graphics g)
{
    RysujObiekt(p, false, g);
    x[p] += DT * vx[p];
    y[p] += DT * vy[p];
    RysujObiekt(p, true, g);
}

Metoda Rysuj­Obiekt ustawia na bitmapie nieba kolorowy piksel wyobraża­jący ciało i rysuje na powierz­chni okna obrazek ciała, gdy wartością argu­mentu punkcik jest true, bądź usuwa obrazek ciała z powierz­chni okna, kopiując w jego miejsce stosowny fragment bitmapy nieba, gdy wartością argu­mentu punkcik jest false. Natomiast metoda Przesun­Obiekt usuwa obrazek ciała z powierz­chni okna, wyznacza współ­rzędne nowej pozycji ciała oraz ustawia kolorowy piksel w nowym miejscu bitmapy nieba i rysuje obrazek ciała na powierz­chni okna.

Nieco bardziej złożona jest metoda korygo­wania prędkości ciała po jego przesu­nięciu. Obliczenie nowych składowych prędkości wymaga bowiem sekwen­cyjnego przeglą­dania wszystkich pozosta­łych ciał celem wyzna­czenia przyśpie­szenia wypadko­wego wyraża­jącego szybkość zmiany prędkości tego ciała:

void KorygujPredkosc(int p)
{
    double ax = 0, ay = 0, dx, dy, r3;
    for (int i = 0; i < n; i++)
        if (i != p)
        {
            dx = x[p] - x[i];
            dy = y[p] - y[i];
            r3 = dx * dx + dy * dy;
            r3 *= Math.Sqrt(r3);
            ax -= m[i] * dx / r3;
            ay -= m[i] * dy / r3;
        }
    vx[p] += DT * ax;
    vy[p] += DT * ay;
}

Animacja ukazująca zachowanie się obiektów układu planetar­nego w czasie będzie oparta na kompo­nencie zegarowym klasy Timer, który generuje zdarzenia z częstotli­wością ustaloną we właści­wości Interval (przyjęto 15 milisekund). Metoda obsługi tych zdarzeń będzie polegać na przesu­nięciu wszystkich ciał układu do nowego miejsca i skorygo­waniu ich prędkości:

private void timer1_Tick(object sender, EventArgs e)
{
    using (Graphics g = CreateGraphics())
        for (int p = 0; p < n; p++)
            PrzesunObiekt(p, g);
    for (int p = 0; p < n; p++)
        KorygujPrędkość(p);
}

Program w C#

Rozbudowę formularza aplikacji rozpoczy­namy od nadania mu tytułu Układ planetarny i usta­wienia jego właści­wości FormBorder­Style na Fixed­Single (pojedyncza stała ramka) i Maxi­mizeBox na False (zabloko­wana maksymali­zacja). Następnie wstawiamy do niego menu z polece­niami Dane, Stop (wyłą­czone), Dalej (wyłą­czone) i Koniec oraz komponent otwie­rania pliku i zegar (rys.). Polecenie Dane ma po wybraniu pliku wejścio­wego wczytać zawarte w nim dane i uruchomić animację, Stop zatrzymać trwającą animację, Dalej wznowić ją po zatrzy­maniu, a Koniec zakończyć przebieg programu. Status tych poleceń określany we właści­wości Enabled (True – włączone, False – wyłą­czone) będzie, z wyjątkiem ostatniego, zależny od zaistniałej sytuacji. Mianowicie polecenie Dane ma być dostępne na początku wykonania programu i po zatrzy­maniu animacji, Stop tylko w trakcie animacji, a Dalej po jej zatrzy­maniu.

Na koniec we właści­wości FileName kompo­nentu okna dialogo­wego otwie­rania pliku usuwany domyślną nazwę, a w jego właści­wości Filter ograni­czamy dostęp tylko do plików tekstowych (ew. zwycza­jowo dopuszczamy wszystkie pliki). Sensowną wartością tej właści­wości jest tekst

Pliki tekstowe (*.txt)|*.txt|Wszystkie pliki (*.*)|*.*

Ustalamy również szybkość animacji, przypisując właści­wości Interval zegara wartość 15. Niesprecy­zowany dotąd rozmiar okna aplikacji najwygo­dniej jest określić w metodzie obsługi zdarzenia Load formu­larza, przyjmując, że jego obszar roboczy jest tej samej wielkości co bitmapa nieba. W platformie .NET menu główne mieści się w obszarze robo­czym okna (inaczej jest np. w środo­wisku Delphi, w którym menu znajduje się, podobnie jak ramka i pasek tytułowy, w obszarze syste­mowym okna). Zatem aby nie tracić części bitmapy nieba, poszerzymy ją u góry o poziomy pasek wielkości menu, który będzie przezeń zasła­niany. Operację tworzenia czarnej bitmapy nieba oraz wyświe­tlanie jej i bitmapek ciał układu możemy zaprogra­mować nastę­pująco:

private void Form1_Load(object sender, EventArgs e)
{
    niebo = new Bitmap(NW, NH + menuStrip1.Height);
    using (Graphics g = Graphics.FromImage(niebo))
        g.Clear(Color.Black);
    ClientSize = new Size(niebo.Width, niebo.Height);
}

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.DrawImage(niebo, 0, 0);
    for (int p = 0; p < n; p++)
        RysujObiekt(p, true, e.Graphics);
}

Oczywiście przy zerowej liczbie ciał żadne nie jest przez metodę obsługi zdarzenia Paint rysowane, ale gdy np. po wczytaniu danych zwinięte okno aplikacji zostanie rozwi­nięte, metoda odtworzy na ekranie bitmapę nieba z orbitami wszystkich ciał i ich obrazkami.

Aby uniknąć wycieku pamięci, utworzona w metodzie Form1_Load bitmapa nieba powinna zostać na końcu wykonania programu zniszczona za pomocą metody Dispose, gdyż mechanizm odśmie­cania pamięci (ang. garbage collector, GC) tego nie zrobi. Zniszczone powinny być też wtedy wszystkie bitmapki ciał. Ta druga operacja jest również wymagana, gdy po przer­waniu animacji mają być wczytane dane opisujące inny układ planetarny. Rozwa­żania te prowadzą do sformu­łowania następu­jącej metody usuwania bitmapek ciał i metody obsługi zdarzenia FormClosed formularza wyzwa­lanej po zamknięciu okna aplikacji:

private void UsuńBitmapki()
{
    while (n > 0)
    {
        n--;
        if (ciało[n] != null) ciało[n].Dispose();
    }
}

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

Pomijając kwestię kontroli poprawności danych, metodę wczyty­wania ich ze stru­mienia teksto­wego do sześciu tablic dynami­cznych i tworzenia obrazków ciał można sformu­łować w postaci:

private void CzytajDane(StreamReader plik)   // Wersja uproszczona
{
    n = int.Parse(plik.ReadLine().Trim());
    m = new double[n];
    x = new double[n];
    y = new double[n];
    vx = new double[n];
    vy = new double[n];
    kolor = new Color[n];
    ciało = new Bitmap[n];
    for (int p = 0; p < n; p++)
    {
        string[] s = plik.ReadLine().Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
        m[p] = double.Parse(s[0]);
        x[p] = double.Parse(s[1]);
        y[p] = double.Parse(s[2]) + menuStrip1.Height;
        vx[p] = double.Parse(s[3]);
        vy[p] = double.Parse(s[4]);
        if (s[5][0] != '#')
            kolor[p] = Kolor[int.Parse(s[5])];
        else
            kolor[p] = ColorTranslator.FromHtml(s[5]);
        ciało[p] = new Bitmap(CW, CW);
        using (Graphics g = Graphics.FromImage(ciało[p]))
        {
            g.Clear(Color.Black);
            using (Brush pędzel = new SolidBrush(kolor[p]))
                g.FillEllipse(pędzel , 0, 0, CW - 1, CW - 1);
            g.DrawEllipse(Pens.White, 0, 0, CW - 1, CW - 1);
        }
        ciało[p].MakeTransparent(Color.Black);
    }
}

Metoda najpierw wczytuje liczbę ciał układu i rezer­wuje pamięć dla siedmiu tablic dynami­cznych, w których będą przecho­wywane masy, współ­rzędne pozycji, składowe prędkości, kolory orbit i obrazki ciał. Jeżeli przedtem istniała­łyby takie tablice, utracenie w ten sposób referencji do nich stanowi dla mecha­nizmu GC przesłanie, że nie są one już potrzebne, więc może zwolnić przydzie­loną im pamięć. Wszystkie elementy nowych tablic są wypełniane bajtami zerowymi, co w przypadku tablicy bitmapek oznacza, że wartościami null. Następnie metoda CzytajDane wczytuje dla każdego ciała wiersz tekstu, który po rozło­żeniu za pomocą metody Split na tablicę łańcuchów interpre­tuje jako dane ciała i zapisuje w tablicach. Pierwszym argu­mentem metody Split jest tablica znaków występu­jących w roli ograni­czników (separa­torów). Jeżeli jest pusta, metoda traktuje spacje jako ograni­czniki. Wczytany z pliku łańcuch można więc podzielić spacjami na ciąg łańcuchów za pomocą instrukcji

string[] s = plik.ReadLine().Split(new char[] { ' ' });

lub

string[] s = plik.ReadLine().Split((char[])null);

Jeżeli dwa ograni­czniki występują obok siebie, określonym przez nie elementem rozkładu jest łańcuch pusty. W rozpatry­wanym przypadku jest to niepożą­dane, ale puste łańcuchy rozkładu można pominąć, wywołując metodę z dodat­kowym argu­mentem RemoveEmpty­Entries (usuń puste wpisy) typu wylicze­niowego StringSplit­Options.

Wyjaśnijmy jeszcze, że metoda CzytajDane uwzglę­dnia rozmiar powiększonej o pasek menu bitmapy nieba, dodając do wczytanej współrzę­dnej y pozycji ciała wysokość menu. Umożliwia również precyzo­wanie koloru orbity ciała zarówno za pomocą liczby całko­witej od 0 do 15 stano­wiącej indeks elementu tablicy Kolor, jak i kodu szesnas­tkowego HTML, który konwertuje na wartość typu Color, wywołując metodę FromHtml klasy ColorTrans­lator. Ponadto po utwo­rzeniu obrazka ciała ustawia za pomocą metody MakeTrans­parent klasy Bitmap jego czarne tło jako przezro­czyste.

Sprawdzanie poprawności danych w pełnej wersji metody CzytajDane obejmuje liczbę ciał układu oraz liczby i wartości parametrów każdego z nich. W przy­padku nieodpo­wiednich danych zgłaszany jest wyjątek, który jest przechwy­tywany w metodzie obsługi pole­cenia Dane menu:

private void daneToolStripMenuItem_Click(object sender, EventArgs e)
{
    if (openFileDialog1.ShowDialog() == DialogResult.OK)
    {
        using (Graphics g = Graphics.FromImage(niebo))
            g.Clear(Color.Black);
        UsuńBitmapki();
        dalejToolStripMenuItem.Enabled = false;
        try
        {
            using (StreamReader plik = new StreamReader(openFileDialog1.FileName))
                CzytajDane(plik);
            Invalidate();
            daneToolStripMenuItem.Enabled = false;
            stopToolStripMenuItem.Enabled = true;
            timer1.Enabled = true;
        }
        catch (Exception ex)
        {
            UsuńBitmapki();
            Invalidate();
            MessageBox.Show(ex.Message, "Błąd", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
}

Jak widać, po wybraniu pliku przez użytko­wnika metoda czyści bitmapę nieba, usuwa bitmapki ciał (jeśli istnieją) i blokuje polecenie Dalej na pasku menu, a następnie w bloku chronionym try–catch czyta z pliku dane, rysuje na powierz­chni okna wszystkie ciała układu, blokuje polecenie Dane i włącza polecenie Stop, a na koniec rozpo­czyna animację dla wczyta­nych danych, urucha­miając zegar. Błąd danych powoduje usunięcie nowo utworzonych bitmapek (z wyzero­waniem liczby ciał układu), wyświe­tlenie czystej bitmapy nieba i wyprowa­dzenie stosownego komunikatu. Działanie metod obsługi poleceń StopDalej polega jedynie na zmianie statusu trzech pierwszych poleceń menu oraz zatrzy­maniu lub urucho­mieniu zegara steru­jącego animacją.

Pełny kod źródłowy C# w pliku Form1.cs programu, który wczytuje z pliku tekstowego dane opisujące hipote­tyczny układ plane­tarny i prezentuje na ekranie ruch ciał tego układu, jest przedsta­wiony na poniższym listingu.

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 System.IO;

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

        private Color[] Kolor = {
            Color.Gold,                 //  0 - złoty
            Color.Blue,                 //  1 - niebieski
            Color.Green,                //  2 - zielony
            Color.DarkCyan,             //  3 - ciemny cyan
            Color.Red,                  //  4 - czerwony
            Color.Magenta,              //  5 - magenta
            Color.Chocolate,            //  6 - czekoladowy
            Color.LightGray,            //  7 - jasnoszary
            Color.Khaki,                //  8 - khaki
            Color.CornflowerBlue,       //  9 - chabrowy
            Color.Lime,                 // 10 - jasnozielony (limonka)
            Color.Cyan,                 // 11 - cyan
            Color.Coral,                // 12 - koralowy
            Color.Violet,               // 13 - fioletowy
            Color.Yellow,               // 14 - żółty
            Color.White};               // 15 - biały

        private const double DT = 0.1;  // Odstęp czasowy
        private const int NW = 800;     // Szerokość bitmapy nieba
        private const int NH = 600;     // Wysokość bitmapy nieba
        private const int CW = 5;       // Rozmiar bitmapki ciała
        private const int PW = CW / 2;  // Przesunięcie jej środka

        private int n = 0;              // Liczba ciał
        private double[] m;             // Masy
        private double[] x, y;          // Współrzędne pozycji
        private double[] vx, vy;        // Składowe prędkości
        private Color[] kolor;          // Kolory
        private Bitmap[] ciało;         // Bitmapki ciał
        private Bitmap niebo;           // Bitmapa nieba (+ pasek menu)

        private void Form1_Load(object sender, EventArgs e)
        {
            niebo = new Bitmap(NW, NH + menuStrip1.Height);
            using (Graphics g = Graphics.FromImage(niebo))
                g.Clear(Color.Black);
            ClientSize = new Size(niebo.Width, niebo.Height);
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(niebo, 0, 0);
            for (int p = 0; p < n; p++)
                RysujObiekt(p, true, e.Graphics);
        }

        private void UsuńBitmapki()
        {
            while (n > 0)
            {
                n--;
                if (ciało[n] != null) ciało[n].Dispose();
            }
        }

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

        private void CzytajDane(StreamReader plik)
        {
            n = int.Parse(plik.ReadLine().Trim());
            if (n < 2)
                throw new Exception("Wymagane co najmniej dwa ciała układu.");
            m = new double[n];
            x = new double[n];
            y = new double[n];
            vx = new double[n];
            vy = new double[n];
            kolor = new Color[n];
            ciało = new Bitmap[n];
            for (int p = 0; p < n; p++)
            {
                string[] s = plik.ReadLine().Split(
                             (char[])null, StringSplitOptions.RemoveEmptyEntries);
                if (s.Length < 6)
                    throw new Exception("Liczba danych w wierszu mniejsza od 6.");
                m[p] = double.Parse(s[0]);
                x[p] = double.Parse(s[1]);
                y[p] = double.Parse(s[2]) + menuStrip1.Height;
                vx[p] = double.Parse(s[3]);
                vy[p] = double.Parse(s[4]);
                if (s[5][0] != '#')
                    kolor[p] = Kolor[int.Parse(s[5]) % Kolor.Length];
                else
                    kolor[p] = ColorTranslator.FromHtml(s[5]);
                ciało[p] = new Bitmap(CW, CW);
                using (Graphics g = Graphics.FromImage(ciało[p]))
                {
                    g.Clear(Color.Black);
                    using(Brush pióro = new SolidBrush(kolor[p]))
                        g.FillEllipse(pióro, 0, 0, CW - 1, CW - 1);
                    g.DrawEllipse(Pens.White, 0, 0, CW - 1, CW - 1);
                }
                ciało[p].MakeTransparent(Color.Black);
            }
        }

        private void RysujObiekt(int p, bool punkcik, Graphics g)
        {
            int a = (int)x[p], b = (int)y[p];
            if (punkcik)
            {
                if (a >= 0 && b >= 0 && a < niebo.Width && b < niebo.Height)
                    niebo.SetPixel(a, b, kolor[p]);
                g.DrawImage(ciało[p], a - PW, b - PW);
            }
            else
            {
                Rectangle r = new Rectangle(a - PW, b - PW, CW, CW);
                g.DrawImage(niebo, r.Left, r.Top, r, GraphicsUnit.Pixel);
            }
        }

        private void PrzesunObiekt(int p, Graphics g)
        {
            RysujObiekt(p, false, g);
            x[p] += DT * vx[p];
            y[p] += DT * vy[p];
            RysujObiekt(p, true, g);
        }

        private void KorygujPrędkość(int p)
        {
            double ax = 0, ay = 0, dx, dy, r3;
            for (int i = 0; i < n; i++)
                if (i != p)
                {
                    dx = x[p] - x[i];
                    dy = y[p] - y[i];
                    r3 = dx * dx + dy * dy;
                    r3 *= Math.Sqrt(r3);
                    ax -= m[i] * dx / r3;
                    ay -= m[i] * dy / r3;
                }
            vx[p] += DT * ax;
            vy[p] += DT * ay;
        }

        private void daneToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                using (Graphics g = Graphics.FromImage(niebo))
                    g.Clear(Color.Black);
                UsuńBitmapki();
                dalejToolStripMenuItem.Enabled = false;
                try
                {
                    using (StreamReader plik = new StreamReader(openFileDialog1.FileName))
                        CzytajDane(plik);
                    Invalidate();
                    daneToolStripMenuItem.Enabled = false;
                    stopToolStripMenuItem.Enabled = true;
                    timer1.Enabled = true;
                }
                catch (Exception ex)
                {
                    UsuńBitmapki();
                    Invalidate();
                    MessageBox.Show(ex.Message, "Błąd", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }

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

        private void dalejToolStripMenuItem_Click(object sender, EventArgs e)
        {
            daneToolStripMenuItem.Enabled = false;
            stopToolStripMenuItem.Enabled = true;
            dalejToolStripMenuItem.Enabled = false;
            timer1.Enabled = true;
        }

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

        private void timer1_Tick(object sender, EventArgs e)
        {
            using (Graphics g = CreateGraphics())
                for (int p = 0; p < n; p++)
                    PrzesunObiekt(p, g);
            for (int p = 0; p < n; p++)
                KorygujPrędkość(p);
        }
    }
}

Poniższy rysunek przedstawia orbity ciał wygene­rowane przez program po ok. jednej minucie i 30 sekun­dach trwania animacji. Plik wejściowy zawierał dane dla układu złożonego z sześciu ciał, z których pierwsze o najwię­kszej masie symboli­zuje gwiazdę (kolor złoty), trzy kolejne są planetami obiega­jącymi gwiazdę po niemal kołowych orbitach (kolory cyan, jasno­zielony i chabrowy), a dwa najmniejsze to komety porusza­jące się po wydłu­żonej elipsie (kolor magenta) i hiperboli (kolor czekoladowy):

   m       x     y     vx    vy   kolor
  8000    400   300     0     0     0
  0,8     475   300     0   -10    11
  0,6     250   300     0     7    10
  1,25    400    60    -6     0     9
  0,0001   50     0     1     3,25  5
  0,0001  800   600    -4    -6     6

Mały obrazek pośrodku układu wyobraża gwiazdę. Bardziej odpowie­dnim jej reprezen­tantem byłby np. obrazek gwiazdki, ryso­wanie go wymaga­łoby jednak modyfi­kacji programu. Inny sposób wyekspono­wania gwiazdy polega na zastą­pieniu jej dwoma krążącymi blisko siebie ciałami symboli­zującymi gwiazdę podwójną. Poniższy rysunek utwo­rzony przez program w porówny­walnym czasie ukazuje orbity siedmiu ciał dla podobnych danych z gwiazdą podwójną (kolor złoty i czerwony) zamiast poje­dynczej:

   m       x     y     vx    vy   kolor
  4000    400   295   -11     0     0
  4000    400   305    11     0     4
  0,8     475   300     0   -10    11
  0,6     250   300     0     7    10
  1,25    400    60    -6     0     9
  0,0001   50     0     1     3,25  #ff69b4   HotPink
  0,0001  800   600    -4    -6     6

(siódmy łańcuch będący nazwą koloru po szesnastkowym jego kodzie jest przez program ignoro­wany, można go traktować jak komentarz). Porównując obydwa rysunki nietrudno zauważyć, że chociaż łączna masa gwiazdy podwójnej jest równa masie gwiazdy pierwo­tnej, orbity pozosta­łych ciał nieco się różnią. Zaburzona orbita elipty­czna komety na drugim rysunku nasuwa skoja­rzenie z kometą Halleya, która wraca w pobliże Słońca średnio co 75 lat. Jej ruch po wydłu­żonej elipsie jest zakłócany przez planety Układu Słone­cznego, głównie Jowisza i Saturna. Przejście komety blisko Ziemi w 1910 roku (w odle­głości 22,4 mln km) było szcze­gólnie widowi­skowe, zaś ostatnie w 1986 roku (150 mln km) bardzo rozczaro­wujące.

Rozwiązanie obiektowe

Wadą zastosowanej implemen­tacji tabli­cowej hipotety­cznego układu plane­tarnego jest rozpro­szenie informacji o każdym ciele w różnych miejscach (siedmiu tablicach) i oddzie­lenie metod opisu­jących zachowanie się ciał układu od ich danych. Obser­wator rzeczywi­stego układu planetar­nego odnosi wrażenie, że każde ciało nie tylko ma swoje cechy, jak np. masa, pozycja w prze­strzeni i nawet kolor (Słońce jest żółte, Ziemia niebieska, Mars rdzawo­czerwony, Księżyc srebrny), ale charakte­ryzuje się też pewnym zacho­waniem, np. zmienia swoją pozycję i prędkość, przyciąga inne ciała. Dzieje się to oczywiście pod wpływem wszecho­becnej grawitacji, ale mózg ludzki w naturalny sposób kojarzy zacho­wanie obiektu z jego cechami. Jednakże opisujące to zacho­wanie metody w powyższym programie nie stanowią jednej całości wraz z danymi, na których działają, nie są formalnie powiązane ani ze sobą, ani z danymi.

Odzwierciedleniem rzeczywistych obiektów w C# są klasy i obiekty. W implemen­tacji obiektowej układu planetar­nego potrzebna jest tylko jedna tablica dynami­czna, której elemen­tami są obiekty opisujące cechy i zacho­wanie ciał tego układu, a dokła­dniej referencje do reprezen­tujących te ciała obiektów. Aby tworzyć obiekty, należy najpierw zdefi­niować klasę stanowiącą pewnego rodzaju przepis (szablon, wzorzec, typ), według którego można je budować. Nową klasę definiu­jemy po zaprojekto­waniu identy­cznego jak poprze­dnio formu­larza drugiej wersji aplikacji. Definicję klasy możemy umieścić w pliku Form1.cs po definicji klasy Form1 bądź w odrębnym pliku. Wybieramy drugie rozwią­zanie, dodając do projektu plik (pole­cenie Dodaj klasę...), którego nazwę Class.cs zmie­niamy na Obiekt.cs, nadając tym samym nowej klasie nazwę Obiekt.

Klasa Obiekt ma korzystać z podsta­wowych narzędzi grafi­cznych do ryso­wania punktu na bitmapie nieba i obrazka ciała na powierz­chni okna, toteż do zestawu dyrektyw using w wygenero­wanym przez Visual Studio kodzie C# dopisu­jemy dyrektywę włącza­jącą przestrzeń nazw System.­Drawing. Pełną defi­nicję klasy można łatwo sformu­łować na podstawie przedsta­wionego powyżej kodu źródło­wego formu­larza pierwszej wersji programu:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;

namespace Planety2
{
    public class Obiekt
    {
        private const int CW = 5;       // Rozmiar bitmapki ciała
        private const int PW = CW / 2;  // Przesunięcie jej środka
        private double m;               // Masa ciała
        private double x, y;            // Współrzędne pozycji
        private double vx, vy;          // Składowe prędkości
        private Color kolor;            // Kolor
        private Bitmap ciało;           // Bitmapka ciała

        public Obiekt(double masa, double xPoc, double yPoc, double vxPoc, double vyPoc, Color kol)
        {
            m = masa;
            x = xPoc;
            y = yPoc;
            vx = vxPoc;
            vy = vyPoc;
            kolor = kol;
            ciało = new Bitmap(CW, CW);
            using (Graphics g = Graphics.FromImage(ciało))
            {
                g.Clear(Color.Black);
                using (Brush pędzel = new SolidBrush(kolor))
                    g.FillEllipse(pędzel, 0, 0, CW - 1, CW - 1);
                g.DrawEllipse(Pens.White, 0, 0, CW - 1, CW - 1);
            }
            ciało.MakeTransparent(Color.Black);
        }

        ~Obiekt()
        {
            if (ciało != null) ciało.Dispose();
        }

        public void Rysuj(bool punkcik, Graphics g, Bitmap niebo)
        {
            int a = (int)x, b = (int)y;
            if (punkcik)
            {
                if (a >= 0 && b >= 0 && a < niebo.Width && b < niebo.Height)
                    niebo.SetPixel(a, b, kolor);
                g.DrawImage(ciało, a - PW, b - PW);
            }
            else
            {
                Rectangle r = new Rectangle(a - PW, b - PW, CW, CW);
                g.DrawImage(niebo, r.Left, r.Top, r, GraphicsUnit.Pixel);
            }
        }

        public void Przesuń(Graphics g, Bitmap niebo, double dt)
        {
            Rysuj(false, g, niebo);
            x += dt * vx;
            y += dt * vy;
            Rysuj(true, g, niebo);
        }

        public void KorygujPrędkość(Obiekt[] obiekty, double dt)
        {
            double ax = 0, ay = 0, dx, dy, r3;
            foreach (Obiekt obiekt in obiekty)
                if (obiekt != this)
                {
                    dx = x - obiekt.x;
                    dy = y - obiekt.y;
                    r3 = dx * dx + dy * dy;
                    r3 *= Math.Sqrt(r3);
                    ax -= obiekt.m * dx / r3;
                    ay -= obiekt.m * dy / r3;
                }
            vx += dt * ax;
            vy += dt * ay;
        }
    }
}

Dwa pola klasy Obiekt są stałymi przejętymi z poprze­dniego programu, a siedem zmiennymi pełniącymi tę samą rolę co odpowiednie elementy występu­jących w nim tablic. Wszystkie pola są prywatne, co oznacza, że dostęp do nich mają tylko metody klasy, które z kolei są publiczne, więc można się do nich odwoływać spoza klasy. Metoda Obiekt jest konstru­ktorem służącym do tworzenia obiektów (instancji) klasy, natomiast ~Obiekt jest destru­ktorem wywoły­wanym automa­tycznie przez mechanizm GC po zakoń­czeniu używania obiektu. Zadaniem konstru­ktora jest inicjali­zacja pól obiektu wartościami argumentów i utworzenie bitmapki reprezen­tującej obrazek ciała z przezro­czystym tłem, zaś destru­ktora zniszczenie tej bitmapki.

Pozostałe metody określają funkcjonal­ność obiektu, odgrywając te same funkcje jak metody o podobnych nazwach w poprze­dnim programie. Nie mają one jednak argumentu identyfi­kującego obiekt (indeks p), jako składniki obiektu mają bowiem bezpo­średni dostęp do jego pól. Dodatkowy argument metod RysujPrzesuń udostę­pnia bitmapę nieba, która nie jest składni­kiem klasy Obiekt, a ma być na niej zazna­czana pikselami koloru orbity pozycja ciała reprezento­wanego przez obiekt. Dwóm ostatnim metodom przekazy­wany jest również odstęp czasowy niezbędny przy wyzna­czaniu przesu­nięcia ciał i korygo­waniu ich prędkości.

W metodzie KorygujPrędkość wymagany jest dostęp do pozosta­łych obiektów układu planetar­nego, umożliwia go tablica wszystkich obiektów przeka­zywana w pierwszym argu­mencie. Elementy tej tablicy nie są jednak przeglą­dane jak poprzednio za pomocą trady­cyjnej pętli for, lecz poręczniej­szej w tym przypadku pętli foreach. Wszystkie elementy tablicy są w jej kolejnych krokach utożsa­miane ze zmienną itera­cyjną obiekt klasy Obiekt. Wartością tej zmiennej jest referencja do aktualnie przeglą­danego obiektu, zaś słowo kluczowe this jest referencją identyfi­kującą obiekt, którego dotyczy metoda. Wynik porównania obu tych referencji jest taki sam jak wynik porównania indeksu przeglą­danego elementu tablicy z indeksem elementu reprezen­tującego obiekt, którego prędkość ma być skory­gowana.

Program w C# (wersja 2)

Po tych rozważaniach możemy przejść do precyzo­wania kodu C# formu­larza drugiej wersji programu przedstawia­jącego zachowanie się ciał hipotety­cznego układu planetar­nego. Oprócz tablicy definiu­jącej zestaw 16 wybra­nych kolorów używanych do ryso­wania ciał i ich orbit polami klasy formu­larza są trzy stałe określa­jące odstęp czasowy i parametry bitmapy nieba oraz dwie zmienne zawiera­jące referencje do tej bitmapy i tablicy obiektów (ciał) układu:

private const double DT = 0.1;  // Odstęp czasowy
private const int NW = 800;     // Szerokość bitmapy nieba
private const int NH = 600;     // Wysokość bitmapy nieba
private Bitmap niebo;           // Bitmapa nieba (+ pasek menu)
private Obiekt[] ob = null;     // Tablica obiektów układu

Przechowywanie liczby ciał w polu formularza jest zbyteczne, ponieważ do sekwen­cyjnego przecho­dzenia po elemen­tach kolekcji ob, gdy jest niepusta, wygodniejsza od pętli for jest pętla foreach. Zresztą określa ją właści­wość Length tablicy ob. Oczywiście jeśli referencja ob jest równa null, liczba ciał układu wynosi zero. Tak więc np. nową wersję metody obsługi zdarzenia Paint formu­larza można sformu­łować następująco:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.DrawImage(niebo, 0, 0);
    if (ob != null)
        foreach (Obiekt obiekt in ob)
            obiekt.Rysuj(true, e.Graphics, niebo);
}

Spośród metod klasy Form1 poprzedniego programu, które nie zostały przenie­sione do klasy Obiekt, najwięcej zmian wymaga metoda CzytajDane, której zadaniem jest wczytanie danych ze stru­mienia tekstowego i utwo­rzenie tablicy obiektów reprezen­tujących wszystkie ciała układu plane­tarnego. Oto jej nowa, pełna wersja:

private void CzytajDane(StreamReader plik)
{
    int n = int.Parse(plik.ReadLine().Trim());
    if (n < 2)
        throw new Exception("Wymagane co najmniej dwa ciała układu.");
    ob = new Obiekt[n];
    for (int p = 0; p < n; p++)
    {
        string[] s = plik.ReadLine().Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
        if (s.Length < 6)
            throw new Exception("Liczba danych w wierszu mniejsza od 6.");
        double m = double.Parse(s[0]);
        double x = double.Parse(s[1]);
        double y = double.Parse(s[2]) + menuStrip1.Height;
        double vx = double.Parse(s[3]);
        double vy = double.Parse(s[4]);
        Color kolor;
        if (s[5][0] != '#')
            kolor = Kolor[int.Parse(s[5]) % Kolor.Length];
        else
            kolor = ColorTranslator.FromHtml(s[5]);
        ob[p] = new Obiekt(m, x, y, vx, vy, kolor);
    }
}

Metoda wczytuje liczbę ciał układu do zmiennej lokalnej i przy­dziela pamięć tablicy dynami­cznej ob. Gdyby taka tablica już istniała, mechanizm GC, korzystając z destruktora klasy Obiekt, zwolni pamięć zajętą przez obiekty wskazywane przez elementy tej tablicy i ją samą, chociaż nie wiadomo, kiedy to zrobi. Następnie metoda CzytajDane wczytuje dla każdego ciała sześć parame­trów do zmiennych lokalnych, tworzy reprezen­tujący go obiekt klasy Obiekt i zapamię­tuje referencję do niego w kolejnym elemencie tablicy ob. Metoda kontro­luje liczbę ciał układu oraz liczbę parametrów i wartości każdego z­nich, zgłaszając w razie nieodpo­wiednich danych wyjątek, który jest przechwy­tywany w metodzie obsługi pole­cenia Dane menu. Kod źródłowy C# drugiej wersji programu animacji hipotety­cznego układu planetar­nego zawarty w­pliku Form1.cs może wyglądać nastę­pująco:

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 System.IO;

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

        private Color[] Kolor = {
            Color.Gold,                 //  0 - złoty
            Color.Blue,                 //  1 - niebieski
            Color.Green,                //  2 - zielony
            Color.DarkCyan,             //  3 - ciemny cyan
            Color.Red,                  //  4 - czerwony
            Color.Magenta,              //  5 - magenta
            Color.Chocolate,            //  6 - czekoladowy
            Color.LightGray,            //  7 - jasnoszary
            Color.Khaki,                //  8 - khaki
            Color.CornflowerBlue,       //  9 - chabrowy
            Color.Lime,                 // 10 - jasnozielony (limonka)
            Color.Cyan,                 // 11 - cyan
            Color.Coral,                // 12 - koralowy
            Color.Violet,               // 13 - fioletowy
            Color.Yellow,               // 14 - żółty
            Color.White};               // 15 - biały

        private const double DT = 0.1;  // Odstęp czasowy
        private const int NW = 800;     // Szerokość bitmapy nieba
        private const int NH = 600;     // Wysokość bitmapy nieba
        private Bitmap niebo;           // Bitmapa nieba (+ pasek menu)
        private Obiekt[] ob = null;     // Tablica obiektów układu

        private void Form1_Load(object sender, EventArgs e)
        {
            niebo = new Bitmap(NW, NH + menuStrip1.Height);
            using (Graphics g = Graphics.FromImage(niebo))
                g.Clear(Color.Black);
            ClientSize = new Size(niebo.Width, niebo.Height);
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(niebo, 0, 0);
            if (ob != null)
                foreach (Obiekt obiekt in ob)
                    obiekt.Rysuj(true, e.Graphics, niebo);
        }

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

        private void CzytajDane(StreamReader plik)
        {
            int n = int.Parse(plik.ReadLine().Trim());
            if (n < 2)
                throw new Exception("Wymagane co najmniej dwa ciała układu.");
            ob = new Obiekt[n];
            for (int p = 0; p < n; k++)
            {
                string[] s = plik.ReadLine().Split(
                             (char[])null, StringSplitOptions.RemoveEmptyEntries);
                if (s.Length < 6)
                    throw new Exception("Liczba danych w wierszu mniejsza od 6.");
                double m = double.Parse(s[0]);
                double x = double.Parse(s[1]);
                double y = double.Parse(s[2]) + menuStrip1.Height;
                double vx = double.Parse(s[3]);
                double vy = double.Parse(s[4]);
                Color kolor;
                if (s[5][0] != '#')
                    kolor = Kolor[int.Parse(s[5]) % Kolor.Length];
                else
                    kolor = ColorTranslator.FromHtml(s[5]);
                ob[p] = new Obiekt(m, x, y, vx, vy, kolor);
            }
        }

        private void daneToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                using (Graphics g = Graphics.FromImage(niebo))
                    g.Clear(Color.Black);
                dalejToolStripMenuItem.Enabled = false;
                try
                {
                    using (StreamReader plik = new StreamReader(openFileDialog1.FileName))
                        CzytajDane(plik);
                    Invalidate();
                    daneToolStripMenuItem.Enabled = false;
                    stopToolStripMenuItem.Enabled = true;
                    timer1.Start();
                }
                catch (Exception ex)
                {
                    ob = null;
                    Invalidate();
                    MessageBox.Show(ex.Message, "Błąd", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }

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

        private void dalejToolStripMenuItem_Click(object sender, EventArgs e)
        {
            daneToolStripMenuItem.Enabled = false;
            stopToolStripMenuItem.Enabled = true;
            dalejToolStripMenuItem.Enabled = false;
            timer1.Start();
        }

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

        private void timer1_Tick(object sender, EventArgs e)
        {
            using (Graphics g = CreateGraphics())
                foreach (Obiekt obiekt in ob)
                    obiekt.Przesuń(g, niebo, DT);
            foreach (Obiekt obiekt in ob)
                obiekt.KorygujPrędkość(ob, DT);
        }
    }
}

Na koniec przytoczmy jeszcze jeden przykład hipote­tycznego układu planetar­nego. Przy odrobinie cierpli­wości udało się metodą prób i błędów tak dobrać opisujące go dane, by animacja obrazo­wała obieg księżyca (kolor srebrny) wokół planety (kolor niebie­skiego nieba) krążącej po orbicie wokół gwiazdy podwójnej:

   m       x     y     vx     vy    kolor
  10000   395   300    0,01   20      0
  10000   405   300    0,01  -20      4
  8       470   370   10     -11      5
  25      400   120  -11       0      #87ceeb   SkyBlue
  0,01    400   112  -12,5    -0,05   #c0c0c0   Silver
  10      150   300    0       9,5   10

Nawet niewielkie zmiany danych wprowa­dzanych do programu ujawniają, jak łatwo może dojść do katastrofy w takim układzie plane­tarnym. Na szczęście Układ Słoneczny wydaje się bardziej stabilny. Czy jednak na pewno? Nieraz słyszy się o upadłych meteory­tach i przelatu­jących niebezpie­cznie blisko asteroidach, a badacze dziejów Ziemi twierdzą, że przyczyną wymarcia dinozaurów 65 mln lat temu były zmiany klimatyczne o chara­kterze globalnym spowodo­wane uderzeniem meteorytu o średnicy 10 km w pobliżu półwyspu Jukatan. Szczególnie emocjo­nującym zjawiskiem, które miało miejsce w lipcu 1994 roku, było uderzenie komety Shoemaker-Levy 9 w Jowisza – największą planetę Układu Słone­cznego. Efekty były widoczne nawet przy użyciu amator­skiego teleskopu. Kometa została najpierw uwię­ziona na orbicie wokół Jowisza, potem rozer­wana na ponad 20 części pod wpływem dużych sił grawita­cyjnych, a na koniec wszystkie jej kawałki spadały na powierz­chnię planety (efektowne zdjęcia można znależć w inter­necie).


Opracowanie przykładu: sierpień 2019