Projekt Snake

      Możliwość komentowania Projekt Snake została wyłączona


Projekt Snake opisany w tym wpisie dotyczy prostej i znanej wszystkim gry w której to użytkownik poruszając się wężykiem stara się uzbierać jak największą ilość punktów.

Sam projekt skierowany jest do osób średnio zaawansowanych jak i dopiero zaczynających. Wpis ma na celu przedstawienia mojego pomysłu i podejścia do gamedevu – tworzenia gier ale również do zachęcenia was, czytelników do takich projektów. Jeśli więc coś wyda wam się niezrozumiałe lub nie dokładnie opisane zapraszam do komentarzy lub innej formy kontaktu z naszą grupą – postaramy się pomóc tak szybko jak tylko to możliwe.

Sama gra napisana jest w PROCESSINGU natomiast sterowanie odbywa się przy pomocy pilota i ARDUINO. Cały opisany kod dostępny jest na naszym githubie, osoby chętne zachęcam do pobrania i przetestowania. W przypadku błędów/uwag/ pomysłów na modyfikację po raz kolejny zapraszam do komentarzy lub kontaktu z naszą grupą.

Odnośnie technologi i podziału poradniku – poradnik składał się będzie z dwóch części:

  • HARDWARE gdzie postaram się omówić wszystko po stornie arduino w raz z całym sprzętem    (Do pisania kodu pod arduino  korzystam z Visual Studio 2017 oraz dodatku Visual Micro)
  • SOFTWARE gdzie przedstawiony zostanie i omówiony cały kod po stronie komputera                    (Do pisania kodu gry korzystam z Eclipse (PROCESSING oparty jest na javie) z czego wynikać mogą  drobne różnice w porównaniu do PROCESSING IDE)
    • Odbieranie danych i poruszanie (proste)
    • Wychodzenie po za bok ekranu
    • Zbieranie punktów
    • Kolizje
    • Detale (grafik, punkty)
    • Podsumowanie i zapowiedź

Część pierwsza ARDUINO

Potrzebny sprzęt:
– Arduino UNO lub inna płytka
– moduł Odbiornik podczerwieni
– płytka stykowa, kilka kabli
– kable USB do komunikacji komputer <=> arduino

Wprowadzenie do obsługi modułu i pilota: http://learnduino.pl/arduino-remote-control/ 

Schemat połączenia z modułem
GND <-> GND
+5V <-> +5V
PIN 11 <-> INPUT

Wszystkie niezbędne informacje zostały opisane  w poście powyżej. Osoby nie zapoznane z tematem zachęcam do przeczytania.

#include <IRremote.h> // biblioteka 

IRrecv irrecv(11); // pin 11 
decode_results results; 

void setup()
{
	Serial.begin(9600);
	irrecv.enableIRIn();
}

void loop()
{
	if (irrecv.decode(&results))
	{
		switch (results.value) // sprawdzamy jaki przycisk 
		{
		case 0xFF609F: // up 
		{
			Serial.println("UP");
			break;
		}
		case 0xFF22DD: // down 
		{
			Serial.println("DOWN");
			break;
		}
		case 0xFFE21D: // left 
		{
			Serial.println("LEFT");
			break;
		}
		case 0xFF02FD: // right
		{
			Serial.println("RIGHT");
			break;
		}
		case 0xFFE01F: // enter
		{
			Serial.println("ENTER");
			break;
		}
		}

		irrecv.resume();
	}

	delay(100);
}

W skrócie jeśli wciśniemy przycisk (strzałkę do góry na pilocie) wysyłany jest komunikat „UP”, jeśli coś innego np Enter wysyłany jest komunikat „ENTER” itd…

Po stronie arduino to wszystko z mojej strony. Uwaga w tym miejscu ważne jest aby wprowadzić swoje kody przycisków, zanim przejdziemy do dalszej części.

(Jeżeli coś wydało wam się nie zrozumiałe lub nie dokładnie opisane zachęcam do komentarzy i pytań, w przyszłości mam pomysł na kilka podobnych projektów gier, jeśli więc jesteście zainteresowani tym tematem ważne jest aby już na początku zrozumieć jak jak najwięcej).

Część druga Processing

Zanim jednak przejdę do pierwszego pod tematu warto by omówić schemat całej aplikacji i organizację tego projektu. Całość składać się będzie z kilku funkcji z czego każda będzie miała odpowiednie zadanie do wykonania – wszystkie funkcje zostaną opisane i omówione.

package snakeld; // 

import processing.core.PApplet; // 


public class SnakeLD extends PApplet { // 

	public void setup() {
	}

	public void draw() {

	}
	
	public void settings()  { // 
		
	}

	public static void main(String _args[]) {
		PApplet.main(new String[] { SnakeLD.class.getName() }); // 
	} 

}

Fragmentu zaznaczone komentarzami „//” nas nie interesuję, są to elementy wymagane w eclipse które nie są konieczne jeśli korzystamy z processing IDE. Tak naprawdę interesuje nas na  razie zawartość funkcji draw() oraz setup()*

Uwaga w przypadku eclipse wielkość okna definiujemy w funkcji settings () natomiast w przypadku Processing IDE w funkcji setup(). Wielkość okna jest stała zatem wspominam o tym tylko teraz.

