Gra 2D, część 11: Edytor poziomów cz. 1

15.03.2011 - Marcin Milewski
TrudnośćTrudność

Tworzenie poziomów przez ręczną edycję plików? Tak było do tej pory. W tym artykule zajmiemy się tworzeniem wbudowanego w grę edytora. Jedną z jego zalet będzie tryb "edytuj i graj" łączący rozgrywkę z możliwością edycji poziomu.

Poprzedni artykuł - Sprawmy by była piękna Następny artykuł - Edytor poziomów cz. 2

Pobierz początkowy kod źródłowy do tego artykułu.

Poniżej znajduje się film przedstawiający pierwszą wersję implementacji wbudowanego edytora poziomów. Ponieważ w tym artykule będziemy zajmowali się głównie kodem edytora, a nie jego graficzną reprezentacją, animacja prezentuje kilka podstawowych możliwości -- przesuwanie planszy w poziomie oraz dodawanie klocka.

Zaczynamy!

Nasz cel: edytor poziomów, który będzie integralną częścią gry. Prawda, że brzmi nieźle? Obecny sposób definiowania poziomów, czyli wpisywanie liczb w pliku tekstowym, nie jest najlepszy. Ręcznie edytując plik z danymi poziomu możemy popełnić wiele błędów. Wpiszemy na przykład za mało liczb w wierszu i poziom będzie zawierał dziwne przesunięcie. Po co nam te kłopoty?

Stwórzmy edytor, który sam zadba o poprawny zapis danych do pliku - maszyna zrobi to zdecydowanie sprawniej niż najbardziej doświadczony człowiek-edytor plików tekstowych. Poza tym, graficzny edytor nie wymaga wiele komentarza w kwestii obsługi. Klikanie jest zdecydowanie bardziej intuicyjne niż wpisywanie tajemniczych liczb.

W tym artykule stworzymy podstawową wersję edytora. Rozszerzymy istniejące już klasy o nowe funkcjonalności, takie jak zapis danych poziomu do pliku czy dynamiczna zmiana jego rozmiaru. Zaimplementujemy takie klasy jak Editor (zarządzająca całą pracą edytora) czy Brush (oznaczającą pędzel, którym można dodawać różne elementy do tworzonego poziomu).

Interesującą częścią tego artykułu będzie niewątpliwie zaprezentowanie mechanizmu "edytuj i graj". Pozwoli on na natychmiastowe przetestowanie utworzonego poziomu w praktyce. Prawda, że ciekawe? Naszą przygodę zaczniemy jednak od bardziej podstawowych mechanizmów. Na początku przedstawmy klasę, która będzie pomocna przy obliczeniach matematycznych opartych na wektorach.

Nowy typ danych - wektor

Pierwsza pomocnicza klasa, która jest nam potrzeba, dostarcza implementacji wektora w sensie matematycznym. Jest to struktura opakowująca współrzędne x oraz y. Dla naszych potrzeb wystarczy przestrzeń dwuwymiarowa. Przy implementacji posłużymy się biblioteką boost::operators (nagłówek boost/operators.hpp), która pozwala zredukować liczbę wierszy koniecznych do umieszczenia w kodzie do minimum. Jak to możliwe? Każdy kto implementował operatory takie jak +, +=, - czy -= na pewno zauważył, że ich kod wygląda niemal identycznie. Tu właśnie wkracza biblioteka z boosta, która wymaga od nas implementacji tylko operatorów +=, -=, ... a reszty "domyśli" się sama. Zobaczmy jak to wygląda:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Plik Vector2.h
#ifndef __VECTOR2_H_INCLUDED__
#define __VECTOR2_H_INCLUDED__
 
struct Vector2
    : boost::addable< Vector2               // Vector2 + Vector2
    , boost::subtractable< Vector2          // Vector2 - Vector2
    , boost::dividable2< Vector2, double    // Vector2 / double
    , boost::multipliable2< Vector2, double // Vector2 * double, double * Vector2
      > > > >
{
    static const Vector2 ZERO;
    Vector2(double x, double y) : x(x), y(y) {}
    double  operator[](int idx) const { return (idx==0?x:y); }
    double& operator[](int idx)       { return (idx==0?x:y); }
    double X() const                  { return x; }
    double Y() const                  { return y; }
    void operator+=(const Vector2& other) { x+=other.x; y+=other.y; }
    void operator-=(const Vector2& other) { x-=other.x; y-=other.y; }
    void operator*=(double scalar)        { x*=scalar; y*=scalar; }
    void operator/=(double scalar)        { x/=scalar; y/=scalar; }
    double Length() const                 { return std::sqrt(x*x+y*y); }
private:
    double x, y;
};
 
#endif // __VECTOR2_H_INCLUDED__
  

Klasy bazowe dobieramy w zależności od tego, jakie operacje chcemy wykonywać. W naszym przypadku jest do dodawania i odejmowanie dwóch wektorów, a także mnożenie i dzielenie przez skalar. I tu okazuje się, że można jeszcze lepiej. Operacje dodawania i odejmowania oraz mnożenia i dzielenia stanowią pewne grupy, dlatego wystarczy napisać:

1
2
3
4
5
6
struct Vector2
    : boost::additive<Vector2, boost::multiplicative2<Vector2, double> >
{
    (...)
};
  

Pomocniczą stałą statyczną ZERO definiujemy w pliku Vector.cpp. Jest to wektor zerowy, który czasem może się przydać.

1
2
3
4
5
#include "StdAfx.h"
#include "Vector2.h"
 
const Vector2 Vector2::ZERO = Vector2(0, 0);
  

Mając wektor definiujemy pomocnicze typy pozycji oraz rozmiaru:

1
2
3
4
5
6
// Plik: BasicMathTypes.h
#include "Vector2.h"
 
typedef Vector2 Size;
typedef Vector2 Position;
  

Mając takie typy, usprawnijmy klasy Sprite oraz Renderer o możliwość ich wykorzystania. Implementacja sprowadza się do odpowiedniego przetłumaczenia dodanych typów na już istniejące. Kompilator powinien takie wywołania rozwinąć, więc nie powstaje żaden narzut na korzystanie z takiej konstrukcji.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Plik: Sprite.cpp (+ deklaracja w Sprite.h)
void Sprite::DrawCurrentFrame(Position position, Size size) const {
    DrawCurrentFrame(position.X(), position.Y(), size.X(), size.Y());
}
 
// Plik: Renderer.cpp (+ deklaracja w Renderer.h)
void Renderer::DrawQuad(Position min_position, Position max_position,
                        double r, double g, double b, double a) const {
    DrawQuad(min_position.X(), min_position.Y(),
             max_position.X(), max_position.Y(),
             r, g, b, a);
}
  
0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com