[C++] Problem z tworzeniem minimapy w grze konsolowej

Witam.

Tworzę strategiczną grę konsolową, z grafiką ze znaków. Pomyślałem, żeby zrobić minimapę. (co jest must have’em w takich grach, moim zdaniem) 

Mechanizm jej tworzenia wygląda tak, że mapa jest dzielona na tyle fragmentów, ile znaków ma minimapa, później jest zliczana ilość lądu i wody w tych fragmentach, i zależnie od tego, czego jest więcej, znak na minimapie jest niebieski lub zielony.

Rzecz w tym, że minimapa absolutnie nie wygląda tak, jak powinna. Oprócz tego znaki są przesunięte, ale to samo w sobie nie powoduje takiego wyglądu minimapy.

Prawdopodobnie zepsułem coś w dzieleniu mapy na fragmenty, możliwe, że z rozmiarem tych fragmentów, albo czymś innym, jednak próbowałem dużo rzeczy z tym robić i nic mi to nie daje.

Testuję to wszystko w osobnym projekcie, używam środowiska CodeBlocks.

 

Tutaj podam wszystkie pliki projektu, w main.cpp zaznaczyłem, gdzie jest tworzona minimapa, oraz gdzie jest wyświetlana.

(plików txt z belkami nie daję, bo dużo ich, a to nie jest teraz w ogóle istotne)

 

main.cpp

klasy.h

klasy.cpp

 

Komentarze w kodzie pomogłyby je zrozumieć. Tobie także, jeśli zerkniesz w te pliki za kilka tygodni.

Kod jest słaby. Uruchomiłem go i zacząłem debugować (powinieneś samemu to robić :P) ale co kawałek napotykam masę magii, często błędnej. :frowning: Przykładowo:

 

struct Belka_pion
{
char belka[260][9];
char kolor[260][9];
};
/*...*/
bool czytaj_pionowa_belke(Belka_pion & belka, char* plikz, char* plikzk)
{
/*...*/
if (plik.good() == true)
{
  for (int i = 0; i<261; i++)
  {
    for (int j = 0; j<9; j++)
    {
      plik >> belka.belka[i][j];
    }
  }
  belki = true;
}

Wykraczasz poza rozmiar belki indeksując [260][8].

W ogóle aplikacja po uruchomieniu ma access violation i się wysypuje (przypuszczam, że brak plików txt z zasobami nie pomaga ;]). Kod jest pełen magicznych liczb i prosi się o refaktoryzację. Kilka wskazówek:

  1. Masz ograniczoną liczbę różnych pól. Zbuduj listę możliwości i indeksuj mapę tą listą. Mapa 260x260 ze znakiem, kolorem, klimatem i terenem to marnotrawstwo pamięci i czasu.

  2. Wszelkie graniczne wartości - wymiary, liczniki, itp. - powinny być w jednym miejscu. Najprościej jest użyć makr i konsekwentnie stosować je. Dzięki temu takie “260” zmieniasz w jednym miejscu, a nie w dziesięciu.

  3. W miejscach, w których robisz dziwną magię, a później odwołujesz się do tablicy[1] sprawdzaj granice albo przynajmniej używaj asercji. Po dodaniu asercji w generowaniu mapy raz na pięć uruchomień widzę, że zapisujesz poza zaalokowaną pamięcią.

  4. Staraj się nie używać w kodzie znaków spoza ASCII w postaci “czystej” - eskejpuj je.

  5. Używasz sporo wątpliwej jakości losowości[2][3].

  6. Dziel kod na mniejsze fragmenty.

  7. Nie używaj stringów w miejscach, gdzie enumy sprawdzą się znakomicie.[4] Pomoże Ci to też w zamianie ifów na switcha.[5]

  8. Jeśli już piszesz pod Windows, staraj się nie mieszać conio z konsolowymi funkcjami Windowsa. Staraj się też nie mieszać stdio z iostream.

  9. W ogóle to nie rozumiem dlaczego pisząc pod Windows nie używasz VS. :stuck_out_tongue: Jest przecież całkowicie darmowe.

