WP XNA 4: Jak zapanować nad teksturami (Texture2D, Vector2, Rectangle)

Gdy tworzyłem swoją pierwszą grę (tzn. przepisywałem z tutoriala) nauczyłem się, że:

  • Texture2D – reprezentuje dowolną grafikę, teksturę
  • Vector2 – punkt na ekranie, np. aktualna pozycja gracza/wroga, początkowa pozycja gracza/wroga, pozycja tektury przycisku, pozycja wyświetlania się wyników
  • Rectangle – przydaje się do wykrywania wszelkich kolizji np.: gracz vs wróg, pocisk vs gracz/wróg, palec gracza vs przycisk

Te 3 klasy + metody Update i Draw to w sumie core każdej gry (gry 2D oczywiście).

Ten wpis jest częścią cyklu o moich przygodach w tworzeniu gier XNA dla Windows Phone.

Niestety pisząc kolejną aplikację/grę, bardziej złożoną, gdzie więcej się dzieje, więcej grafik itd. okazało się, że tworzenie kolejnych pól w głównej klasie przestaje mieć sens. Kod przestaje być czytelny, a modyfikacja czy dopisywanie kolejnych pól i zmiennych z teksturami i wektorami też zaczyna być problematyczne.

Początkowo rozbiłem główną klasę gry na klasy częściowe (partial class) i pogrupowałem odpowiedzialności w różnych plikach. Tak rozdzieliłem części Update, Draw, efekty dźwiękowe, wibracja, input użytkownika itd. Ale dalej to było jedna wielka klasa wszystko-potrafiąca.

Wówczas zacząłem stosować podobne podejście jak z wcześniej opisywanymi klasami Settings i Defaults. Otóż utworzyłem swego rodzaju „repozytorium” – klasy, które w jednym centralnym miejscu trzymają referencje do wszystkich moich tekstur oraz wektorów-pozycji przycisków, napisów itd.

Takie podejście było pomocne, gdy tworzyłem Space Bridge, gdzie zdecydowałem się na dwa widoki podczas rozgrywki. Gracz może grać w normalnym widoku (widok konsoli z przyciskami) oraz w widoku powiększonym, gdzie cały ekran wypełnia gra, a konsola i przyciski są niewidoczne.

 

Musiałem przygotować 2 komplety grafik (normalne i powiększone), a następnie używać ich wymiennie w projekcie, w zależności w jakim widoku był gracz. Utworzenie dwóch klas, które przechowywały oba komplety grafik oraz zawierały informacje, gdzie te tekstury mają być wyświetlone, bardzo ułatwiło mi oprogramowanie całości. Klasy umieszczam w osobnym katalogu i osobnej przestrzeni nazw. Na rysunku można zauważyć klasy takie jak Texture oraz TextureConsoleView i TextureScreenView. Pierwsza klasa zawiera tekstury wspólne dla wszystkich widoków, a następne są już dedykowane pod konkretny widok. Tak samo postępuję z klasami Position, gdzie trzymane są współrzędne dla tekstur.

Kod tych klas jest bardzo prosty:

namespace SpaceBridge.Defines
{
	public static class Texture
	{
		public static Texture2D Pause;
		public static Texture2D Tooltips;
		public static Texture2D GameOver;

		public static void LoadTextures(ContentManager contentManager)
		{
			Pause = contentManager.Load("Graphics/Pause");
			Tooltips = contentManager.Load("Graphics/Tooltips");
			GameOver = contentManager.Load("Graphics/GameOver");
		}
	}
}

Analogicznie wygląda klasa TextureConsoleView, ale ponieważ zawiera sporo właściwości to nie będę jej całej tu wklejał:

namespace SpaceBridge.Defines
{
	public static class TextureConsoleView
	{
		public static Texture2D Background;

		public static class Button
		{
			public static Texture2D APressed;
			public static Texture2D BPressed;
			public static Texture2D CPressed;

			public static Texture2D LeftPressed;
			public static Texture2D RightPressed;
		}

		public static class Bridge
		{
			public static Texture2D Left;
			public static Texture2D Center;
			public static Texture2D Right;
		}

		// ...