(Jeżeli coś wydało wam się nie zrozumiałe lub nie dokładnie opisane zachęcam do komentarzy i pytań, w przyszłości mam pomysł na kilka podobnych projektów gier, jeśli więc jesteście zainteresowani tym tematem ważne jest aby już na początku zrozumieć jak jak najwięcej).

Pod temat pierwszy:
Odbieranie danych i poruszanie (proste)

Przede wszystkim odbieranie danych z Arduino a dokładnie komunikacja z portem COM.
Po raz kolejny nie ma sensu przepisywać coś co już kiedyś opisałem tutaj: http://learnduino.pl/tworzenie-wykresow-z-uzyciem-processingu/

Na początku tworzymy dwie nowe zmienne String .

String com = null, v = "DOWN";

W pierwszej zapisane będę dane odebrane z portu COM druga określa aktualny kierunek naszego węża.

Odbieranie danych odbywa się w funkcji keybord() , funkcja ta ma za zadanie zebrać dane a następnie je porównać. Brzmi skomplikowanie ale w rzeczywistości wszystko jest bardo proste.

com = myPort.readStringUntil(lf).trim();

Ważne aby na końcu dodać .trim(); co pozwoli uniknąć nam białych znaków.

com.equals("UP") 
//objekt_string.equals(objekt_do_porównania);

Funkcja ta służy do porównywania obiektów tekstowych jakimi są zmienne String.
Ważna jest aby nie korzystać z operatorów porównania (operator „==”)który w różnych sytuacjach może zadziałać błędnie.

Drugą funkcją jest funkcja moveLoop() to w niej na podstawie odebranych danych zmieniana będzie pozycja naszego gracza.

void moveLoop() {
		if (v == "UP") {
			position = new PVector(position.x, position.y - 1);
		}
		if (v == "DOWN") {
			position = new PVector(position.x, position.y + 1);

		}
		if (v == "LEFT") {
			position = new PVector(position.x - 1, position.y);

		}
		if (v == "RIGHT") {
			position = new PVector(position.x + 1, position.y);

		}
	}

Sprawdzamy w którą stronę ma się poruszać nasz wąż i zmieniamy jego pozycję.

Dlaczego nie robimy tego od razu w funkcji keybord()? 
Odpowiedź jest bardzo prosta, nasz wąż może się poruszać jeśli nic nie zostało wciśnięte.  

Zmieniamy pozycję naszego wektora (wektor położenia obiektu) czyli zmieniamy pozycję kwadratu. W tym miejscu kończy się na razie poruszanie naszym obiektem, do tematu jeszcze wrócimy przy rozbudowywaniu poruszania o system kolizji.

Trzecią dodaną funkcją jest funkcja draw_stuff(), to właśnie tu opisane będzie wszystko co ma zostać narysowane. Na ten moment ważny jest tylko jeden fragment kodu.

		rect(position.x * 32, position.y * 32, 32, 32);

Mamy kwadrat o wymiarach 32 x 32 którego przesuwamy o:
– 32 * wektor przesunięcia X
– 32 * wektor przesunięcia Y

Początkowa pozycja naszego wektora to (5,5) a wiec pozycja 160, 160 względem sceny renderowania…
– ruch w prawo -> uzyskujemy wektor (6,5) czyli pozycję 192, 160 względem naszego sceny renderowania
– ruch w lewo – > uzyskujemy wektor (4,5) czyli pozycję 128, 160 względem naszego sceny renderowania
– ruch do góry – > uzyskujemy wektor (5,4) czyli pozycję 160, 128 względem naszego sceny renderowania
– ruch na dół- > uzyskujemy wektor (5,6) czyli pozycję 160, 192 względem naszego sceny renderowania
itd…

Aby dokładnie zrozumieć zagadnienie poruszania się polecam ołówek i kartkę w kratkę.

Końcowym etapem jest modyfikacja funkcji draw() oraz setup() tutaj wszystko powinno być raczej proste  i zrozumiał.  Efektem uruchomienia tego kodu będzie kwadrat który bardzo szybko będzie się poruszał w dół.

package snakeld;

import processing.core.PApplet;
import processing.core.PVector;
import processing.serial.Serial;

public class SnakeLD extends PApplet {

	int lf = 10;
	Serial myPort;

	String com = null, v = "DOWN";
	PVector position = new PVector(5, 5);

	public static void main(String _args[]) {
		PApplet.main(new String[] { SnakeLD.class.getName() });
	}

	public void keybord() {

		while (myPort.available() > 0) {

			com = myPort.readStringUntil(lf).trim();

			if (com != null) {

				if (com.equals("UP")) {
					v = "UP";
				}
				if (com.equals("DOWN")) {
					v = "DOWN";
				}
				if (com.equals("LEFT")) {
					v = "LEFT";
				}
				if (com.equals("RIGHT")) {
					v = "RIGHT";
				}
				if (com.equals("ENTER")) {

				}
			}
		}
	}

