Cel projektu

Celem tego projektu jest stworzenie programu w C++, który będzie pozwalał użytkownikowi:

  • dodawać do sceny ciała niebieskie, które będą oddziaływać ze sobą grawitacyjnie
  • modyfikować ich parametry, takie jak masa, prędkość, położenie, jasność itd. za pomocą prostego interfejsu
  • pauzować, przyspieszać, cofać, resetować symulację
  • poruszać się kamerą po scenie za pomocą myszki, przybliżać i oddalać widok
  • wykreślać i przewidywać trajektorie obiektów
  • ustalać punkt odniesienia dla trajektorii
  • zablokować widok na jednym obiekcie.

Program ma spełniać również następujące założenia:

  • ruch planet ma być dokładnie obliczany za pomocą algorytmu Rungego-Kutty (opisanego w tym artykule)
  • program ma być zoptymalizowany
  • ruch będzie odbywał się w jednej płaszczyźnie i przedstawiany będzie widok z góry
  • oświetlenie planet, zaćmienia (gdy jedna planeta przechodzi za drugą)
  • planety będą przedstawiane jako koła o różnych kolorach
  • anti-aliasing planet
  • zarządzanie obiektami w kodzie powinno być łatwe i uporządkowane.

Kod źródłowy

Kod źródłowy projektu jest dostępny w repozytorium GitHub.

Wyświetlanie okna - SDL2

Do wyświetlania okna i rysowania na nim w tym projekcie użyłem bibliotekę SDL2. Dobry poradnik, jak ją używać, można znaleźć tutaj.

Inicjalizacja SDL

W pliku main.cpp dodałem bibliotekę SDL2:

#include <SDL2/SDL.h>

a następnie utworzyłem zmienne i obiekty:

// wymiary okna
const int WIDTH = 800;
const int HEIGHT = 600;

// okno
SDL_Window* window = NULL;
// renderer
SDL_Renderer* renderer = NULL;

Obiekt renderer będzie odpowiedzialny za rysowanie pikseli w oknie window. Stworzyłem funkcję inicjalizującą SDL:

// funkcja odpowiadająca za inicjalizację SDL
bool init() {
	bool success = true;
	// inicjalizuj SDL
	if (SDL_Init(SDL_INIT_VIDEO) < 0) {
		printf("Could not init SDL: %s\n", SDL_GetError());
		success = false;
	} else {
		// utwórz okno
		window = SDL_CreateWindow("grav-sim", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WIDTH, HEIGHT, SDL_WINDOW_SHOWN);
		if (window == NULL) {
			printf("Could not create window: %s\n", SDL_GetError());
			success = false;
		} else {
			// stwórz renderer
			renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
			if (renderer == NULL) {
				printf("Could not create renderer: %s\n", SDL_GetError());
				success = false;
			}
		}

	}
	return success;
}

Jeśli inicjalizacja nie przebiegnie pomyślnie, funkcja zwróci false. Potrzebna jest również funkcja “sprzątająca” po wszystkim:

// funkcja odpowiadająca za "posprzątanie" po wszystkim
void close() {
	// zniszcz okno, zwolnij miejsce w pamięci, zamknij SDL
	SDL_DestroyWindow(window);
	window = NULL;
	SDL_Quit();
}

Rejestrowanie wejścia od użytkownika

W tym celu utworzyłem obiekt SDL_Event, który będzie wykrywał zdarzenia, takie jak wciśnięcie klawiatury, myszki itd. oraz funkcję, która na podstawie wykrytego zdarzenia wypisze informację do konsoli:

bool running = true;
SDL_Event event;

void handleEvents() {
    while(SDL_PollEvent(&event)) { // funkcja sprawdzająca zdarzenia
        switch(event.type) { // typ zdarzenia
            case SDL_KEYDOWN:
                printf("Key press\n"); // wciśnięcie klawisza na klawiaturze
                break;
            case SDL_KEYUP:
                printf("Key up\n"); // puszczenie klawisza na klawiaturze
                break;
            case SDL_QUIT:
                running = false; // zamknięcie okna
                printf("Quitting\n");
                break;
            default:
                break;
        }
    }
}

Zmienna running mówi, czy program jest uruchomiony. Przyda się ona w jednym z kolejnych kroków.

Rysowanie

Utworzyłem funkcję, która będzie odpowiedzialna za rysowanie na ekranie. Dodałem w niej również rysowanie białego prostokąta:

void draw() {
	SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); // ustawiamy kolor tła (RGBA)
	SDL_RenderClear(renderer); // zapełniamy tło

	SDL_Rect rect; // tworzymy nowy obiekt typu prostokąt
	rect.x = 100; // ustalamy jego położenie i rozmiary
	rect.y = 100;
	rect.w = 100;
	rect.h = 100;

	SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF); // ustawiamy kolor prostokąta
	SDL_RenderFillRect(renderer, &rect); // rysujemy wypełniony prostokąt

	SDL_RenderPresent(renderer); // wyświetlamy obraz na ekranie
}

Pętla programu

Utworzyłem funkcję zawierającą główną pętlę programu:

void loop() {
	while (running) {
		handleEvents();
		draw();
	}
}

Na razie kod jest uruchamiany w pętli while, czyli jak najszybciej jest to możliwe. Jest to oczywiście nieopłacalne i niestabilne. Zostanie to naprawione później.

Tak wygląda moja funkcja main():

int main(int argc, char* argv[]) {
	if (!init()) {
		return 1;
	}
	loop();
	close();
	return 0;
}

Co dalej?

Powyższy program wyświetla okno, a w nim biały prostokąt na czerwonym tle. W następnej części opiszę proces rysowania kół i okręgów za pomocą algorytmu midpoint oraz to, jak dodałem planety.