Dziś stworzymy prostą gierkę w XNA jaką jest slider kafelków, które tworzą jeden cały obraz. Naszym zadaniem będzie przesuwanie kafelków w taki sposób, aby stworzyły kompletny obraz. Wymagana znajomość XNA (na litość boską, choć trochę.. albo i więcej) Zaczynajmy.
Koncept intersejfu:

Jak więc widać - nasz główny obraz będzie rozmiaru 450x450 px oraz składał się z 25 kafelków (dokładnie 24, jeden bedzię pusty). Jeden kafelek będzie miał 450*450/25 = 90 px.
Przepraszam za błędne dane na rysunku ale zrobiłem to zanim policzyłem dane - lepiej żeby wymiary były całkowite :)
Teraz przejdźmy do konceptu struktury programu.
-Główna klasa programu
-Klasa reprezentująca jeden kafelek obrazu
-Klasa reprezentująca obraz złożony z 25 kafelków klasy powyżej
Ok - stwórzmy więc nowy projekt w VS2010 (oczywiście XNA dla WP 7.1). Nazwa dowolna.

Zajmijmy się teraz bazową klasą reprezentującą pojedynczy kafelek, którą nazwiemy Tile.

Teraz dodajmy następujące pola globalne:
private const int WIDTH = 90; //rozmiar kafelka
private Vector2 _position; //pozycja kafelka względem początku obrazu (wektor)
private Vector2 _positionInImage; //pozycja prostokąta, w którym przechowywany jest oryginalny kawałek obrazka
private Vector2 _newPosition; //nowy pozycja, na którą kafelek ma się przesunąć
public bool _isEmpty = false; //flaga, czy kafelek powinien być pustym
private TimeSpan _framerate; //czas mmiędzy wiświetlaniem kolejnych klatek
Nasz pojedynczy kafelek będzie reprezentować 3 wektory. Wektor _position będzie odpowiadać za pozycję kafelka względem początku obrazka, _positionInImage będzie oznaczać stały fragment lewego górnego prostokąta 90x90 z oryginalnego obrazka (czyli jeszcze nie pomieszanego) a natomiast _newPosition będzie oznaczać wektor, na który nasz kafelek powinien się przesunąć (wygodne przy tworzeniu animacji).
Natomiast _framerate będzie zmienną przechowywującą swojego rodzaju czasomierz, który odlicza czas od upłynięcia poprzedniej klatki (dzięki nie niemu będziemy przemieszczać klocek np. 3 razy na sekundę, a nie 33 (tyle jest klatek na sekundę domyślnie) - co by było za szybko.
Przejdźmy teraz do konstruktora naszej klasy Tile:
public Tile(Vector2 position, Vector2 positionInImage)
{
_position = position; //inicjujemy zmienne
_newPosition = position;
_positionInImage = positionInImage;
_framerate = TimeSpan.FromMilliseconds(0);
}
Powinien on się wydawać dość prosty :)
Teraz kilka przydatnych metod:
public Vector2 GetPositionInImage
//opisujemy którą cześć obrazka kafelek przechowywuje
{
get { return _positionInImage; }
}
public void Move(Vector2 vector)
//ustalamy na którą pozycję kafelek powinien się przesunąć
{
_newPosition = vector;
}
public Vector2 GetTheVector
//zwracmy pozycję kafelka albo pozcyję, na którą dopiero dąży
{
get
{
return _newPosition;
}
}
public Vector2 GetOldTheVector
//zwracamy pozycję kafelka albo pozycję z której zaczął animacje
{
get
{
return _position;
}
}
public bool IsMoving()
//jeśli wektor starego położenia jest różny od tego z nowym położenim, to znaczy że jesteśmy w trakcie animacji
{
return _position != _newPosition;
}
public void SetTheVector(Vector2 vector)
//ustawiamy pozycję kafelka, pomijając animację
{
_position = vector;
_newPosition = _position;
}
public bool Conflict(int x, int y, Vector2 globalVector)
//sprawdza, czy zadane punkty znajdują się w obrębie kafelka. Potrzebne będzie do reagowania na dotyk użytkownika. Współrzędne położenia palca pobierzmy z zewnątrz.
{
//tworzymy prostkokąt w okół zadanej współrzędnej o wymiarach 4x4, gdzie zadany punkt znajduje się dokładnie w jego środku
var collision = new Rectangle((int)_newPosition.X + (int)globalVector.X, (int)_newPosition.Y + (int)globalVector.Y, WIDTH, WIDTH);
//metoda intersects sprawdza, czy dwa prostokąty są w kolizji i zwraca wartość logiczną
return collision.Intersects(new Rectangle(x-2, y-2, 4, 4));
}
public void Draw(SpriteBatch sprite, Texture2D image, Vector2 globalImage)
//rysujemy pojedynczą klatkę
{
if (_isEmpty) return; //jeśli nasz kafelek jest pusty, to opuszczamy procedurę rysowania
//klasa sprite reprezentuje zbiór method do rysowania na ekranie
sprite.Begin(); //rozpocznij proces rysowania
sprite.Draw(image, (globalImage + _position), new Rectangle((int)_positionInImage.X, (int)_positionInImage.Y, WIDTH, WIDTH), Color.White, 0, Vector2.Zero, 1, SpriteEffects.None, 0);
//(globalImage + _position) - wektor położenia całego obrazka dodany do położenia kafelka względem obrazka dam nam położenia kafelka względem całego ekranu
//new Rectangle((int)_positionInImage.X, (int)_positionInImage.Y, WIDTH, WIDTH) - wycinamy odpowiedni kawałek z oryginalnego obrazka o wymiarach 90x90)
public void Update(GameTime gameTime)
//aktualizujemy logikę kafelka
{
_framerate -= gameTime.ElapsedGameTime; //aktualizujemy czas pozostały do następnej klatki
if (_position != _newPosition && _framerate.TotalMilliseconds <= 0) //jeżeli mamy zmienić pozycję i nastąpił czas aktualizji animacji
{
if (_position.X > _newPosition.X) _position.X -= 5; //przesuwamy w dół jeśli trzeba;
if (_position.X < _newPosition.X) _position.X += 5; //przesuwamy w górę jeśli trzeba;
if (_position.Y > _newPosition.Y) _position.Y -= 5; //przesuwamy w lewo jeśli trzeba;
if (_position.Y < _newPosition.Y) _position.Y += 5; //przesuwamy w prawo jeśli trzeba;
_framerate = TimeSpan.FromMilliseconds(25); /odliczamy teraz 25 ms aby narysować nastepną klatkę
}
}
sprite.End(); //konczymy rysowanie
}
Ok - nasza klasa bazowa Tile została ukończona. Teraz przydałoby się stworzyć jakąś klasę, która będzie koordynować nasze 25 kafelków. Nazwiemy ją GameBoard. Jak zwykle zmienne:
private List<Tile> _container; // przechowywujemy tutaj nasze kafelki;
private Texture2D _mainImage; //zmienna przechowywująca naszą teksturę
private Vector2 _emptyVector; //wskaźnik na jeden pusty kafelek - ułatwi nam to nawigowanie
private readonly SpriteBatch _sprite; //menager do rysowania
private readonly Vector2 _globalVector; //główny wektor położenia naszej planszy - czyli głównego obrazka
Kontruktor naszej nowej klasy:
public GameBoard(Texture2D image, SpriteBatch sprite, Vector2 globalVector)
{
_mainImage = image; //aktualizujemy teksturę
_container = new List<Tile>(); //tworzymy nową listę
_sprite = sprite; //oraz przekazujemy menagera.
for (var j = 0; j < 5; j++)
{
for (var i = 0; i < 5; i++) //tworzymy 25 kafelków obok siebie. wektory wskazują na lewy górny róg pojedynczego kafelka. Chwilo obraz nie jest jeszcze "pocięty" więc fragment oryginalnego obrazka ma taki sam wektor położenia w obrazku jak położenie kafelka
{
var item = new Tile(new Vector2(j * 90, i * 90), new Vector2(j * 90, i * 90));
_container.Add(item);
}
}
_emptyVector = new Vector2(0, 0); //nasz pusty kafelek (lewy górny róg)
_container[0]._isEmpty = true;
_globalVector = globalVector; //położenie całego obrazka
}
Teraz zajmijmy się pojedynczymi metodami:
public void SetNewMainImage(Texture2D image)
//zmieniamy obrazek, który jest "pocięty".
{
_mainImage = image;
}
public void Solve()
//ustawia kafelki ustawia tak, aby tworzyły kompletny obrazek, animując je przy okazji
{
foreach (var tile in _container)
{
tile.Move(tile.GetPositionInImage); //przesuwamy każdy kafelek, na tą pozycję, na której położony jest fragment obrazu który przetrzymuje
}
_emptyVector = FindEmpty();
}
private List<E> ShuffleList<E>(List<E> inputList)
//metoda pomocnicza, sortuje listę dowolnego typu
{
var randomList = new List<E>();
var r = new Random();
while (inputList.Count > 0)
{
var randomIndex = r.Next(0, inputList.Count);
randomList.Add(inputList[randomIndex]); //add it to the new, random list
inputList.RemoveAt(randomIndex); //remove to avoid duplicates
}
return randomList; //return the new random list
}
public void ShuffleTiles(bool animationEnabled)
//losuje listę wektorów położenia
{
var vectorList = _container.Select(item => item.GetTheVector).ToList();
//budujemy najpierw listę wektorów poło��enia kafelków za pomocą wyrażenia LINQ
vectorList = ShuffleList(vectorList); //losujemy
for (var i = 0; i < vectorList.Count; i++) if (!animationEnabled) _container[i].SetTheVector(vectorList[i]); else _container[i].Move(vectorList[i]);
//aktualizujemy położenia kafelka (ustawiamy mu nową pozycję wraz z odtworzeniem animacji
_emptyVector = FindEmpty(); //szukamy pozycji pustego kafelka
}
public void Draw()
//rysujemy po kolei każdy kafelek
{
foreach (Tile t in _container) t.Draw(_sprite, _mainImage, _globalVector);
}
private Vector2 FindEmpty()
//szukamy pustego kafelka i zwracamy wektor jego położenia
{
var i = 0;
for (; i < _container.Count; i++)
{
var tile = _container[i];
if (tile._isEmpty) return tile.GetTheVector;
}
return Vector2.Zero;
}
private bool MoveIsAvailable(Tile tile, out Vector2 vector)
//metoda sprawdza czy w sąsiedztwie zadanego kafelka jest puste miejsce (aby przesunąc) i w razie jeśli tak, to zwraca położenie tego pustego miejsca
{
if (((int)_emptyVector.X + 90 == (int)tile.GetTheVector.X && (int)_emptyVector.Y == (int)tile.GetTheVector.Y) |
((int)_emptyVector.X - 90 == (int)tile.GetTheVector.X && (int)_emptyVector.Y == (int)tile.GetTheVector.Y) |
((int)_emptyVector.X == (int)tile.GetTheVector.X && (int)_emptyVector.Y + 90 == (int)tile.GetTheVector.Y) |
((int)_emptyVector.X == (int)tile.GetTheVector.X && (int)_emptyVector.Y - 90 == (int)tile.GetTheVector.Y))
{
vector = _emptyVector;
return true;
}
else
{
vector = Vector2.Zero;
return false;
}
}
public Tile FindEmptyTile()
//zwraca pusty kafelek
{
foreach (var item in _container)
{
if (item._isEmpty) return item;
}
return null;
}
public void Update(GameTime gameTime)
//aktualizuje logikę wszystkich kafelków
{
var touches = TouchPanel.GetState(); //pobierz wszystkie punkty dotyku ekranu przez użytkownika
foreach (var item in _container) //aktualizujemy każdy kafelek osobno
{
if (touches.Count == 1) //jeśli mamy jeden punkt dotyku
{
if (item.Conflict((int)touches[0].Position.X, (int)touches[0].Position.Y, _globalVector)) //jesli kafelek został dodatknięty
{
Vector2 position;
if (MoveIsAvailable(item, out position) && !item.IsMoving())
//sprawdzmy, czy jest pusty kafelek obok
{
_emptyVector = item.GetOldTheVector;
FindEmptyTile().SetTheVector(_emptyVector);
item.Move(position); //przeuswamy kafelek i aktualizujemy położenie pustego
}
}
}
item.Update(gameTime); //aktualizujemy odrębną logikę kafelka (animacje)
}
}
Zostało nam teraz już tylko napisać główny kod programu, który znajdziecie tutaj (sposób jaki jak przechowywuje i aktualizuje dane może być nietrywialne ;P)
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
private GameBoard _gameBoard;
private readonly Vector2 globalVector = new Vector2(15, 800 - 450 - 15);
private int _currentImage = 0; //numer głownego obrazka, może być kilka
private List<Texture2D> _imagesList = new List<Texture2D>(); //lista głównych obrazków
private Texture2D _arrowText; //tekstury przycisków,
private Texture2D _shuffleButtonText;
private Texture2D _bg;
private Texture2D _solve;
private SoundEffect _tapSound; //efekt dźwiękowy musi być, może załapie się jeszcze w GeekClubie :D
//położenia wszystkich głównych elementów na ekranie
private Rectangle _lArrow = new Rectangle(480 - 100 - 15, 15, 100, 100);
private Rectangle _rArrow = new Rectangle(15, 15, 100, 100);
private Rectangle _shuffleButton = new Rectangle(100+15+25, 15, 200, 100);
private Rectangle _solveRect = new Rectangle(15,115, 450, 100);
public Game1()
{
graphics = new GraphicsDeviceManager(this) {PreferredBackBufferWidth = 480, PreferredBackBufferHeight = 800}; //odpowiednie wymiary ekranu wybieramy
graphics.ApplyChanges();
Content.RootDirectory = "Content";
TouchPanel.EnabledGestures = GestureType.Tap;
TargetElapsedTime = TimeSpan.FromTicks(333333);
InactiveSleepTime = TimeSpan.FromSeconds(1);
}
protected override void Initialize()
{
// TODO: Add your initialization logic here
base.Initialize();
}
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
//bezczelnie ładujemy dane z plików
for (int i = 1; i <= 8; i++ )
{
Texture2D item = Content.Load<Texture2D>("images/" + i);
_imagesList.Add(item);
}
_arrowText = Content.Load<Texture2D>("images/larrow");
_shuffleButtonText = Content.Load<Texture2D>("images/shuffle");
_bg = Content.Load<Texture2D>("images/bg");
_solve = Content.Load<Texture2D>("images/solve");
_tapSound = Content.Load<SoundEffect>("tap");
_gameBoard = new GameBoard(_imagesList[_currentImage], spriteBatch, globalVector); //towrzymy sobie plansze
_gameBoard.ShuffleTiles(false); //mieszamy kafelki, bez animacji (bo po co przy starcie?)
}
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
//olać pamięć :D
}
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
_gameBoard.Update(gameTime); //aktualizujemy plansze
while (TouchPanel.IsGestureAvailable) //śmieci - sprawdzmy tylko czy jakiś przycisk nie został "tap"nięty i jak coś obsługujemy zdażenie
{
var gesure = TouchPanel.ReadGesture();
Rectangle touchRect = new Rectangle((int)gesure.Position.X - 2, (int)gesure.Position.Y - 2, 4, 4);
if (touchRect.Intersects(_lArrow) && _currentImage < _imagesList.Count - 1)
{
_currentImage++;
_gameBoard.SetNewMainImage(_imagesList[_currentImage]);
_tapSound.Play();
}
else if (touchRect.Intersects(_rArrow) && _currentImage > 0)
{
_currentImage--;
_gameBoard.SetNewMainImage(_imagesList[_currentImage]);
_tapSound.Play();
}
else if (touchRect.Intersects(_shuffleButton))
{
_gameBoard.ShuffleTiles(true);
_tapSound.Play();
}
else if (touchRect.Intersects(_solveRect))
{
_gameBoard.Solve();
_tapSound.Play();
}
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(); //rysujemy przyciski, tło, plansze.
spriteBatch.Draw(_bg, Vector2.Zero, Color.White);
spriteBatch.Draw(_solve, _solveRect, Color.White);
spriteBatch.Draw(_arrowText, _lArrow, Color.White);
spriteBatch.Draw(_arrowText, _rArrow, null, Color.White, 0, Vector2.Zero, SpriteEffects.FlipHorizontally, 0);
spriteBatch.Draw(_shuffleButtonText, _shuffleButton, Color.White);
spriteBatch.End();
_gameBoard.Draw();
base.Draw(gameTime);
}
}
Uff.. pewnie dużo nierozumiałego kodu, namieszanego, nieoptymalnego.. ale co tam, ważne że działa:
Jeśli filmik z youtube nie działa, to klik.
Wrzucone przeze mnie na marketplace'a, może się przyjmie :)
Powodzenia w kompilowaniu, w razie co zawalać pytaniami 