	void moveLoop() {
		if (v == "UP") {
			position = new PVector(position.x, position.y - 1);
		}
		if (v == "DOWN") {
			position = new PVector(position.x, position.y + 1);

		}
		if (v == "LEFT") {
			position = new PVector(position.x - 1, position.y);

		}
		if (v == "RIGHT") {
			position = new PVector(position.x + 1, position.y);

		}
	}

	public void draw_stuff() {
		noStroke();
		background(255);
		fill(255, 0, 0);
		rect(position.x * 32, position.y * 32, 32, 32);
	}

	public void settings() {
		size(576, 576); 
	}

	public void setup() {
		color(123, 0, 0);
		myPort = new Serial(this, Serial.list()[0], 9600);
		myPort.clear();

	}

	public void draw() {

		keybord();

		moveLoop();
		draw_stuff();

	}
}

(Jeżeli coś wydało wam się nie zrozumiałe lub nie dokładnie opisane zachęcam do komentarzy i pytań, w przyszłości mam pomysł na kilka podobnych projektów gier, jeśli więc jesteście zainteresowani tym tematem ważne jest aby już na początku zrozumieć jak jak najwięcej).

Pod temat drugi:
Wychodzenie po za bok ekranu

void border() {
		if (position.x > 17) {
			position.x = 0;
		}
		if (position.x < 0) {
			position.x = 17;
		}

		if (position.y > 17) {
			position.y = 0;
		}
		if (position.y < 0) {
			position.y = 17;
		}
	}

Opisany fragment będzie bardzo krótki i szybki, dotyczy on interakcji z bokami ekrany.  W nowej funkcji sprawdzamy kolejno czy (wektor pozycji) pozycja X oraz Y  nie wychodzi po za ekran a jeśli tak to przenosimy gracz na drugą stroną (zmieniamy wektor).

Efektem tej modyfikacji będzie zapętlenie ruchu naszej postaci, problemem nadal jednak jest szybkość poruszania się. Dodamy więc kolejną funkcję która pozwoli nam ograniczyć poruszanie się gracza.

Schemat działania oparty jest na millis() o podobnym sposobie pisałem już kiedyś tutaj: http://learnduino.pl/arduino-millis-vs-delay/

Po pierwsze dodamy dwie nowe zmienne typu int – pierwsza oznacz czas między kolejnym ruchem, druga rozpoczyna odliczanie.

int  time_break = 200, start = millis(), lf = 10;

Kolejnym etapem jest dodanie funkcji pause() w funkcji tej sprawdzamy czy czas od aktualizacji odliczania jest większy niż czas między ruchami:
– jeśli tak zwracamy true  i aktualizujemy czas
– jeśli nie zwracamy false i  aktualizujemy czas

boolean pauses() {
		if (millis() - start >= time_break) {
			start = millis();
			return true;
		} else {
			return false;
		}
	}

Po uruchomieniu programy obiekt będzie się poruszał z określoną przez nas szybkością oraz w określonym przez nas kierunku.

 

package snakeld;

import processing.core.PApplet;
import processing.core.PVector;
import processing.serial.Serial;

public class SnakeLD extends PApplet {

	int time_break = 200, start = millis(), lf = 10;
	Serial myPort;

	String com = null, v = "DOWN";
	PVector position = new PVector(5, 5);

	public static void main(String _args[]) {
		PApplet.main(new String[] { SnakeLD.class.getName() });
	}

	void border() {
		if (position.x > 17) {
			position.x = 0;
		}
		if (position.x < 0) {
			position.x = 17;
		}

		if (position.y > 17) {
			position.y = 0;
		}
		if (position.y < 0) {
			position.y = 17;
		}
	}

	boolean pauses() {
		if (millis() - start >= time_break) {
			start = millis();
			return true;
		} else {
			return false;
		}
	}

	public void keybord() {

		while (myPort.available() > 0) {

			com = myPort.readStringUntil(lf).trim();

			if (com != null) {

				if (com.equals("UP")) {
					v = "UP";
				}
				if (com.equals("DOWN")) {
					v = "DOWN";
				}
				if (com.equals("LEFT")) {
					v = "LEFT";
				}
				if (com.equals("RIGHT")) {
					v = "RIGHT";
				}
				if (com.equals("ENTER")) {

				}
			}
		}
	}

	void moveLoop() {
		if (v == "UP") {
			position = new PVector(position.x, position.y - 1);
		}
		if (v == "DOWN") {
			position = new PVector(position.x, position.y + 1);

		}
		if (v == "LEFT") {
			position = new PVector(position.x - 1, position.y);

		}
		if (v == "RIGHT") {
			position = new PVector(position.x + 1, position.y);

		}
	}

	public void draw_stuff() {
		noStroke();
		background(255);
		fill(255, 0, 0);
		rect(position.x * 32, position.y * 32, 32, 32);
	}

	public void settings() {
		size(576, 576); // 576 x 576
	}

	public void setup() {
		color(123, 0, 0);
		myPort = new Serial(this, Serial.list()[0], 9600);
		myPort.clear();

	}

	public void draw() {

		keybord();

		if (pauses()) {
			moveLoop();
		}

		border();

		draw_stuff();

	}
}