		public static void LoadTextures(ContentManager contentManager)
		{
			Background = contentManager.Load("Graphics/ConsoleView/Background_en");

			Bridge.Left = contentManager.Load("Graphics/ConsoleView/Bridge/BridgeLeft");
			Bridge.Center = contentManager.Load("Graphics/ConsoleView/Bridge/BridgeCenter");
			Bridge.Right = contentManager.Load("Graphics/ConsoleView/Bridge/BridgeRight");

			Difficulty.Normal = contentManager.Load("Graphics/ConsoleView/Difficulty/Normal");
			Difficulty.Hard = contentManager.Load("Graphics/ConsoleView/Difficulty/Hard");

			Life.Life1 = contentManager.Load("Graphics/ConsoleView/Lifes/Life1");
			Life.Life2 = contentManager.Load("Graphics/ConsoleView/Lifes/Life2");
			Life.Life3 = contentManager.Load("Graphics/ConsoleView/Lifes/Life3");
			// ...

Widzimy tutaj tekstury dla wciśniętych przycisków (przyciski z normalnym stanem są w teksturze tła, więc nie mam ich jako osobne grafiki), grafiki reprezentujące most w każdym położeniu, cały interface użytkownika itd… Dodatkowo jest tu metoda LoadTextures, którą trzeba wywołać na początku gry, aby załadować wszystkie potrzebne tekstury.

Position jest bardzo podobne. Tutaj znajdziemy sporo właściwości typu Vector2 z współrzędnymi przycisków, napisu ilości punktów, miejsce gdzie wyświetlam ilość straconych żyć, miejsce gdzie poszczególne grafiki astronautów mają być wyświetlane itd. Ponadto konkretnie dla przycisków trzymam tutaj właściwości typu Rectancgle, które przedstawiają obszar przycisków. Dzięki temu łatwo jest później liczyć kolizję palec vs przycisk – i łatwo interpretować, co zostało naciśnięte.

Przykład dla współrzędnych ilości punktów gracza, przycisków oraz ich obszary klikania:

namespace SpaceBridge.Defines
{
	public class PositionConsoleView
	{
		public static class Button
		{
			public static readonly Vector2 Left = new Vector2(41, 317);
			public static readonly Rectangle LeftArea = new Rectangle(17, 286, 125, 125);

			public static readonly Vector2 Right = new Vector2(684, 318);
			public static readonly Rectangle RightArea = new Rectangle(657, 286, 125, 125);

			public static readonly Vector2 DifficultyNormal = new Vector2(675, 53);
			public static readonly Rectangle DifficultyNormalArea = new Rectangle(675, 53, 49, 34);

			public static readonly Vector2 DifficultyHard = new Vector2(675, 108);
			public static readonly Rectangle DifficultyHardArea = new Rectangle(675, 108, 49, 34);

			public static readonly Vector2 Timer = new Vector2(675, 163);
			public static readonly Rectangle TimerArea = new Rectangle(675, 163, 49, 34);
		}
		public static class Font
		{
			public static readonly Vector2 Score = new Vector2(372, 132);
		}
		// ...

Dalsze używanie jest dziecinnie proste i bardzo wygodne. Na przykład, gdy chcemy narysować aktualną ilość punktów gracza:

private void DrawScoreConsoleView(SpriteBatch spriteBatch)
{
	var font = Defines.Font.DigitalSmallFont;
	var position = Defines.PositionConsoleView.Font.Score;
	var text = game.Score.ToString();

	spriteBatch.DrawString(font, text, position, Color.Black);
}

Zachęcam do komentowania. Wszelkie uwagi, pytania, wasze rozwiązania czy znalezione u mnie błędy bardzo mile widziane 🙂

Advertisements

6 thoughts on “WP XNA 4: Jak zapanować nad teksturami (Texture2D, Vector2, Rectangle)

  1. Pingback: WP XNA 0: Wstęp | Wojciech Poniatowski [PL]

  2. Pingback: dotnetomaniak.pl

  3. Microsoft zaleca stosowanie SpriteSheet-ów w celu zwiększenia wydajności , w powyższym zastosowaniu to może nie ma znaczenia ale w klasę Texture mozna by było wpleść odpowiednie zachowania które wyświetlają tylko dany prostokąt ze SpriteSheet, wtedy w słownikowym repozytorium masz tylko definicje wyświetlanych obszarów, od razu ogarnięta podstawową animację i możliwość definiowania obszarów np za pomocą pliku XML oraz zdefiniowane podstawowe BoundingBoxy do kolizji. Co do dwóch kompletów grafik można naprzykład wiekszy komplet, rysować na RenderTarget2D a później skalować do mniejszego widoku. Skalowanie jest na GPU (wydajność) a wszystkie współrzędne i zachowania definiowane tylko raz.
    Bardzo fajny tutorial i początkującym napewno się przyda.

    • co do spritesheetów to nie zapominajmy o procesorach, na creators jest bardzo fajny ContentProcessor który tworzy takiego spritesheeta z kilku obrazków (nikt nie musi w paincie siedzieć i patrzeć jaki obszar dany ludzik np. zajmuje

  4. Dzięki za komentarz. Nie jestem profesjonalnym game-developerem i wasze uwagi na pewno się przydadzą.

  5. Pingback: WP XNA 5: Wygodne zarządzanie dźwiękiem (SoundProvider) | Wojciech Poniatowski [PL]

Możliwość komentowania jest wyłączona.