[1] Na przykład:

if ((((j<3) || (j>s1 - 3)) || ((k<3) || (k>s2 - 3))) && ((mapa[y + j - 3][x + k - 3].kolor == 'b') || (mapa[y + j + 3][x + k + 3].kolor == 'b')))
/*...*/
mapa[y + j][x + k].znak = 'U';

[2] Dlaczego modulo 259?

y = ((rand() % 259) + 1);

[3] Funkcje srand/random dają marnej jakości losowość. Używanie czasu do zasilania srand niewiele w tym pomaga. Użyj czegoś porządnego, albo nie babraj się w ogóle z seedowaniem, bo szkoda na to czasu. Szczególnie, że na etapie rozwoju kodu czasem warto mieć przewidywalne rezultaty, z przewidywalnego seeda. :wink:

srand(time(NULL));

[4]Przykładowo:

if (fore == "black") tekst = 0;

[5]N.p.:

if (typ == "belka") nkolor = kolor;
else if (kolor == 'b') nkolor = 'n';
else if (kolor == 'g') nkolor = 'l';
else if (kolor == 'w') nkolor = 'h';
else if (kolor == 'r') nkolor = 'm';
else if (kolor == 'y') nkolor = 'd';
else if (kolor == 's') nkolor = 'c';
else if (kolor == 'p') nkolor = 'v';
else if (kolor == 'a') nkolor = 'a';

Jeśli chodzi o debugowanie, to mam nikłą, a właściwie żadną wiedzę z tego zakresu, więc logicznie mogę tego zrobić.

Ten rozmiar belki, to nie wiem, czemu w pętli było mniejsze od 261, nie pojawiał mi się żaden błąd i wszystko działało, więc tego nie zauważyłem.

Co do tej listy możliwości to nie do końca rozumiem, o co ci chodzi.

Nie wiem, co masz na myśli mówiąc dziwna magia, natomiast o czymś takim jak asercja to nigdy nie słyszałem…

Co do eskejpowania znaków, to znajdź mi sekwencję dla chociażby znaku “Ű”, będę bardzo wdzięczny.

Jeśli mowa o losowaniu, to faktycznie z góry ustalony rezultat może się przydać, natomiast nie rozumiem, co masz do biednego sranda…

Z dzieleniem kodu, to też nie rozumiem, o co ci chodzi.

Stringów używam i używać będę, ponieważ 1 to nie kolor, “blue” to jest kolor.

A switcha można zrobić ze stringami, zrobiłem tam ify, bo… nie wiem, bardziej elegancko wyglądają. 

Co do mieszania bibliotek… używam tego, co znam. Nic na to nie poradzę, że znajduje się w różnych bibliotekach.

Nie używam VS (czyli, jak mniemam, środowiska od Microsoftu, którego pełnej nazwy zapomniałem) bo czemu nie CodeBlocks?

 

Generalnie dostałem odpowiedź na chyba wszystko oprócz tego, o co się pytałem.

Twój kod jest nieczytelny i nieudokumentowany - ciężko jest go prześledzić nie wiedząc co ma robić i znaleźć powód, dla którego nie działa. Przykładowo czy magiczna wartość 260 w belce ma jakiś związek z rozmiarem mapy, czy nie? Dlaczego podany wyżej przykład [1] ma taki, a nie inny kod? Być może Ty wiesz co miałeś na myśli - ja nie. Masz dwie opcje: albo opiszesz co i jak ten kod ma robić (bo że robi rzeczy dziwne, to widać, ale chyba nie o to chodziło), albo zdebugujesz go samemu.

Co do pomniejszych pytań: znaków spoza zakresu 0…127 nie powinieneś używać w ogóle w połączeniu z funkcjami, które przyjmują chary. Pod windowsem do reszty zakresu powinieneś używać funkcji akceptujących wchar_t. U z dwoma umlautami (bo przypuszczam, że o to chodzi) ma kod 220/0xDC.