(Jeżeli coś wydało wam się nie zrozumiałe lub nie dokładnie opisane zachęcam do komentarzy i pytań, w przyszłości mam pomysł na kilka podobnych projektów gier, jeśli więc jesteście zainteresowani tym tematem ważne jest aby już na początku zrozumieć jak jak najwięcej).

Pod temat trzeci:
Zbieranie punktów

W tym fragmencie postaram się opisać system dzięki któremu możliwe będzie zdobywanie punktów i rozwijanie naszego węża.Nie ukrywam że jest to najbardziej zaawansowana a co za tym idzie jest to najtrudniejszy moment w tym projekcie. Po pierwsze zajmiemy się zbieraniem punktów, konieczne będzie wprowadzenie kilku zmiennych i obiektów.

Pierwszym etapem który musimy omówić jest wektor. Wektory są elementem języków programowania ale nie chodzi tu o te znane z matematyki czy fizyki. Najprościej mówiąc wektor to tablica (zbiór danych) które dowolnie możemy zmieniać w czasie działania programu.

Dla osób chcących dowiedzieć się więcej: https://pl.wikipedia.org/wiki/Tablica_(informatyka)

Zanim przejdziemy dalej przedstawię jeszcze zbiór funkcji z jakich możemy korzystać kiedy posługujemy się wektorem.

ArrayList<PVector> vectors = new ArrayList<PVector>(); // tworzymy nowy wektor 

vectors.add(new PVector(0, 2)); // dodajemy nowa pozycję (wektor pozycji) 

vectors.add(0, new PVector(vectors.get(0).x, vectors.get(0).y + 1)); // dodajemy nowa pozycję (wektor pozycji) w określonym miejscu (zero czyli na początku wektora)

vectors.size(); zwraca ilość elementów w wektorze 

vectors.remove(0); // usuwamy określony element z wektora (zero czyli początek wektora)

vectors.remove(vectors.size() - 1); // usuwamy ostatni element 

vectors.clear(); // czyścimy wektor 

 

Uwaga w Eclipse korzystam z wektora (listy) dostępnej w języku JAVA, jeśli nie korzystasz z tego IDE tylko z PROCESSING IDE powinieneś skorzystać z ArrayList.

 

PVectror position zastąpiliśmy nowym wektorem.

	ArrayList<PVector> vectors = new ArrayList<PVector>();

Po tej zmianie konieczne jest zmian w prawie wszystkich funkcji które do tej pory zostały stworzone. Postaram się teraz pokrótce opisać sens tych zmian.

void border() {
		if (vectors.get(0).x > 17) {
			vectors.get(0).x = 0;
		}
		if (vectors.get(0).x < 0) {
			vectors.get(0).x = 17;
		}

		if (vectors.get(0).y > 17) {
			vectors.get(0).y = 0;
		}
		if (vectors.get(0).y < 0) {
			vectors.get(0).y = 17;
		}
	}

Zmieniony został zapis, ale działanie funkcji takie samo.

vectors.get(0).x
vectors.get(0).y

To pozycje X i Y pierwszego elementu czyli głowy węża.

void moveLoop() {
		if (v == "UP") {
			vectors.add(0, new PVector(vectors.get(0).x, vectors.get(0).y - 1));
			vectors.remove(vectors.size() - 1);
		}
		if (v == "DOWN") {
			vectors.add(0, new PVector(vectors.get(0).x, vectors.get(0).y + 1));
			vectors.remove(vectors.size() - 1);
		}
		if (v == "LEFT") {
			vectors.add(0, new PVector(vectors.get(0).x - 1, vectors.get(0).y));
			vectors.remove(vectors.size() - 1);
		}
		if (v == "RIGHT") {
			vectors.add(0, new PVector(vectors.get(0).x + 1, vectors.get(0).y));
			vectors.remove(vectors.size() - 1);
		}
	}

W tej funkcji zaszło bardzo dużo zmian ale to właśnie dzięki nim możliwe jest od teraz prawidłowe poruszanie naszym wężem.

	vectors.add(0, new PVector(vectors.get(0).x, vectors.get(0).y + 1));
			

Dodajemy nową pozycję na początku wektora, tym samym zmieniając pozycję głowy naszego węża a co za tym idzie zmieniając cała jego pozycję. Sens tej operacji jest bardzo prosty, przesuwamy głową na nową pozycję, pierwszą część ciała na stare miejsce głowy, drugą część ciała na stara miejsce pierwszej części ciała, trzecią część ciała na stara miejsce drugiej części ciała, czwarta na trzecią, piątą na czwartą itd… zależnie ile części ciała ma nasz wąż.

			vectors.remove(vectors.size() - 1);

Na koniec usuwamy ostanie miejsce gdyż nie jest już ono nam potrzebne. Ostatni fragment przesunął się na przed ostatni pozycję. Przedstawiona tutaj operacja zachodzi w kółko dzięki czemu możliwy jest płynny ruch.

Ostatnią zaktualizowaną funkcją jest funkcja draw_stuff, tym razem pojawia się tutaj pętla for w której ustawiany jest kolor (zależnie czy głowa czy część ciała) a następnie dany element jest rysowany.

Uwaga, pętla napisana jest w  taki sposób aby wszystko rysowana było od tyłu, tzn jako pierwszy koniec ogona a głowa jako ostatnia. Nie ma to szczególnego znaczenia ale daje to fajny efekt przy kolizjach, a sama pętla wygląda na super trudną do zrozumienia (ale tylko z pozoru).

void draw_stuff() {
		noStroke();
		background(255);

		for (int i = vectors.size() - 1; i >= 0; i--) {
			if (i == 0) {
				fill(0, 0, 255);
			} else {
				fill(0, 255, 0);
			}

			rect(vectors.get(i).x * 32, vectors.get(i).y * 32, 32, 32);
		}

	}

Ostatnim elementem jest dodania nowych części ciała do naszego węża ( tak aby początek gry odbywał się kiedy wąż ma już jakaś swoją początkową wielkość, a kolejne elementy będzie zdobywał poprzez zbieranie punktów). W tym celu należy uzupełnić nasz wektor kilkoma (trzema) fragmentami, odbywa się to w funkcji setup().

		vectors.add(new PVector(0, 2));
		vectors.add(new PVector(0, 1));
		vectors.add(new PVector(0, 0));

W tym momencie jesteśmy już w stanie poruszać się czymś co przypomina węża, pora więc na najważniejszy element gry czyli zbieranie punktów. Po raz kolejny zaczynamy więc od dodania nowego systemu, w którym zawarte będzie zbieranie punktów oraz zwiększanie wielkości węża.

int time_break = 200, start = millis(), lf = 10, point = 0;

Na początku zaczynamy od dodania nowej zmiennej w które zapisany będzie ilość uzyskanych punktów.

PVector bonus = new PVector(5, 5);

Kolejnym etapem jest dodanie obiektu który będzie reprezentował nasze punkty „jedzonko”.

void getPoint() {
		if (bonus.x == vectors.get(0).x && bonus.y == vectors.get(0).y) { // sprawdzamy czy pozycja głowy jest taka sama jak obiektu "jedzenie"
			point = point + 1; // zwiększamy ilosć punktów 
			bonus = new PVector((int) random(0, 16),(int) random(0, 16)); // losujemy nową pozycję "obiektu jedzenie"

			for (int i = 0; i < vectors.size(); i++) { 
				if (bonus.x == vectors.get(i).x && bonus.y == vectors.get(i).y) {

					bonus = new PVector((int) random(0, 16),(int) random(0, 16));

					i = 0;

				}
			}

			vectors.add(new PVector(vectors.get(vectors.size() - 1).x, vectors
					.get(vectors.size() - 1).y)); // zwiększamy wilkosć węza 
		}
	}

Najważniejszym elementem jest funkcja getPoitn(), w jej wnętrzu sprawdzamy czy gracz ma tą same pozycje co obiekt „jedzenie” a jeśli tak jest zwiększamy jego wielkość i ilość zdobytych punktów, oraz losujemy nowe położenie obiektu „jedzenie” .

	for (int i = 0; i < vectors.size(); i++) {
				if (bonus.x == vectors.get(i).x && bonus.y == vectors.get(i).y) {

					bonus = new PVector((int) random(0, 16),(int) random(0, 16));

					i = 0;

				}
			}

W funkcje tej znajduje się też fragment zabezpieczający aby pozycja obiektu „jedzeni” nie była taka sama jak pozycja którego z fragmentów ciała węża. Sprawdzamy czy pozycja jest taka sama jak ogona węża, jeśli tak losujemy nowa pozycję a sprawdzania zaczynamy od nowa i tka do momentu uzyskania właściwej pozycji. Ostatnim elementem jest edycja funkcji draw_stuff i dodanie nowego obiektu.

package snakeld;

import java.util.ArrayList;

import processing.core.PApplet;
import processing.core.PVector;
import processing.serial.Serial;

public class SnakeLD extends PApplet {

	int time_break = 200, start = millis(), lf = 10, point = 0;
	Serial myPort;

	String com = null, v = "DOWN";
	ArrayList<PVector> vectors = new ArrayList<PVector>();
	PVector bonus = new PVector(5, 5);

	public static void main(String _args[]) {
		PApplet.main(new String[] { SnakeLD.class.getName() });
	}

	void border() {
		if (vectors.get(0).x > 17) {
			vectors.get(0).x = 0;
		}
		if (vectors.get(0).x < 0) {
			vectors.get(0).x = 17;
		}

		if (vectors.get(0).y > 17) {
			vectors.get(0).y = 0;
		}
		if (vectors.get(0).y < 0) {
			vectors.get(0).y = 17;
		}
	}

	boolean pauses() {
		if (millis() - start >= time_break) {
			start = millis();
			return true;
		} else {
			return false;
		}
	}

	void getPoint() {
		if (bonus.x == vectors.get(0).x && bonus.y == vectors.get(0).y) {
			point = point + 1;

			bonus = new PVector((int) random(0, 16), (int) random(0, 16));

			for (int i = 0; i < vectors.size(); i++) {
				if (bonus.x == vectors.get(i).x && bonus.y == vectors.get(i).y) {

					bonus = new PVector((int) random(0, 16),
							(int) random(0, 16));

					i = 0;

				}
			}

			vectors.add(new PVector(vectors.get(vectors.size() - 1).x, vectors
					.get(vectors.size() - 1).y));
		}
	}