Jeśli będziesz się opierał dobrym praktykom programistycznym, to zaszkodzisz bardziej sobie, niż komukolwiek innemu. :slight_smile: Może i 1 to nie kolor, ale BLUE już tak. Z kolei “blue” to nie kolor, to ciąg znaków. Programujesz, a nie piszesz referat - upieranie się, że 1 to nie kolor jest mało praktyczne. W kodzie wartości całkowite mają wiele znaczeń, zależnie od kontekstu. Na tym między innymi polega programowanie: na określaniu kontekstu danych, na których sie operuje. Może Ci się wydawać, że GOKOP to Ty, ale dla kodu forum i strony jesteś numerem 192563.

Ze srandem masz jeden podstawowy problem: rzutownie typu implicite. Staraj się tego nie robić.

To, że przy kompilacji nie pojawia Ci się błąd nie znaczy wcale, że program jest poprawny. Nawet jeśli się dobrze wykona u Ciebie 100 razy, nie znaczy wcale, że jest poprawny. Poprawność kodu określa się na nieco innym poziomie. Twój kod jest zdecydowanie niepoprawny - nie tylko przy iterowaniu pól belki, ale i przy tworzeniu mapy (doczytaj co to asercje, przyda Ci się, bo uzupełniony o nie kod szybko wykrył przekraczanie zakresu przy dodawaniu różnych elementów mapy).

Na temat podstaw debugowania polecam :arrow: https://www.youtube.com/playlist?list=PL5D59D2682ED5B2EA

Możesz też pocztać :arrow: http://gynvael.coldwind.pl/?id=238 (konkretnie część 4.)

 

Gdybyś, zamiast w kilkunastu miejscach wpisywać tą samą wartość bezpośrednio, używał nazwanych stałych to łatwiej by Ci było unikać takich błędów.

Stałe możesz tworzyć np. za pomocą dyrektyw preprocesora

#define SIZE 260

wtedy każde wystąpienie SIZE w kodzie będzie zastępowane przez 260.

Albo, za pomocą modyfikatora const

const int SIZE = 260;

Takie stałe, pod względem językowym, to po prostu zmienne, ale których nie możesz modyfikować. (właściwie nawet trochę lepsze niż zwykłe zmienne, bo tych stałych możesz używać też kontekstach, w których wartość wyrażenia musi być znana w czasie kompilacji).

Zaletą używania nazwanych stałych jest to, że gdy będziesz chciał zmienić jej wartość, to zmieniasz tylko w jednym miejscu, a nie w niewiadomo-ilu ryzykując, że coś przegapisz.

Możesz też mieć kilka stałych o takich samych wartościach ale różnym znaczeniu i nie ryzykować, że Ci się pomylą (po czasie można zapomnieć, że np. 42 w jednym miejscu oznaczało co innego niż 42 w innym).

Inny rodzaj stałych to tzw. wartości wyliczeniowe, np.

enum Color
{
	Black,
	Red,
	Green,
	Blue,
	White
};

Możesz teraz deklarować zmienne wyliczeniowe typu Color.

Color tlo = Blue;

Tego typu zmienna może przyjąć wartość ze zbioru { Black, Red, Green, Blue, White }. Kompilator zapewni, że tym opisowym wartościom będą odpowiadały różne liczby (w praktyce kolejne licząc od 0).

Możesz też samemu przyporządkować wartości liczbowe jakie uważasz za odpowiednie

enum Color
{
	Black = 0,
	Red = 4,
	Green = 2,
	Blue = 1,
	White = 7
};

:arrow: http://pl.wikipedia.org/wiki/Asercja_(informatyka)

W skrócie polega to na sprawdzaniu czy w danym miejscu kodu są spełnione określone warunki. Jeśli warunki nie są spełnione, oznacza to, że wystąpił błąd.

W C++ do wstawiania asercji możesz użyć makra assert z nagłówka <cassert> (lub <assert.h>).