	public void keybord() {

		while (myPort.available() > 0) {

			com = myPort.readStringUntil(lf).trim();

			if (com != null) {

				if (com.equals("UP")) {
					v = "UP";
				}
				if (com.equals("DOWN")) {
					v = "DOWN";
				}
				if (com.equals("LEFT")) {
					v = "LEFT";
				}
				if (com.equals("RIGHT")) {
					v = "RIGHT";
				}
				if (com.equals("ENTER")) {

				}
			}
		}
	}

	void moveLoop() {
		if (v == "UP") {
			vectors.add(0, new PVector(vectors.get(0).x, vectors.get(0).y - 1));
			vectors.remove(vectors.size() - 1);
		}
		if (v == "DOWN") {
			vectors.add(0, new PVector(vectors.get(0).x, vectors.get(0).y + 1));
			vectors.remove(vectors.size() - 1);
		}
		if (v == "LEFT") {
			vectors.add(0, new PVector(vectors.get(0).x - 1, vectors.get(0).y));
			vectors.remove(vectors.size() - 1);
		}
		if (v == "RIGHT") {
			vectors.add(0, new PVector(vectors.get(0).x + 1, vectors.get(0).y));
			vectors.remove(vectors.size() - 1);
		}
	}

	void draw_stuff() {
		noStroke();
		background(255);

		for (int i = vectors.size() - 1; i >= 0; i--) {
			if (i == 0) {
				fill(0, 255, 0);
			} else {
				fill(0, 0, 255);
			}

			rect(vectors.get(i).x * 32, vectors.get(i).y * 32, 32, 32);
		}

		fill(255, 0, 0);
		rect(bonus.x * 32, bonus.y * 32, 32, 32);
	}

	public void settings() {
		size(576, 576); // 576 x 576
	}

	public void setup() {
		color(123, 0, 0);
		vectors.add(new PVector(0, 2));
		vectors.add(new PVector(0, 1));
		vectors.add(new PVector(0, 0));
		myPort = new Serial(this, Serial.list()[0], 9600);
		myPort.clear();

	}

	public void draw() {

		keybord();

		if (pauses()) {
			moveLoop();
		}

		getPoint();
		border();

		draw_stuff();

	}
}

(Jeżeli coś wydało wam się nie zrozumiałe lub nie dokładnie opisane zachęcam do komentarzy i pytań, w przyszłości mam pomysł na kilka podobnych projektów gier, jeśli więc jesteście zainteresowani tym tematem ważne jest aby już na początku zrozumieć jak jak najwięcej).

Pod temat czwarty:
Kolizje

Kolizje są ważnym elementem każdej gry, ich działanie jest bardzo proste i bardzo proste do wytłumaczenia. Jest to funkcja lub system mający wykryć kiedy zachodzi jakaś integracja jednego obiektu z innym. W naszym projekcie interesowała nas będzie kolizja węża z samym sobą – czyli moment w którym dojdzie do zderzenia głowy z ogonem.

	boolean moved = false, game = true; 

Na początku dodamy dwie nowe zmienne bool określające status gry (rozgrywka trwa / koniec rozgrywki) oraz druga określająca czy dokonano zmiany położenia (wykonano jakiś ruch).

Tym razem nie będziemy korzystać z nowej funkcji a zajmiemy się edycją już istniejącej.

public void keybord() {

		while (myPort.available() > 0) {

			com = myPort.readStringUntil(lf).trim();

			if (com != null) {

				if (com.equals("UP") && v != "DOWN" && v != "UP" && !moved) {
					v = "UP";
					moved = true;
				}
				if (com.equals("DOWN") && v != "UP" && v != "DOWN" && !moved) {
					v = "DOWN";
					moved = true;
				}
				if (com.equals("LEFT") && v != "RIGHT" && v != "LEFT" && !moved) {
					v = "LEFT";
					moved = true;
				}
				if (com.equals("RIGHT") && v != "LEFT" && v != "RIGHT"
						&& !moved) {
					v = "RIGHT";
					moved = true;
				}
				if (com.equals("ENTER")) { // space bar
			
				}
			}
		}
	}

W tej aktualizacji zlikwidowane zostały czy problemy z do tych czasowym systemem poruszania.
– jeśli poruszamy się w danym kierunku np w lewo nie dokujemy żadnej reakcji jeśli użytkownik po raz kolejny będzie chciał się poruszać w tym kierunku.
– jeśli poruszamy się w danym kierunku np w prawo zablokowana została zmiana kierunku poruszania się w stronę przeciwną w tym wypadku w lewo.
– wyeliminowana został możliwość poruszania się w miejscu, dzięki zmiennej moved, zmiana kierunku poruszania możliwa jest tylko po uprzednim wykonaniu jednego ruchu ( w tym celu edycja obejmuje także funkcje moveLoop()).

void colision() {

		for (int i = 1; i < vectors.size(); i++) {

			if (vectors.get(i).x == vectors.get(0).x
					&& vectors.get(i).y == vectors.get(0).y) {
				game = !game;
			}
		}
	}