Podstawowe pytanie: Jakie kodowanie znaków?

Do kodowania plików tekstowych na Windowsie w wersji polskiej standardowo używana jest strona kodowa 1250, natomiast w konsoli używana domyślnie jest strona kodowa 852, ponieważ taka był używana na DOSie i jest używana w konsoli ze względu na kompatybilność z DOSowymi programami (co prawda na 64bitowych Windowsach już nie można uruchamiać programów na DOSa, ale za to trzeba zachować kompatybilność z Windowsowymi programami konsolowymi, które były pisane z wzięciem pod uwagę, że konsola stosuje DOSOwą stronę kodową – ach te demony przeszłości :slight_smile: )

Znak Ű (unicode 0170) kodowany w CP1250 ma kod xDB , natomiast kodowany w CP852 ma kod xEB.

Więc jeśli plik z kodem źródłowym masz kodowany w CP1250 a konsola używa CP852 to będzie wyświetlać co innego niż masz w kodzie.

Dlatego najlepiej używać unicodu i nie babrać się ze stronami kodowymi :wink:

 

Wątpię, żeby komuś się chciało dokładnie analizować ten kod (a przynajmniej nie za darmo). Za to dostałeś wiele cennych wskazówek, które możesz wykorzystać przy następnym projekcie :wink:

Nie ma większego znaczenia jaka strona kodowa - zamiast babrać się w takie archaiczne rozważania, lepiej użyć MBCS i mieć sprawę z głowy. Dlatego napisałem “jeśli chodzi o U z umlautem”, bo nie mam w ogóle pewności, że baza forum ma odpowiednie locale i to co widzę zgadza się z intencją autora. :wink:

Hę? Ale są różne kodowania MBCS, głównie dla języków azjatyckich, którym nie starcza 8 bitów na znak ( natomiast na znaki polskie wystarczy 8 bitów i to jeszcze na spółkę z kilkoma innymi językami :wink: ).

Jeśli za dużo się myśli o stronach kodowych i na już myśl przychodzą MBCS to najlepiej użyć którejś transformacji Unicode’u (który ma aspiracje obejmować wszystkie znaki używane przez ludzkość) do kodowania wszystkich tekstów.

Tylko jeszcze kwestia, którą UTF wybrać :wink:

Chyba najczęściej używana jest UTF-8, pod względem technicznym możnaby zaliczyć do MBCS (chociaż nie do końca, bo nie jest zbiór (Set) znaków, tylko sposób kodowania znaków ze zbioru Unicode, który sam nie definiuje kodowania), zwykle daje się przepychać przez systemy projektowane z myślą o ASCII (w przypadku bardziej upierdliwych systemów można zastosować UTF-7). Windowsową konsolę też da się zmusić do użycia UTF-8, tylko trzeba pilnować, żeby wielobajtowe znaki były wypisywane w całości; jeśli w jednym wywołaniu wyślemy kawałek sekwencji, a w kolejnym resztę, to zamiast jednego znaku, który chcieliśmy, mogą pojawić się dwa znaki zastępcze.

Natomiast jeśli już programujemy konkretnie na Windows to lepiej używać UTF-16, bo jest to kodowanie używane wewnętrznie przez system. W przeciwieństwie do UTF-8, w którym znaki mogą mieć długość od 1 do 6 (a właściwie 4) bajtów, w UTF-16 zazwyczaj każdy znak zajmuje równo 2 bajty (no dobra jest cos takiego jak Surrogate Pairs, ale większość praktycznie użytecznych znaków raczej mieści się na planie podstawowym).

@GOKOP Na temat Unicode’u polecam ten artykuł http://web.archive.org/web/20111012012958/http://warsztat.gd/articles.php?x=view&id=270 (jakby nie chciał się otworzyć, to wersja spakowana http://chomikuj.pl/Rolek__/Dokumenty/Unicode+w+Visual+Cpp,4454081681.zip(archive)) (co prawda ma w tytule „Visual”, ale sporo tekstu jest o samych kodowaniach oraz o używaniu Unicode’u (a konkretnie UTF-16) w WinAPI).