Ostatnim elementem jest dodanie funkcji sprawdzającej czy zaszła kolizja z samym sobą a jeśli tak nastąpi zmiana statusy gry czyli koniec rozgrywki.

	if (com.equals("ENTER")) { 
					vectors.clear(); // czyścimy wketor 
					vectors.add(new PVector(0, 2)); // doajmey 3 początkowe części ciała
					vectors.add(new PVector(0, 1));
					vectors.add(new PVector(0, 0));
					v = "DOWN"; // ustawiamy kierunek

					point = 0; // zerujemy ilość punktów
					game = true; // zmiemy status gry ponownie na aktywną rozgrywkę 
			
				}

W tym miejscu warto by także dodać przycisk, restartu dzięki któremu po przegranej (lub w jakimkolwiek dowolnym momencie) zresetujemy grę.

Ostatnim elementem jest aktualizacji pętli głównej gry czyli funkcji draw().

package snakeld;

import java.util.ArrayList;

import processing.core.PApplet;
import processing.core.PVector;
import processing.serial.Serial;

public class SnakeLD extends PApplet {

	int time_break = 200, start = millis(), lf = 10, point = 0;
	Serial myPort;

	String com = null, v = "DOWN";
	ArrayList<PVector> vectors = new ArrayList<PVector>();
	PVector bonus = new PVector(5, 5);
	boolean moved = false, game = true; 

	public static void main(String _args[]) {
		PApplet.main(new String[] { SnakeLD.class.getName() });
	}

	void border() {
		if (vectors.get(0).x > 17) {
			vectors.get(0).x = 0;
		}
		if (vectors.get(0).x < 0) {
			vectors.get(0).x = 17;
		}

		if (vectors.get(0).y > 17) {
			vectors.get(0).y = 0;
		}
		if (vectors.get(0).y < 0) {
			vectors.get(0).y = 17;
		}
	}

	boolean pauses() {
		if (millis() - start >= time_break) {
			start = millis();
			return true;
		} else {
			return false;
		}
	}
	
	void colision() {

		for (int i = 1; i < vectors.size(); i++) {

			if (vectors.get(i).x == vectors.get(0).x
					&& vectors.get(i).y == vectors.get(0).y) {
				game = !game;
			}
		}
	}

	void getPoint() {
		if (bonus.x == vectors.get(0).x && bonus.y == vectors.get(0).y) {
			point = point + 1;

			bonus = new PVector((int) random(0, 16), (int) random(0, 16));

			for (int i = 0; i < vectors.size(); i++) {
				if (bonus.x == vectors.get(i).x && bonus.y == vectors.get(i).y) {

					bonus = new PVector((int) random(0, 16),
							(int) random(0, 16));

					i = 0;

				}
			}

			vectors.add(new PVector(vectors.get(vectors.size() - 1).x, vectors
					.get(vectors.size() - 1).y));
		}
	}

	public void keybord() {

		while (myPort.available() > 0) {

			com = myPort.readStringUntil(lf).trim();

			if (com != null) {

				if (com.equals("UP") && v != "DOWN" && v != "UP" && !moved) {
					v = "UP";
					moved = true;
				}
				if (com.equals("DOWN") && v != "UP" && v != "DOWN" && !moved) {
					v = "DOWN";
					moved = true;
				}
				if (com.equals("LEFT") && v != "RIGHT" && v != "LEFT" && !moved) {
					v = "LEFT";
					moved = true;
				}
				if (com.equals("RIGHT") && v != "LEFT" && v != "RIGHT"
						&& !moved) {
					v = "RIGHT";
					moved = true;
				}
				if (com.equals("ENTER")) { 
					vectors.clear();
					vectors.add(new PVector(0, 2));
					vectors.add(new PVector(0, 1));
					vectors.add(new PVector(0, 0));
					v = "DOWN";

					point = 0;
					game = true;
			
				}
			}
		}
	}

	void moveLoop() {
		if (v == "UP") {
			vectors.add(0, new PVector(vectors.get(0).x, vectors.get(0).y - 1));
			vectors.remove(vectors.size() - 1);
			moved = false;
		}
		if (v == "DOWN") {
			vectors.add(0, new PVector(vectors.get(0).x, vectors.get(0).y + 1));
			vectors.remove(vectors.size() - 1);
			moved = false;
		}
		if (v == "LEFT") {
			vectors.add(0, new PVector(vectors.get(0).x - 1, vectors.get(0).y));
			vectors.remove(vectors.size() - 1);
			moved = false;
		}
		if (v == "RIGHT") {
			vectors.add(0, new PVector(vectors.get(0).x + 1, vectors.get(0).y));
			vectors.remove(vectors.size() - 1);
			moved = false;
		}
	}

	void draw_stuff() {
		noStroke();
		background(255);

		for (int i = vectors.size() - 1; i >= 0; i--) {
			if (i == 0) {
				fill(0, 255, 0);
			} else {
				fill(0, 0, 255);
			}

			rect(vectors.get(i).x * 32, vectors.get(i).y * 32, 32, 32);
		}

		fill(255, 0, 0);
		rect(bonus.x * 32, bonus.y * 32, 32, 32);
	}

	public void settings() {
		size(576, 576); // 576 x 576
	}

	public void setup() {
		color(123, 0, 0);
		vectors.add(new PVector(0, 2));
		vectors.add(new PVector(0, 1));
		vectors.add(new PVector(0, 0));
		myPort = new Serial(this, Serial.list()[0], 9600);
		myPort.clear();

	}

	public void draw() {


		keybord();

		if (game) {
			if (pauses()) {
				moveLoop();
			}

			border();
			getPoint();

			color(0); 
			draw_stuff();

		} else {
		
		}

		myPort.clear();
		delay(100);
	}

}

(Jeżeli coś wydało wam się nie zrozumiałe lub nie dokładnie opisane zachęcam do komentarzy i pytań, w przyszłości mam pomysł na kilka podobnych projektów gier, jeśli więc jesteście zainteresowani tym tematem ważne jest aby już na początku zrozumieć jak jak najwięcej).

Pod temat piąty:
Detale

Nasza gra wygląda już jak prawdziwa gra i właściwie projekt można by zakończyć w tym miejscu. Jest jednak jeszcze kilka możliwości dzięki którym możemy uczynić swoją grę bardziej atrakcyjną co nie będzie miało wpływu na rozgrywkę ale będzie miało wpływ na odbiór produkcji przez użytkownika.

Jednym z ważniejszych elementów gier w dzisiejszym świecie jest grafika, co prawda w naszym projekcie nie  mamy za dużego pola do popisu ale zawsze możemy coś zmienić. Jak wiadomo w naszej grze za grafikę odpowiedzialna jest funkcja draw_stuff a poprzez kilka prostych zmian w jej budowie możemy już uzyskać ciekawy efekt.

oid draw_stuff(int b_r, int b_g, int b_b, int h_r, int h_g, int h_b,
			int t_r, int t_g, int t_b, int p_r, int p_g, int p_b) {
		noStroke();
		background(b_r, b_g, b_b);

		for (int i = vectors.size() - 1; i >= 0; i--) {
			if (i == 0) {
				fill(h_r, h_g, h_b);
			} else {
				fill(t_r, t_g, t_b);
			}

			rect(vectors.get(i).x * 32, vectors.get(i).y * 32, 32, 32);
		}

		fill(p_r, p_g, p_b);
		rect(bonus.x * 32, bonus.y * 32, 32, 32);
	}

Rozbudowaliśmy naszą funkcję o możliwość wprowadzania danych w standardzie RGB dzięki czemu łatwiej będziemy mogli zarządzać kolorami. Zmienioną funkcję możemy teraz zastosować np do zmiany koloru kiedy gracz zakończy rozgrywkę.

Innym detalem jaki warto dodać jest system wyników. Na razie nie musi on być rozbudowany, na początek wystarczy że użytkownik będzie mógł sprawdzić swój rekord w rozgrywce.

Zaczynamy od dodania nowej zmiennej max_score, w której przechowywany będzie aktualny rekord.

	int time_break = 200, start = millis(), lf = 10, point = 0, max_score = 0;

Aktualizację rekordu umieszczamy w funkcji w keybord() przy klawiszy „Enter”. Wynik porównujemy z rekordem jeśli gracz kończy jedną nową rozgrywkę a zaczyna kolejną czyli w momencie wciśnięcia przycisku reset (w naszym przypadku przycisk „enter” na pilocie).

Ostatnim etapem jest wyświetlanie rekordu oraz aktualnego wyniku po zakończonej rozgrywce. Wszystkie te elementy odbywać się będę w funkcji draw(), ale zanim jeszcze do tego przejdziemy konieczne jest ustawienie odpowiedniej czcionki, wielkości i koloru tekstu w funkcji setup().

		textFont(createFont("Arial", 64, true));

Czcionka: Ariel
Wielkość czcionki: 64
Gładkie litery: Tak

public void draw() {

		keybord();

		if (game) {
			if (pauses()) {
				moveLoop();
			}

			colision();

			border();
			getPoint();

			// background r g b
			// head r g b
			// body r g b
			// point r g b

			color(0); // clear
			draw_stuff(204, 255, 153, 204, 102, 0, 0, 153, 0, 153, 0, 0);

			// white
			// blue
			// green
			// red

		} else {
			color(0); // clear
			draw_stuff(229, 204, 255, 153, 204, 204, 229, 255, 255, 255, 255,
					102);

			fill(255, 128, 0);
			textAlign(CENTER, TOP);
			text("Best Score:" + max_score, 288, 200);
			textAlign(CENTER, BOTTOM);
			text("Your Score:" + point, 288, 400);

			// red
			// blue
			// purple
			// blue
		}

		delay(100);
	}
}

Na koniec wszystkie zmiany wprowadzamy do funkcji draw().
Od teraz nasza gra prezentuje się tak:

A tak prezentuje się zakończona rozgrywka:


Na koniec załączam jeszcze link do naszego githuba gdzie dostępny jest pełny kod gry.

[ECLIPSE PROCESSING]
https://github.com/LearnDuino/SnakeLD/blob/master/SnakeLD.java
[Processing IDE]
https://github.com/LearnDuino/SnakeLD/blob/master/SnakeP.pde
[Arduino]
https://github.com/LearnDuino/SnakeLD/blob/master/Snake%20LD.ino