Nie zamierzam gonić za zmieniającym się celem dyskusji. Mówimy o Windowsie, o aplikacji konsolowej, o jakimś prostym znaku diakrytycznym. Nie o hanzi, hangulu, katakanie, hiraganie, ani setkach innych alfabetów. W tym kontekście, w przypadku tego co autor chce napisać, użycie MBCS (czy bardziej: DBCS) jest wystarczającym rozwiązaniem, dużo bardziej “wodoszczelnym” niż stosowanie ASCII 128-255 z użyciem printf().

Windows nie używa wewnętrznie UTF-16 (a przynajmniej znaczna część nie), tylko arbitralnej, dwubajtowej reprezentacji. Niektóre nowsze komponenty wewnętrznie operują na UTF-8. Ale to wszystko bez znaczenia, bo nie o tym była mowa. :slight_smile:

Wiesz co oznacza MBCS? Jest to ogólny typ stron kodowych , w których znaki mogą zajmować więcej niż jeden bajt. (W dokumentacji Microsoftu, czasem mianem MBCS określa się wszystko inne niż Unicode)

I tak trzeba wybrać jakąś stronę kodową, a czy będzie ona typu SBCS (jednobajtowa), DBCS (dwubajtowa) czy MBCS (wielobajtowa) to zależy ile znaków obejmuje.

 

Więc po co MBCS? :wink:

 

Który można znaleźć na stronach kodowych typu SBCS.

A jak chcemy mieć znaki z wielu różnych języków (niekoniecznie azjatyckich, wszystkie znaki używane w Europie już nie zmieszczą się naraz na jednej SBCS) to zamiast szukać jakiejś dziwnej MBCS, lepiej po prostu użyć Unicode’u.

 

ASCII nie definiuje znaków o kodach 128-255. Co się wyświetli zależy od strony kodowej. :wink:

Ciężki z Ciebie przypadek. :slight_smile: Od początku doskonale wiesz co mam na myśli (np. że pisząc MBCS i wskazując na wchar_t miałem na myśli to, co uzyskasz na niemalże każdym komputerze, z którego nie korzysta jakiś Azjata korzystając z DBCS, jeśli zależy Ci na polskich znakach, a chcesz się uniezależnić w wystarczającym stopniu od strony kodowej), ale czepiasz się uogólnień… w sumie nie wiadomo po co. Może dla frajdy, może dla popisu - ciężko stwierdzić. Ignorujesz kontekst tego tematu i wytrwale ciśniesz w każdym kierunku, w którym możesz coś doszczegółowić. Gdyby miało to wartość dla autora tematu, to może bym się w to dalej bawił. ;] Ale tak nie jest. Gdyby z kolei faktycznie zależało Ci na usystematyzowaniu zagadnienia, napisałbyś bloga na ten temat. Ale najwyraźniej na tym Ci też nie zależy. Dlatego kończę tę jałową dyskusję. Spodziewam się jakiejś ciekawej szpileczki na koniec, ale nie zamierzam na nią odpowiadać. Więc niech chociaż będzie dobra, ok? :wink:

No właśnie nie.

Podejrzewałem, że niewłaściwie posługujesz się terminem MBCS, a teraz właściwie mam już pewność.

Chciałem tylko uświadomić, że MBCS != Unicode, ale jeśli chcesz trwać w błędzie to twoja sprawa :wink:

 

Takie informacje można znaleźć na Wikipedii oraz MSDN, ale trzeba wiedzieć czego szukać i mieć świadomość, że istnieją różne sposoby kodowania znaków.

 

Nie uważam tego za sprawę osobistą i szpilek nie zamierzam Ci wbijać.

Widocznie mamy różne podejście do dzielenia się wiedzą :wink:

Wesołych Świąt :slight_smile: