Zrób to sam : Gry zręcznościowe na Androida


Wprowadzenie do świata gier Androida
Witamy. Nauczysz się jak tworzyć gry zręcznościowe na Androida "od projektu do publikacji". Po zakończeniu lektury ,będziesz miał wiedzę potrzebną do stworzenia zabawnej i atrakcyjnej gry na Androida. Zaletą jest to że gry można będzie uruchamiać zarówno na telefonach jaki i tabletach z systemem Android To niezwykle satysfakcjonujące, usiąść i zagrać w grę własnoręcznie napisaną. Jest to szczególnie trafne w odniesieniu do gier zręcznościowych, które idealnie nadają się do telefonu.
Co powinieneś wiedzieć
Powinieneś posiadać praktyczną wiedzę na temat środowiska Android. Oznacza to ,że powinieneś być dobrze zorientowany w Javie i Android SDK, próbując swych sił w budowie projektów i aplikacji na Androida. Powinieneś być również zapoznany z IDE Eclipse. W naszym przypadku jest to Eclipse Indigo. O ile to możliwe do debuggowania kodu użyj emulatora , pobierz urządzenie Android jeśli poważnie myślisz o projektowaniu gier. I na końcu , powinieneś mieć przynajmniej podstawową wiedzę na temat programowania gier. A oznacza to w praktyce ,że powinieneś mieć podstawową wiedzę na temat OpenGL ES i jak używać go w grach dla Androida
Czego się nauczysz
Nauczysz się jak używając wiedzy o środowisku programistycznym Android i OpenGL ES tworzyć ciekawe gry. Stworzysz grę, która wypływa z konwencji stylu zręcznościowego. Gra Prison Break zawiera wiele elementów bardziej skomplikowanych gier. Na końcu nauczysz się kluczowych umiejętności programowania gier zręcznościowych na Androida. Poniżej znajduje się lista (w przypadkowej kolejności) niektórych umiejętności, jakich będziesz nabierał w miarę czytania tekstu :
- Wyświetlanie i manipulowani grafiką z Open GL
- Praca z zasobami ludzkimi takimi jak bitmapy
- Tworzenie i zabijanie wątków w Androidzie
- Tworzenie ekranu powitalnego, systemu menu i silnika gry
Programowanie gier na Androida
Projektowanie gier na Androida ma swoje plusy i minusy. Dlatego powinieneś być ostrożny nim rozpoczniesz. Po pierwsze, gry na Androida są tworzone w Javie, ale nie jest to pełna Java. Wiele z pakietów które mogą być używane z OpenGL jest zawartych w Android SDK. Wiele ,ale nie wszystkie, a pakiety które są bardzo przydatne da projektantów gier, np. 3D , nie są zawarte. Tak więc nie wszystkie pakiety przydatne przy budowie gry będą dostępne w Androidzie. Z każdym nowym wydaniem Android SDK , powiększa się ich liczba,. Musisz być świadomy z jakich pakietów musisz korzystać. Inną wadą Androida jest to , że to co zyskuje na łatwości programowania traci na szybkości i mocy. Większość gier wideo, jak te napisane dla PC lub konsol, jest stworzonych w językach niskopoziomowych takich ja C lub nawet Asembler. Daje to programistom większą kontrolę nad tym jak kod jest wykonywany przez procesor i środowisko w jakim jest uruchamiany. My nie będziemy się zagłębiać w niskopoziomowe projektowanie gry. Dlaczego? Ponieważ Java jest szeroko znana,, łatwa w użyciu i można tworzyć bardzo ciekawe i satysfakcjonujące gry. W istocie, jeśli jesteś doświadczonym programistą Javy, możesz zauważyć, że twoje umiejętności wcale nie giną kiedy stosujesz je do Androida. Jeśli nie masz doświadczenia w javie, nie martw się . Java jest świetnym językiem aby zacząć się uczyć. Dlatego my będziemy pisać nasze gry w Javie.

Co to jest gra zręcznościowa?
Nauczysz się gry w stylu gier zręcznościowych. Sformujesz również operacyjną definicję stylu gier zręcznościowych. Dowiesz się również więcej o Prison Break, grze którą wspólnie stworzymy
Nasza gra : Prison Break
Nauczysz się jak stworzyć grę nazwaną Prison Break . Jest to gra, która polega na odchyleniu trajektorii piłki w stronę ściany z cegieł , aby ją przebić. Gra jest luźno oparta na grze Brekout z Atari. Prison Break zawiera wszystkie elementy niezbędna dla zbudowania dobrej bazy wiedzy dla projektowania gier. Nauczysz się o wielokątach, renderowaniu tekstury, podstawowej fizyce gier i wykrywaniu zderzeń. Są to pojęcia, z których z pewnością skorzystasz w innych grach. Przeprowadzimy cię prze projektowanie Prison Break w naturalnym porządku, do początku do końca. Otrzymasz próbki kodu i wyjaśnienie dla tworzenia i odtwarzania gry na urządzeniu z Androidem. Poniższy rysunek pokaże ci Twoją , skończoną grę : Prison Break. Da ci to jasny obraz tego co nas czeka…
Tworzenie menu
Stworzymy dwuczęściowe menu ekranowe dla naszej gry. Menu ekranowe jest stworzone z dwóch różnych "ekranów" zawierających pięć różnych obrazów
Zanim zaczniemy
Zanim przejdziemy do kodu, są dwie rzeczy jakie trzeba przygotować. Pierwsza ,to stworzenie nowego projektu Androida nazwanego prisonbreak. Ten projekt będzie przechowywał cały kod i obrazy używane przez nas. Po drugie, utwórz obrazy dla naszej gry. Potrzebujemy ich pięć : ekran powitalny, dwa różne stany przycisku Start, i dwa różne stany przycisku Exit : Ekran powitalny, Start 1, Start 2, Exit 1, Exit 2. Ważną rzeczą jest aby nazwy obrazów były pisane z małej litery. Jest tak, ponieważ w innym przypadku, dużych liter lub notacji wielbłąda Androif nie będzie umiał rozpoznać obrazków. Te pięć obrazków powinno się znaleźć w folderze res/drawable-hdpi ponieważ będą odczytywnae przy użyciu metod Android SDK
Tworzenie ekranu powitalnego o menu głównego
Menu główne Prison Break składa się z ekranu powitalnego , które zamienia się w menu ekranowe. Menu ekranowe składa się z przycisków Start i Exit. W grze, ekran powitalny i tło menu głównego będą takimi samymi obrazkami. Daje to efekt przycisków na blaknący tle.
PrisonbreakActivity
Pierwszym naszym plikiem jest PrisonBreakActivity. Jest to główny plik , jaki jest tworzony dla projektu. Oto jego kod:
package com.jfdimarzio;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.WindowManager;
public class PrisonbreakActivity extends Activity {
/** Wywoływane kiedy najpierw jest tworzona aktywność. */
@Override
public void onCreate(Bundle savedInstanceState) {
PBGameVars.display = ((WindowManager) getSystemService(Context.WINDOW
SERVICE)).getDefaultDisplay();
super.onCreate(savedInstanceState);
/*display the splash screen image*/
setContentView(R.layout.splashscreen);
/*start ekranu powitalnego i menu głównego w wątku czasu opóźnienia */
PBGameVars.context = this;
new Handler().postDelayed(new Thread() {
@Override
public void run() {
Intent mainMenu = new Intent(PrisonbreakActivity.this,
PBMainMenu.class);
PrisonbreakActivity.this.startActivity(mainMenu);
PrisonbreakActivity.this.finish();
overridePendingTransition(R.layout.fadein,R.layout.fadeout);
}
}, PBGameVars.GAME THREAD DELAY);
}
}
Patrząc na ten kod, można zauważyć ,że odnosi się do kilku innych plików. Pierwszym jest wielokrotny, współdzielącym zmienne w klasie, nazwany PBGamesVars.java. Tworzy nową klasę w programie o takiej nazwie i dodaje do niej zmienne
public static Display display;
public static Context context;
public static final int GAME THREAD DELAY = 3000;
public static final int MENU BUTTON ALPHA = 0;
public static final boolean HAPTIC BUTTON FEEDBACK = true;
Następnie PrisonbreakActivity odnosi się do układu umieszczopnego pod res/layout/splashscreen. Poniższy listing pokazuje zawartość splashscreen.xml:
< ?xml version = "1.0" encoding = "utf-8" ? >
< FrameLayout >
xmlns:android = " http: //schemas.android.com /apk /res /android"
android:layout width = "match parent"
android:layout height = "match parent">
< ImageView android:id = "@ + id/splashScreenImage"
android:src = "@drawable/prisonbreaksplash"
android:layout width = "match parent"
android:layout height = "match parent">
< /ImageView >
< TextView
android:text = "game by: j.f.dimarzio graphics by: ben eagel"
android:id = "@ + id/creditsText"
android:layout gravity = "center horizontal|bottom"
android:layout height = "wrap content"
android:layout width = "wrap content">
< /TextView >
< /FrameLayout >
Kolejny plik wywoływany przez PrisonBreakActivity to PBMainMenu.java. Najpierw jednak spójrzmy na więcej rozkładu użytego w PrisonBreal.java. Spójrz na overridePendingTrasiton() wywoływane a głównej aktywności. Wywołanie występuje dla dwóch układów przejściowych. Pierwszy definiuje rozjaśniony ekran powitalny , a drugi definiuje przyciemnienie menu ekranowego. Poniższe listingi zawierają kod , odpowiednio fadein.xml i fadeout.xml.Choć na pierwszy rzut oka wyglądają tak samo ,ale są pewne kluczowe różnice. Fundamentalna różnica jest taka, że przy rozjaśnieniu jest używany interpolator akcelracji , podczas gdy przy ściemnianiu jest używany interpolator zwalniania.
fadein.xml
< ?xml version = "1.0" encoding = "utf-8" ? >
< alpha xmlns:android = "http: //schemas.android.com /apk /res /android"
android:interpolator = "@android:anim/accelerate interpolator"
android:fromAlpha = "0.0"
android:toAlpha = "1.0"
android:duration = "1000" />
fadeout.xml
< ?xml version = "1.0" encoding = "utf-8" ? >
< alpha xmlns:android = "http: //schemas.android.com /apk /res /android"
android:interpolator = "@android:anim/decelerate interpolator"
android:fromAlpha = "1.0"
android:toAlpha = "0.0"
android:duration = "1000" />
PriosnbreakActivity wyświetla obrazek ekranu powitalnego a potem zmienia obraz w wywołaniu PBMAinMenu.java. Spójrzmy więc co to jest PBMainMenu.java
PBMainMenu
PBMainMenu steruje uruchamianiem i zamykaniem głównej pętli gry; dlatego puszą być wyświetlone dwa przyciski : Start i Exit. Poniższy listing pokazuje bieżący kod PBMainMenu.java (późnije nieco do niego dodamy). Zauważ ,że PBMainMenu jest nową aktywnością i musi być zdefiniowany jako taki w AndroidManifest dla twojego projektu
public static Display display;
public static Context context;
public static final int GAME THREAD DELAY = 3000;
package com.jfdimarzio;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
public class PBMainMenu extends Activity {
/** Wywoływane kiedy aktywność jest tworzona po raz pierwszy */
final PBGameVars engine = new PBGameVars();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
PBGameVars.context = getApplicationContext();
/** Ustawienie opcji menu przycisku */
ImageButton start = (ImageButton)findViewById(R.id.btnStart);
ImageButton exit = (ImageButton)findViewById(R.id.btnExit);
start.getBackground().setAlpha(PBGameVars.MENU BUTTON ALPHA);
start.setHapticFeedbackEnabled(PBGameVars.HAPTIC BUTTON FEEDBACK);
exit.getBackground().setAlpha(PBGameVars.MENU BUTTON ALPHA);
exit.setHapticFeedbackEnabled(PBGameVars.HAPTIC BUTTON FEEDBACK);
exit.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
int pid = android.os.Process.myPid();
android.os.Process.killProcess(pid);
}
});
}
}
Podobnie jak PrisonbreakActivity, PBMainMenu również używa trzech różnych plików rozkładu. Pierwszym, jest main.xml. Układ ten definiuje ekran menu głównego, który widzi gracz. Zawiera przyciski Start i Exit:
< ?xml version = "1.0" encoding = "utf-8" ? >
android:orientation = "vertical"
android:layout width = "match parent"
android:layout height = "match parent"
>
< ImageView android:id = "@ + id/mainMenuImage" android:src = "@drawable/prisonbreaksplash"
android:layout width = "match parent"
android:layout height = "match parent" >
< /ImageView >
< RelativeLayout
android:id = "@ + id/buttons"
android:layout width = "match parent"
android:layout height = "wrap content"
android:orientation = "horizontal"
android:layout alignParentBottom = "true"
android:layout marginBottom = "20dp ">
< ImageButton
android:id = "@ + id/btnStart"
android:clickable = "true"
android:layout alignParentLeft = "true"
android:layout width = "wrap content"
android:src = "@drawable/startselector"
android:layout height = "wrap content" >
< /ImageButton >
< ImageButton
android:id = "@ + id/btnExit"
android:layout width = "wrap content"
android:src = "@drawable/exitselector"
android:layout height = "wrap content"
android:layout alignParentRight = "true"
android:clickable = "true" >
< /ImageButton >
< /RelativeLayout >
< /RelativeLayout >
PBMainMenu wywołuje również dwo pliki układu znane jako selektory. Pliki te definiują co się wydarzy kiedy gracz wybierze przycisk Start lub Exit. Dla naszej gry chcemy aby widoczne przyciski Start i Exit były zamieniane, jak gdyby gracz naciskał je swoim palcami. Układy selektorów obsługują wymianę tych obrazków. Poniższy listing pokazuje zawartość selektora wyjścia i selektora uruchamiania, odpowiednio:
exitselector.xml
< ?xml version = "1.0" encoding = "utf-8" ? >
< selector
xmlns:android = http: //schemas.android.com /apk /re s/android >
< item android:state pressed = "true" android:drawable = "@drawable/
exitbtndown" / >
< item android:drawable = "@drawable/exitbtn" / >
< /selector>
startselector.xml
< ?xml version = "1.0" encoding = "utf-8"? >
< selector
xmlns:android = http: //schemas.android.com /apk /res /android >
< item android:state pressed = "true" android:drawable = "@drawable /
startbtndown" / >
< item android:drawable = "@drawable/startbtn" / >
< / selector >
Powinieneś teraz móc skompilować i uruchomć kod. Kiedy to zrobisz, zobaczysz ekran powitalny, który przechodzi w menu. Dotknięcie przycisku Exit zabije główną aktywność i wyjdziesz z gry. Jednakże dotknięcie przycisku Start nie wywoła niczego
Rysowanie tła
Stworzyliśmy i sfinalizowali menu główne do gry. Możesz skompilować i uruchomić kod na emulatorze Androida lub telefonie z Androidem w trybie debuggowania, i zobaczyć funkcjonalny ekran menu głównego. Przycisk Exit pozwala na zabicie proces gry. Jednak przycisk Start nie zawiera żadnego kodu. Czas napisać kod dla tego przycisku i tła dla gry. Dla stworzenia tła użyjemy wywołania OpenGL ES. Poprzednio używaliśmy metod Android SDK dla wyświetlenia grafik, takich jak menu ekranowe czy przyciski . Teraz popracujemy w OpneGL ES. Zaczniemy od napisania kodu , który jest aktywowany przez przycisk Start w menu głównym
Uruchomienie gry
Przycisk Start, położony w menu głównym, jest używany przez gracza do uruchomienia gry. Kiedy uruchamia grę, uruchamiana jest nowa Android Activity, która kontroluje wszystkie funkcje gry. Dlaczego przy uruchamianiu gry konieczna jest nowa aktywność? Jest tak, aby projektant gry miał większą elastyczność w kontrolowaniu sposobu w jaki gra jest wykonywana. Jeśli chcesz dodać do menu głównego nowe funkcje, które nie są powiązane z grą - na przykład, konfigurator - jest to dobry sposób na ochronę przed rozrostem kodu. Poniższy listing pokazuje kod PBMainMenu, jaki zaczęliśmy wcześniej. Pogrubiony kod został dodany dla uruchomienia PBGame Activity. Dodaja ten kod do swojego PBMainMenu. Następnie stworzymy PBGame Activity
PBMainMenu.java
package com.jfdimarzio;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
public class PBMainMenu extends Activity {
/** Wywoływane kiedy aktywnośćjest tworzona po raz pierwszy*/
final PBGameVars engine = new PBGameVars();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
PBGameVars.context = getApplicationContext();
/**Ustawienie opcji przycisku menu */
ImageButton start = (ImageButton)findViewById(R.id.btnStart);
ImageButton exit = (ImageButton)findViewById(R.id.btnExit);
start.getBackground().setAlpha(PBGameVars.MENU BUTTON ALPHA);
start.setHapticFeedbackEnabled(PBGameVars.HAPTIC BUTTON FEEDBACK);
exit.getBackground().setAlpha(PBGameVars.MENU BUTTON ALPHA);
exit.setHapticFeedbackEnabled(PBGameVars.HAPTIC BUTTON FEEDBACK);
start.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
/** Start Gry!!!! */
Intent game = new Intent(getApplicationContext(),PBGame.class);
PBMainMenu.this.startActivity(game);
}
});
exit.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
int pid= android.os.Process.myPid();
android.os.Process.killProcess(pid);
}
});
}
}
Zwróć uwagę ,że kiedy klikniesz przycisk Start, kod powie PBMainMenuu aby uruchomił PBGame Activity. Nie musisz mieć PBGame jeszcze. Stwórzmy go więc. Stworzymy nową aktywność nazwaną PBGame w projekcie Prison Break. PBGame jest wyjątkowo prostym kodem.PBGame ma zamiar usatwić zawartość widoku aktywności dla renderowania gry i sterowania zdarzeniami onPause i onResume. Pamiętaj jednak ,że kiedy mówimy o onPause i onResume, nie są one funkcjami gry raczej metodami Androida, które są wywoływane kiedy Android pauzuje lub wznawia aktywność. Kod nowej aktywności PBGame powinna wyglądać jak ten:
PBGame.java
package com.jfdimarzio;
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
public class PBGame extends Activity {
final PBGameVars gameEngine = new PBGameVars();
private PBGameView gameView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gameView = new PBGameView(this);
setContentView(gameView);
}
@Override
protected void onResume() {
super.onResume();
gameView.onResume();
}
@Override
protected void onPause() {
super.onPause();
gameView.onPause();
}
}
Zauważ ,że metoda onCreate () ustawia zawartość widoku aktywności dla nowej instancji PBgameView. PBGameView jest nową klasą , która rozszerza GLSurfacceView. W kolejnej części wprowadzimy GLSurfaceView jeśli stworzyłeś PBGameView
Tworzenie SurfaceView i Renderer W tej sekcji stworzysz SurfaceView i Renderer dla gry. PBGameView jest prostą klasą , która rozszerza się do GLSurfaceView OpenGL . Jeśli niegdy nie projektowałeś w OpenGL ES, pomyśl o GLSurfaceView jako płótnie na którym OpenGL rysuje grę. GLSurfaceView jest tym co Android wyświetla na ekranie. Nie może jednak działać sam. GLSurfaceView potrzebuje odpowiedniego GLSurfaceView Renderer dla renderowania gry na powierzchni .Zaczynając od GLSurfaceView, stworzymy nową klasę nazwaną PBGameView i rozszerza GLSurfaceView jak pokazano to tutaj:
PBGameView.java
package com.jfdimarzio;
import android.content.Context;
import android.opengl.GLSurfaceView;
public class PBGameView extends GLSurfaceView {
private PBGameRenderer renderer;
public PBGameView(Context context) {
super(context);
renderer = new PBGameRenderer();
this.setRenderer(renderer);
}
}
Jest to raczej mała klasa. Zobaczysz ,że cel jedynego konstruktora w klasie jest stowrzenie instancji renderowania (PBGameRenderee). Stwórzmy nową klasę nazwaną PBGameRenderer w projekcie Prison break. Ta klasa jest potrzebna do rozszerzenia GLSurfaceView.Renderer jak poniżej :
PBGameRenderer.java
package com.jfdimarzio;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class PBGameRenderer implements Renderer{
private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;
@Override
public void onDrawFrame(GL10 gl) {
loopStart = System.currentTimeMillis();
try {
if (loopRunTime < PBGameVars.GAME THREAD FPS SLEEP){
Thread.sleep(PBGameVars.GAME THREAD FPS SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
gl.glClear(GL10.GL COLOR BUFFER BIT | GL10.GL DEPTH BUFFER BIT);
loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width,height);
gl.glMatrixMode(GL10.GL PROJECTION);
gl.glLoadIdentity();
gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
// TODO Auto-generated method stub
gl.glEnable(GL10.GL TEXTURE 2D);
gl.glClearDepthf(1.0f);
gl.glEnable(GL10.GL DEPTH TEST);
gl.glDepthFunc(GL10.GL LEQUAL);
}
}
Renderer ma trzy metody które można przesłonić : onSurfaceCreated(), onSurfaceChanged() i onDrawFrame. Jako projekant, nie będziesz wywoływał żadnej z tych metod w kodzie, GLSurfaceView jest odpowiedzialny za wywołanie wszystkich tych metod we właściwym czasie. Pokrótce, onSurfaceCreated() działa jako konstruktor dla renderera i jest wywoływana kiedy renderer jest tworzony. Jeśli wszystko jest uruchomione gładko, powinna być wywoływana tylko raz; więc całe ustawienie kodu jest skierowane do tej metody. Jedyny kod , który wywołujemy w podprogramie ustawień jest funkcja OpenGL, ustawiająca teksturę i głębokość bufora. Metoda onSurfaceChanged () jest wywoływana kiedy zmienia się powierzchnia. Nie myl tego z onDrawFrame(). Rysowanie ramki nie stanowi o zmianie powierzchni. Metoda onSurfaceChanged() jest również wywoływana za pierwszym razem kiedy renderer jest wywoływany, po konfiguracji. W onSurfaceChanged (), chcemy ustawić widok portu gry i wywołać potokowanie renderowania OpenGL dla rysowania obiektów. Widok portu gry jest obszarem świata gier, który jest rysowany na ekranie. Myśl o widoku portu jako wyświetlaczu na kamerze. Kiedy ustawisz kamerę w określonym kierunku, widzisz tylko małą część całego świata. Tak więc kiedy tworzysz grę używając OpenGL : możemy stworzyć więcej "obiektów" w swoim świecie, które możesz zobaczyć w danym czasie. Widok portu mówi OpenGL co oczekuje zobaczyć po zrenderowaniu na wyświetlaczu. Uważaj, kiedy używasz zmiennych width i height , które są przekazywane do onSurfaceChanged(). Kiedy GLSurfaceView wywołuje onSurfaceChanged(), width i height są przekazywane niekoniecznie z prawdziwą wysokością i szerokością ekranu. Aby pobrać pełną szerokość i wysokość ekrany użyj context.display.width i context.display.height. Będziesz chciał wstawić cały ten kod , który jest wywoływany dla każdej klatki w onDrawFrame(). Obejmuje obliczania szybkości klatki, kod dla rysowania wszystkich obiektów w grze, wykrywanie zderzeń i czyszczenie. Jedyny kod jaki jest uruchamiane w każdej klatce w powyższym listingu to główny wątek dla szybkości klatki, jak również metoda OpenGL dla wyczysczenia bufora. W kolejnej części stworzymy klasę, która rysuje tło; a potem wywołamy tą klasę z onDrawFrame() i narysujemy tło na ekranie
Tworzenie klasy tła
W Prison Break, mamy zamiar stworzyć klasę, która obsłuży ustawienia indeksów, wierzchołków i tekstur używanyc do narysowania tła dla gry. Każdy nowe element , który dodajemy do tej gry występuje z formatem tej klasy. Obraz tła , jakiego używasz powinien być skopiowany do folderu res/drawable-nodpi w projekcie i nazwany bg1.png. Dodanie poniższej zmiennej do twojego PBGameVars pomoże ci odnieść się do późniejszego obrazu
public static final int BACKGROUND = R.drawable.bg1
Moze to być np. taki obrazek : Przykładowy obrazek tła. Teraz czas ustawić kalse dla stworzenia tła. Tworzymy nową klasę w projekcie Prison Break nazwaną PBBackground.java. Potrzebujemy trzech metod w tej klasie : konstruktora, metody draw () i metody loadTexture(). Konstruktor ładuje wierzchołek, indeks i tablicę tekstur do buforó1). Ponieważ jest to najważniejszy krok w określani jak wyglądać będzie tło kiedy zostanie wyrenderowane na ekranie, zajmijmy się przez chwilę omówienie co to są tablice i jak są używane. Tablica wierzchołka jest używana do definiowania rogów wielokąta , do którego odwzorowane zostanie tło . Rogi są definiowane przez uycie ich osi x,y i z w kartezjańskim układzie współrzędnych. Dlatego, tworząc kwadrat, dostarcza współrzędnych x,y i z dolnego lewego rogu, górnego lewego rogu, prawego górnego rogu i dolnego prawego rogu, odpowiednio. Na końcu, wielokąt jaki narysowaliśmy jest kwadratem (lub prostokątem), OpenGL w rzeczywistości rysuje trójkąty prostokątne. Dwa trójkąty umieszczone obok siebie tworzą kwadrat. Celem bufora indeksów jest powiedzenie OpenGL o kolejności indeksów brzegów trójkąta, zatem powiedzenie OpenGL, który róg w buforze wierzchołków jest rysowany. Innymi słowy, jeśli bufor indeksów to 0,1,2,0,2,3 , wtedy rogi w buforze wierzchołków to lewy dolny róg, górny lewy róg, górny prawy róg i dolny prawy róg : Kwadrat z trójkątów. Na koniec, tablica tekstur mówi OpenGL , jaki róg tekstury (lub obrazka) odwzorowuje określone rogi twoich wierzchołków. Ponieważ nie ma głębi w odwzorowaniu tekstury, tablica tekstur ma tylko współrzędne x i y . Jeśli pracujesz z wielokątem 3D, który ma wierzchołki korzystające ze współrzędnej z, a chcesz odwzorować teksturę do tych wierzchołków, musisz dostarczyć rogów tekstur używając współrzędnych x i y. Faktycznie, tablica tekstur będzie powtarzana dla każdego wierzchołka. Jeśli wierzchołki się z zmienią, jeśli pracujesz z wielokątem prostoliniowym, twoja tablica tekstury pozostanie taka sama. Metoda draw() tej klasy jest wywoływana w każdej klatce. Ta metoda używa informacji macierzowej, która jest modyfikowana w rendererze do narysowania tła. Zawiera również ustawienie dla wybranje ściany wielokątów , które nie są renderowane. Ostatnia metoda, loadTexture() zawiera wywołanie pobrania obrazka, który jej przekazujesz i ładowanie obrazk do OpenGL jako tekstury. Klasa używa tej tekstury w metodzie draw(). Poniższy listing pokazuje kod klasy PBBackground
PBackground class
package com.jfdimarzio;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.microedition.khronos.opengles.GL10;
import android.contenet.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
public classPBBackground {
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indexBuffer;
private int[] textures = new int[1];
private float vertices [] = {
0.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
};
private float texture [] = {
0.0f, 0.0f,
1.0f, 0f,
0f, 1f,
};
private byte indices[] = {
0,1,2,
0,2,3,
};
public PBBackground() {
ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
indexBuffer = ByteBuffer.allocateDirect(indices.length);
indexBuffer.put(indices);
indexBuffer.position(0);
}
public void draw(GL10 gl) {
gl.glBindTexture(GL10.GL TEXTURE 2D, textures[0]);
gl.glFrontFace(GL10.GL CCW);
gl.glEnable(GL10.GL CULL FACE);
gl.glCullFace(GL10.GL BACK);
gl.glEnableClientState(GL10.GL VERTEX ARRAY);
gl.glEnableClientState(GL10.GL TEXTURE COORD ARRAY);
gl.glVertexPointer(3, GL10.GL FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL FLOAT, 0, textureBuffer);
gl.glDrawElements(GL10.GL TRIANGLES, indices.length, GL10.GL UNSIGNED
BYTE, indexBuffer);
gl.glDisableClientState(GL10.GL VERTEX ARRAY);
gl.glDisableClientState(GL10.GL TEXTURE COORD ARRAY);
gl.glDisable(GL10.GL CULL FACE);
}
public void loadTexture(GL10 gl,int texture, Context context) {
InputStream imagestream = context.getResources().
openRawResource(texture);
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(imagestream);
}catch(Exception e){
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}
gl.glGenTextures(1, textures, 0);
gl.glBindTexture(GL10.GL TEXTURE 2D, textures[0]);
gl.glTexParameterf(GL10.GL TEXTURE 2D, GL10.GL TEXTURE MIN FILTER,
GL10.GL NEAREST);
gl.glTexParameterf(GL10.GL TEXTURE 2D, GL10.GL TEXTURE MAG FILTER,
GL10.GL LINEAR);
GLUtils.texImage2D(GL10.GL TEXTURE 2D, 0, bitmap, 0);
bitmap.recycle();
}
}
W końcowej części weźmiemy klasę PBBackground i wywołamy ją z PBGameRederer, rysując tło na ekranie z pomocą OpenGL
Rysowanie tła
Stworzymy nową instancję tła i wywołamy ją z PBGameRenderer. Kroki są następujące:
1.Tworzymy egzemplarz PBBackground. Jest to zrozumiałe samo przez się; zanim użyjemy klasy tła , musimy go utworzyć
2.Ładujemy obrazek bg1.png jako teksturę tła. Ponieważ obraz musi być załadowany raz, wywołamy metodę klasy tła loadTexture() z metodzie onSurfaceCreated() z PBGameRenderer.
3.Tworzymy nową metodę w PBGameRenderer, która modyfikuje rozmiar wielokąta tła . Jeśli tego nie zrobisz, nie będzie pasował rozmiar jakiego oczekujemy na ekranie.
4.Wywoła tą nową metodę z onDrawFrame(). Będzie rysowane tło na ekranie w każdej klatce. Jeśli nie wywołasz tej metody, nie będzie rendereowane
Poniższy listing pokazuje kod dla PBGameRendererl nowy kod dla rysowania tła jest pogrubiony:
PBGameRenderer z wywołaniem rysowania tła
package com.jfdimarzio;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class PBGameRenderer implements Renderer{
private PBBackground background = new PBBackground();
private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;
@Override
public void onDrawFrame(GL10 gl) {
// TODO Auto-generated method stub
loopStart = System.currentTimeMillis();
// TODO Auto-generated method stub
try {
if (loopRunTime < PBGameVars.GAME THREAD FPS SLEEP){
Thread.sleep(PBGameVars.GAME THREAD FPS SLEEP - loopRunTime);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
gl.glClear(GL10.GL COLOR BUFFER BIT | GL10.GL DEPTH BUFFER BIT);
drawBackground1(gl);
loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// TODO Auto-generated method stub
gl.glViewport(0, 0, width,height);
gl.glMatrixMode(GL10.GL PROJECTION);
gl.glLoadIdentity();
gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
// TODO Auto-generated method stub
gl.glEnable(GL10.GL TEXTURE 2D);
gl.glClearDepthf(1.0f);
gl.glEnable(GL10.GL DEPTH TEST);
gl.glDepthFunc(GL10.GL LEQUAL);
background.loadTexture(gl,PBGameVars.BACKGROUND, PBGameVars.context);
}
private void drawBackground1(GL10 gl){
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(1f, 1f, 1f);
background.draw(gl);
gl.glPopMatrix();
}
}

Skompiluj i uruchom project. W menu głównym kliknij przycisk Start. Powinieneś zobaczyć takie tło : Zrenderowane tło. Zanim zakończymy, kilka słów o trybach macierzy OpenGL Widząc wywołanie w PBGameRenderze, może to być nieco mylące to co rob. OpenGL ma trzy tryby w których można modyfikować różne macirerze w potokowaniu renderowaniu. Te trzy tyrby (i macierze) to ModelView, Texture i Projection. Praca w tych trybach wymaga pewnego abstrakcyjnego myślenia, ale nie jest to zbyt trudne. Umieszczenie OpenGL w trybie ModelView ładuje macierz ModelView. Macier ModelView kontroluje każdy zbiór wielokątów w świecie OpenGL. Tryb Texture, z drugiej strony, ładuje macierz dla wszystkich tekstur. . Zapamiętaj, że podczas wiązania tekstur ze zbiorem wierzchołków, w świecie OpenGLsą one jeszcze zawarte w dwóch innych macierzach. Jeśli jest 50 obiektów na ekranie, każda z tekstur, umieszczających OpenGL w trybie Texture daje dostęp do wszystkich 50 tekstur - nie tylko do jednej na której chcesz pracować. Tryb Project ładuje macierz, która steruje kamerę OpenGL. Wewnątrz każdego trybu macierzy, istnieją określone polecenia, których możesz użyć do pracy z obiektami w tych matrycach. Polecenie glLoadIdentity() mówi OpenGL aby załadował niezmodyfikowaną kopię macierzy. Powiedzmy , na przykład, że jesteś w trybie Teture i masz czerwoną teksturę, którą odwzorowujesz do kwadratu. W trybie Texture wymieniasz ją na zieloną. Wywołanie glLoadIdentity() ładuje macierz tekstur czerwoną teksturę. Polecenie glPushMatrix() wykonuje podobną funkcję. Poelcenie to daje ci kopie bieżącej macierzy, w jej bieżącym stanie. Dlatego w naszym ostatnim przykładzie, jeśli przeszliśmy do wywołania glPushMatrix() niż glLoadIdentity(), uzyskasz kopię tej macierzy z zieloną teksturą. Jeśli wywołasz glPlusMatrix() po wywołaniu glLoadIdentity, wtedy uzyskasz kopię macierzy z czerwoną teksturą. Kiedy wykonaliśmy już pracę związaną z kopiowaniem macierzy, którą stworzyliśmu używając glPushMatrix(), użyj glPopMatrix() do zapisania tej kopii z powrotem do potokowania OpenGL. Jest to przydatne jeśli mamy wiele transformacji ,które chcemy stworzyć na macierzy a nie chcesz spowodować nieumyślne problemy na macierzy głównej. W końcu, istnieą trzy polecenia których użyć do przekształcenia swoich macierzy : glScale, glTranslate i glRotate. Jak wskazują ich nazwy, glScale i glRotate skalują i obracają macierz, odpowiednio. Ponownie , efekt tych poleceń zależy od wielkości trybu macierzy w jakim są. Polecenie glTranslate przesuwa macierz o dany zbiór współrzędnych.
Tworzenie postaci gracza i przeszkód
Nauczyłeś się ja dodać obrazek tła dla gry. Stworzyłeś klasę, która , kiedy jest tworzona, da ci wszystkie zasoby jakich potrzebujesz dla dodania tła. Tutaj poznamy i zastosujemy to dla cegieł, rakietki gracza i piłki. Da to nam wszystkie obiekty ekranowe potrzebne w naszej grze. Musimy jednak najpierw zrobić kilka rzeczy.
Zanim zaczniemy
Musimy dodać kilka rzeczy do naszego projektu, którymi będą obrazki używane jako cegły, rakietka i piłka. Dla cegieł użyjemy czegoś takiego jak spritesheet, co pozwoli nam łatwiej użyć różnych obrazków przez zawarcie ich w jednym fizycznym pliku. Spritesheet jest pojedynczym plikiem obrazka, który zawiera wewnątrz wszystkie inne obrazki dla określonych animacji lb zbioru postaci. Na przykład , jeśli stworzyłeś grę, w której główna postać spaceruje po ekranie, spritesheet dla tej postaci będzie zawierał wszystkie klatki używane do stworzenia animacji postaci. Podobnie sytuacja wygląda jeśli chodzi o piłkę. Ten spritesheet ma dwa różne obrazki piłki do wyboru. Obrazki użyte w Prison Break dla cegieł, rakietki i piłki są pokazane tu : Cegły, Rakietka, Piłka. Może zauważyłeś , że cegły i rakietka są raczej kwadratami niż prostokątami. To dobrze, ponieważ daje ci to szansę na przeciągniecie obrazu do prostokąta używając OpenGL. Następnie musisz dodać zmienne do pliku PBGaneVArs. Dodaj następujące linie do twojego PBGameVars :
public static float playerBankPosX = -.73f;
public static int playerAction = 0;
public static final int PLAYER MOVE LEFT 1 = 1;
public static final int PLAYER RELEASE = 3;
public static final int PLAYER MOVE RIGHT 1 = 4;
public static final float PLAYER MOVE SPEED = .2f;
public static final int PADDLE = R.drawable.goldbrick;
public static final int BRICK BLUE = 1;
public static final int BRICK BROWN = 2;
public static final int BRICK DARK GRAY = 3;
public static final int BRICK GREEN = 4;
public static final int BRICK LITE GRAY = 5;
public static final int BRICK PURPLE = 6;
public static final int BRICK RED = 7;
public static final int BRICK SHEET = R.drawable.bricksheet;
public static final int BALL SHEET = R.drawable.ballsheet;
public static final float BALL SPEED = 0.01f;
public static float ballTargetY = 0.01f;
public static float ballTargetX = -1.125f;
Tworznie klasy rakietki gracza
Dodamy nową klasę do naszego projektu. Ta klasa będzie wyglądała podobnie do klasy jaką stworzyliśmy da tła, PBBackground. Klasa zawiera konstruktor, metodę draw() i metodę loadTexture(). Kod dla klasy PBPlayer jest pokazany tu:
package com.jfdimarzio;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
public class PBPlayer {
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indexBuffer;
private int[] textures = new int[1];
private float vertices[] = {
0.0f, 0.0f, 0.0f,
1.5f, 0.0f, 0.0f,
1.5f, .25f, 0.0f,
0.0f, .25f, 0.0f,
};
private float texture[] = {
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f,
};
private byte indices[] = {
0,1,2,
0,2,3,
};
public PBPlayer() {
ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
indexBuffer = ByteBuffer.allocateDirect(indices.length);
indexBuffer.put(indices);
indexBuffer.position(0);
}
public void draw(GL10 gl) {
gl.glBindTexture(GL10.GL TEXTURE 2D, textures[0]);
gl.glFrontFace(GL10.GL CCW);
gl.glEnable(GL10.GL CULL FACE);
gl.glCullFace(GL10.GL BACK);
gl.glEnableClientState(GL10.GL VERTEX ARRAY);
gl.glEnableClientState(GL10.GL TEXTURE COORD ARRAY);
gl.glVertexPointer(3, GL10.GL FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL FLOAT, 0, textureBuffer);
gl.glDrawElements(GL10.GL TRIANGLES, indices.length, GL10.
GL UNSIGNED BYTE, indexBuffer);
gl.glDisableClientState(GL10.GL VERTEX ARRAY);
gl.glDisableClientState(GL10.GL TEXTURE COORD ARRAY)
; gl.glDisable(GL10.GL CULL FACE);
}
public void loadTexture(GL10 gl,int texture, Context context) {
InputStream imagestream =
context.getResources().openRawResource(texture);
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(imagestream);
}catch(Exception e){
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}
gl.glGenTextures(1, textures, 0);
gl.glBindTexture(GL10.GL TEXTURE 2D, textures[0]);
gl.glTexParameterf(GL10.GL TEXTURE 2D, GL10.GL TEXTURE MIN FILTER,
GL10.GL NEAREST);
gl.glTexParameterf(GL10.GL TEXTURE 2D, GL10.GL TEXTURE MAG FILTER,
GL10.GL LINEAR);
GLUtils.texImage2D(GL10.GL TEXTURE 2D, 0, bitmap, 0);
bitmap.recycle();
}
}
Tworzenie klasy dla cegieł
Potrzebujemy klasy , która będzie przedstawiała cegły. Ta klasa musi być trochę inna. Ponieważ mamy zamiar użyć spritesheet dla klasy cegieł, nie możesz wywołać loadTexture() w ten sam sposób w jaki wywoływałeś przy tle czy rakietce. Musisz załadować wszystkie spritesheety do tablicy i przekazać je razem. Dlatego, musimy usunąć metodę loadTexture() i stworzyć nową klasę do obsługi tekstur sritesheet.
PBBRick
package com.jfdimarzio;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
public class PBBrick {
public float posY = 0f;
public float posX = 0f;
public float posT = 0f;
public boolean isDestroyed = false;
public int brickType = 0;
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indexBuffer;
private float vertices[] = {
0.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, .25f, 0.0f,
0.0f, .25f, 0.0f,
};
private float texture[] = {
0.0f, 0.0f,
0.25f, 0.0f,
0.25f, 0.25f,
0.0f, 0.25f,
};
private byte indices[] = {
0,1,2,
0,2,3,
};
public PBBrick(int type) {
brickType = type;
ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
indexBuffer = ByteBuffer.allocateDirect(indices.length);
indexBuffer.put(indices);
indexBuffer.position(0);
}
public void draw(GL10 gl, int[] spriteSheet) {
gl.glBindTexture(GL10.GL TEXTURE 2D, spriteSheet[0]);
gl.glFrontFace(GL10.GL CCW);
gl.glEnable(GL10.GL CULL FACE);
gl.glCullFace(GL10.GL BACK);
gl.glEnableClientState(GL10.GL VERTEX ARRAY);
gl.glEnableClientState(GL10.GL TEXTURE COORD ARRAY);
gl.glVertexPointer(3, GL10.GL FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL FLOAT, 0, textureBuffer);
gl.glDrawElements(GL10.GL TRIANGLES, indices.length,
GL10.GL UNSIGNED BYTE, indexBuffer);
gl.glDisableClientState(GL10.GL VERTEX ARRAY);
gl.glDisableClientState(GL10.GL TEXTURE COORD ARRAY);
gl.glDisable(GL10.GL CULL FACE);
}
}
Ponieważ PBBrick używa spriteshhet dla tekstur, musimy stworzyć nową klasę , która obsłuży te tekstury. Nazwiemy ją PBTextures. Klasa PBTextures przechowuje tablicę tekstur i serwuje poprawną dla właściwej klasy, jakiej potrzebujesz. Kod klasy PBTexture :
package com.jfdimarzio;
import java.io.IOException;
import java.io.InputStream;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
public class PBTextures {
private int[] textures = new int[3];
public PBTextures(GL10 gl){
gl.glGenTextures(3, textures, 0);
}
public int[] loadTexture(GL10 gl, int texture, Context context,int
textureNumber) {
InputStream imagestream =
context.getResources().openRawResource(texture);
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(imagestream);
}catch(Exception e){
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}
gl.glBindTexture(GL10.GL TEXTURE 2D, textures[textureNumber - 1]);
gl.glTexParameterf(GL10.GL TEXTURE 2D, GL10.GL TEXTURE MIN FILTER,
GL10.GL NEAREST);
gl.glTexParameterf(GL10.GL TEXTURE 2D, GL10.GL TEXTURE MAG FILTER,
GL10.GL LINEAR);
gl.glTexParameterf(GL10.GL TEXTURE 2D, GL10.GL TEXTURE WRAP S,
GL10.GL CLAMP TO EDGE);
gl.glTexParameterf(GL10.GL TEXTURE 2D, GL10.GL TEXTURE WRAP T,
GL10.GL CLAMP TO EDGE);
GLUtils.texImage2D(GL10.GL TEXTURE 2D, 0, bitmap, 0);
bitmap.recycle();
return textures;
}
}
Tworzenie klasy PBBall
Stworzymy nową klasę PBBAll. Klasa ta , podobnie jak PBBrick, używa spritesheet, więc nie loadTexture. Klasa PBBall jest bardzo podobna do PBBrick. Kod dla PBBall :
package com.jfdimarzio;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.Random;
import javax.microedition.khronos.opengles.GL10;
public class PBBall {
public float posY = 0f;
public float posX = 0f;
public float posT = 0f;
public int ballMode = 0;
private Random randomPos = new Random();
private int damage = 0;
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indexBuffer;
private float vertices[] = {
0.0f, 0.0f, 0.0f,
0.25f, 0.0f, 0.0f,
0.25f, 0.25f, 0.0f,
0.0f, 0.25f, 0.0f,
};
private float texture[] = {
0.0f, 0.0f,
0.50f, 0.0f,
0.50f, 0.50f,
0.0f, 0.50f,
};
private byte indices[] = {
0,1,2,
0,2,3,
};
public PBBall() {
posY = (randomPos.nextFloat() + 1f) * (float)(-1.75 - -1.6);
posX = randomPos.nextFloat() * .75f;
ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);
byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
indexBuffer = ByteBuffer.allocateDirect(indices.length);
indexBuffer.put(indices);
indexBuffer.position(0);
}
}
public void draw(GL10 gl, int[] spriteSheet) {
gl.glBindTexture(GL10.GL TEXTURE 2D, spriteSheet[2]);
gl.glFrontFace(GL10.GL CCW);
gl.glEnable(GL10.GL CULL FACE);
gl.glCullFace(GL10.GL BACK);
gl.glEnableClientState(GL10.GL VERTEX ARRAY);
gl.glEnableClientState(GL10.GL TEXTURE COORD ARRAY);
gl.glVertexPointer(3, GL10.GL FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL FLOAT, 0, textureBuffer);
gl.glDrawElements(GL10.GL TRIANGLES, indices.length,
GL10.GL UNSIGNED BYTE, indexBuffer);
gl.glDisableClientState(GL10.GL VERTEX ARRAY);
gl.glDisableClientState(GL10.GL TEXTURE COORD ARRAY);
gl.glDisable(GL10.GL CULL FACE);
}
}
Teraz mamy wszystkie klasy gotowe dla cegieł, rakietki i piłki, czas wstawić wszystko razem i nauczyć się jak wywołać je w PBGameRenderer, prawda? Nie całkiem. Są dwie mniejsze pomocne klasy, których będziesz potrzebować aby ułatwić sobie życie później.
PBRow i PBWall
W starych grach, cegły , które trzeba było przełamać są ułożone we wzór ceglanego muru. Jest też tak w przypadku Prison Break. Mamy zamiar stworzyć dwie klasy PBRow i PBWall, aby pomóc w tworzeniu instancji cegieł i ułożenie ich w ceglany mur. Klasa PBWall będzie się składać z określonej liczby wierszy. Wiersze te są oddzielone instancjami PBRow, która z kolei jest wykonana z wcześniej określonej liczby i układu PBBrick. Dlatego kiedy inicjalizujesz grę, będziesz musiał stworzyć instancję PBWall i powiedzieć ile wierszy chcesz; PBWall zajmie się resztą. Stwrozymy nową klasę nazwaną PBWall. Kod dla PBWall :
package com.jfdimarzio;
public class PBWall {
public PBRow[] rows;
public PBWall(int numberOfRows){
rows = new PBRow[numberOfRows];
for(int x = 0; x < = numberOfRows - 1; x ++)
{
rows[x] = new PBRow(x);
}
}
}
Następnie tworzymy klase nazwana PBRow. Kod :
package com.jfdimarzio;
import java.util.Random;
public class PBRow {
public PBBrick[] bricks;
private Random brickType = new Random();
private boolean isRowOdd = false;
private int numberOfBricks = 0;
public PBRow(int rowNumber){
if(rowNumber 2 > 0)
{
numberOfBricks = 4;
isRowOdd = true;
}
else
{
numberOfBricks = 5;
isRowOdd = false;
}
bricks = new PBBrick[numberOfBricks];
for(int x = 0; x < numberOfBricks ; x++)
{
bricks[x] = new PBBrick((int) (brickType.nextFloat() * 7));
if(isRowOdd)
{
bricks[x].posX = x - 2f ;
bricks[x].posY = (rowNumber * .25f) + 1 ;
}
else
{
bricks[x].posX = x - 2.5f;
bricks[x].posY = (rowNumber * .25f) + 1 ;
}
}
}
}
Teraz wstawimy wszystko razem w PBGameRendered
Wywołanie cegieł, rakietki i piłki w PBGameRenderer
Pierwsza rzecz jaką musisz wykonać jest stworzenie zmiennych dla ściany, rakietki, piłki spritesheet′ów itd. Poniżej mamy nowe zmienne jakie musisz dodać do PBGameRenderer:
private PBPlayer player1 = new PBPlayer();
private PBBall ball = new PBBall();
private PBTextures textureLoader;
private int[] spriteSheets = new int[3];
private int numberOfRows = 4;
private PBWall wall;
Po tych zmiennych, musimy dodać jakieś loader tekstur do metody onSurfaceCreated. Kod jaki musisz dodać jest w poniższym fragmencie :
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
// TODO Auto-generated method stub
initializeBricks();
textureLoader = new PBTextures(gl);
spriteSheets = textureLoader.loadTexture(gl, PBGameVars.BRICK SHEET,
PBGameVars.context, 1);
gl.glEnable(GL10.GL TEXTURE 2D);
gl.glClearDepthf(1.0f);
gl.glEnable(GL10.GL DEPTH TEST);
gl.glDepthFunc(GL10.GL LEQUAL);
background.loadTexture(gl,PBGameVars.BACKGROUND, PBGameVars.
context);
player1.loadTexture(gl,PBGameVars.PADDLE, PBGameVars.context);
}
Zauważ ,że onSurfaceCreated() wywołuje nową metodę, initializeBricks(). Ta nowa metoda tworzy ścianę dla ciebie
private void initializeBricks(){
wall = new PBWall(numberOfRows);
}
Teraz potrzebujesz metody, która rysuje cegły w każdej ramce; coś co można nazwać metodą drawFrame(), podobną do metody drawBackground(). Metoda drawBricks() wywoływana jest w każdej klatce i oferuje parę funkcji. Po pierwsze, przez iterację PBWall i odczyt flagi isDestroyed każdej cegły, określa czy cegła ma być tknięta w grze. Jeśli cegła została zniszczona, jest pomijana w pętli rysunku, uniemożliwiając jej ponowne renderowanie na ekranie, co powoduje ,że znika z gry. Po drugie metoda drawBricks() korzysta z instrukcji case zbudowanej wokół brickType każdej cegły dla określenia który obrazek w spritesheet′cie cegły zostanie użyty jako tekstura dla określonej cegły. Jest to ważna część kodu ponieważ używa glTranslatef() do przesunięcia spritesheeta do właściwej tekstury dla każdej cegły. Pomyśl o tym tak : cegła, która jest rysowana w grze jest rozmiaru cegły, ale spritesheet, który przechowuje wszystkie obrazki jest rozmiaru siedmiu cegieł. Dlatego, używając glTranslatef(), masz zamiar przenieść spritesheet wokół cegły dopóki poprawny obrazek cegły jest odwzorowany dla właściwej cegły. Widzimy ten kod w działaniu :
private void drawBricks(GL10 gl){
for (int x = 0; x < wall.rows.length; x++)
{
for(int y = 0; y < wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
switch (wall.rows[x].bricks[y].
brickType){
case PBGameVars.BRICK BLUE:
gl.glMatrixMode(GL10.
GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f,
1f);
gl.glTranslatef(wall.
rows[x].bricks[y].posX, wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.50f, 0.25f , 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK BROWN:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f,
1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX, wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,
0.50f , 0.0f);
wall.rows[x].bricks[y].
draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK DARK GRAY:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f,
1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX, wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.25f,
0.25f , 0.0f);
wall.rows[x].bricks[y].
draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK GREEN:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f,
1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX, wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,
0.25f , 0.0f);
wall.rows[x].bricks[y].
draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK LITE GRAY:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f,
1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX, wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.25f,
0.0f , 0.0f);
wall.rows[x].bricks[y].
draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK PURPLE:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f,
1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX, wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.50f,
0.0f , 0.0f);
wall.rows[x].bricks[y].
draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK RED:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f,
1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX, wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,
0.0f , 0.0f);
wall.rows[x].bricks[y].
draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
default:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f,
1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX, wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,
0.0f , 0.0f);
wall.rows[x].bricks[y].
draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
}
}
}
}
}
Na koniec potrzebujemy dwóch metod dla przesunięcia rakietki gracza i piłkę po klatkach. Najpierw w metodzie moveBall() możesz zauważyć ,że istnieją podstawowe matematyczne trajektorie wykonywane w tle. tak by przenieść je z losowego punktu startowego do punktu na ekranie. Ta matematyka nie bierze pod uwagę wykrywania zderzeń lub ugięć kątowych.
private void moveBall(GL10 gl){
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
ball.posX + = (float) ((PBGameVars.ballTargetX - ball.posX )/
(ball.posY / (PBGameVars.ballTargetY )));
ball.posY - = PBGameVars.ballTargetY * 3;
gl.glTranslatef(ball.posX, ball.posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
ball.draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
}
Przeniesienie rakietki jest podobne; jednak, używa zmiennych z PBGameVars dla określenia gdzie przesunąć gracza
private void movePlayer1(GL10 gl){
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (PBGameVars.playerAction == PBGameVars.PLAYER MOVE LEFT 1 &&
PBGameVars.playerBankPosX > 0)
{
PBGameVars.playerBankPosX = PBGameVars.playerBankPosX -
PBGameVars.PLAYER MOVE SPEED;
}
else if(PBGameVars.playerAction == PBGameVars.PLAYER MOVE
RIGHT 1 && PBGameVars.playerBankPosX < 2.5)
{
PBGameVars.playerBankPosX = PBGameVars.playerBankPosX +
PBGameVars.PLAYER MOVE SPEED;
}
gl.glTranslatef(PBGameVars.playerBankPosX, .5f, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
}
Zmienna playerAction w poprzednim kodzie okreśa czy gracz chce przesunąć rakietkę w lewo lub prawo. W świecie rzeczywistym, gracz dotyka albo lewą albo prawą stronę ekranu urządzenia dla przeniesienia rakietki. Aby wykryć dotyk ekranu i ustawić właściwą zmienną , dodamy kod do pliku PBGame:
package com.jfdimarzio;
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
public class PBGame extends Activity {
final PBGameVars gameEngine = new PBGameVars();
private PBGameView gameView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gameView = new PBGameView(this);
setContentView(gameView);
}
@Override
protected void onResume() {
super.onResume();
gameView.onResume();
}
@Override
protected void onPause() {
super.onPause();
gameView.onPause();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
int height = PBGameVars.display.getHeight() / 4;
int playableArea = PBGameVars.display.getHeight() - height;
if (y > playableArea){
switch (event.getAction()){
case MotionEvent.ACTION DOWN:
if(x < PBGameVars.display.getWidth() / 2){
PBGameVars.playerAction = PBGameVars.PLAYER
MOVE LEFT 1;
}else{
PBGameVars.playerAction = PBGameVars.PLAYER
MOVE RIGHT 1;
}
break;
case MotionEvent.ACTION UP:
PBGameVars.playerAction = PBGameVars.PLAYER RELEASE;
break;
}
}
return false;
}
Kompletny kod PBGameRenderer :
package com.jfdimarzio;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class PBGameRenderer implements Renderer{
private PBBackground background = new PBBackground();
private PBPlayer player1 = new PBPlayer();
private PBBall ball = new PBBall();
private PBTextures textureLoader;
private int[] spriteSheets = new int[3];
private int numberOfRows = 4;
private PBWall wall;
private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;
private float bgScroll1;
@Override
public void onDrawFrame(GL10 gl) {
// TODO Auto-generated method stub
loopStart = System.currentTimeMillis();
// TODO Auto-generated method stub
try {
if (loopRunTime < PBGameVars.GAME THREAD FPS SLEEP){
Thread.sleep(PBGameVars.GAME THREAD FPS SLEEP -
loopRunTime);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
gl.glClear(GL10.GL COLOR BUFFER BIT | GL10.GL DEPTH BUFFER BIT);
drawBackground1(gl);
movePlayer1(gl);
drawBricks(gl);
moveBall(gl);
loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// TODO Auto-generated method stub
gl.glViewport(0, 0, width,height);
gl.glMatrixMode(GL10.GL PROJECTION);
gl.glLoadIdentity();
gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
// TODO Auto-generated method stub
initializeBricks();
textureLoader = new PBTextures(gl);
spriteSheets = textureLoader.loadTexture(gl, PBGameVars.BRICK SHEET,
PBGameVars.context, 1);
gl.glEnable(GL10.GL TEXTURE 2D);
gl.glClearDepthf(1.0f);
gl.glEnable(GL10.GL DEPTH TEST);
gl.glDepthFunc(GL10.GL LEQUAL);
background.loadTexture(gl,PBGameVars.BACKGROUND, PBGameVars.
context);
player1.loadTexture(gl,PBGameVars.PADDLE, PBGameVars.context);
}
private void drawBackground1(GL10 gl){
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(1f, 1f, 1f);
background.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
}
private void initializeBricks(){
wall = new PBWall(numberOfRows);
}
private void drawBricks(GL10 gl){
for (int x = 0; x < wall.rows.length; x++)
{
for(int y = 0; y < wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
switch (wall.rows[x].bricks[y].brickType){
case PBGameVars.BRICK BLUE:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.5-f, 0.25f,0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK BROWN:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .5f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posZ, wall.rows[x].bricks[y].posY,0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTraslatef(0.0f,0.50f,0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK DARK GRAY:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.25f, 0.25f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK GREEN:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.25f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK LITE GRAY:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.25f, 0.0f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK PURPLE:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.50f, 0.0f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK RED:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.0f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
default:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.0f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
}
}
}
}
}
private void moveBall(GL10 gl){
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
ball.posX += (float) ((PBGameVars.ballTargetX - ball.posX )/ (ball.posY
/(PBGameVars.ballTargetY )));
ball.posY -= PBGameVars.ballTargetY * 3;
gl.glTranslatef(ball.posX, ball.posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
ball.draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
}
private void movePlayer1(GL10 gl){
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (PBGameVars.playerAction == PBGameVars.PLAYER MOVE LEFT 1 &&
PBGameVars.playerBankPosX > 0)
{
PBGameVars.playerBankPosX = PBGameVars.playerBankPosX -
PBGameVars.PLAYER MOVE SPEED;
}
else if(PBGameVars.playerAction == PBGameVars.PLAYER MOVE RIGHT 1 &&
PBGameVars.playerBankPosX < 2.5)
{
PBGameVars.playerBankPosX = PBGameVars.playerBankPosX +
PBGameVars.PLAYER MOVE SPEED;
}
gl.glTranslatef(PBGameVars.playerBankPosX, .5f, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
}
}
Wykrywanie zderzeń
Dodaliśmy już do gry elementy takie jak cegły, rakietkę gracza i piłka. Po uruchomieniu kodu, jednak, stwierdzisz ,ze chociaż możesz poruszać rakietką, nie ma to wpływu na rozgrywkę. Piłka porusza się od dowolnej pozycji i upada na dole ekranu. Powodem tego jest wyraźny brak wykrywania zderzeń w grze.
Cel wykrywania zdarzeń
Dokładna definicja wykrywania zdarzeń, jest taka ,ze wykrywa kiedy elementy na ekranie się zderzyły. Dobre systemy wykrywania zderzeń testują i oceniają kiedy elementy w grze stykają się ze sobą. To zapewnia również metodę, która reaguje na te zderzenia. System wyrywania zderzeń dla Prison Break będzie prostyu, ale pokaze jak działa wykrywanie zderzeń. Pamiętaj ,że OpenGL nie ma wbudowanego wykrywania zderzeń lub możliwości testowania wierzchołków; po prostu nie jest stworzony do tego. Twoim obowiązkiem jako programuisty jest dostarczyć takiego mechanizmu do gry
Wykrywanie zderzeń w Prison Break
Zanim zaczniemy pisać system wykrywania zderzeń dla Prison Brreak, musimy omówić co jest do tego potrzebne. Zawsze lepiej wiedzieć jak działa system zanim zaczniesz go kodować. Poniżej jest lista pozycji, które musimy przetestować w systemie wykrywania zderzeń dla Prison Break:
• Czy piłka tarfia w rakietkę?
• Czy piłka trafia w cegłę? • Czy piłka trafiła w prawą część "ściany" lub brzeg ekranu?
• Czy piłka trafiłą w lewą część "ściany" lub brzeg ekranu?
• Czy piłka wyszła poza górną granicę gry?
• Czy piłka wyszła poza dolną granicę gry?
Oprócz tych testów, system wykrywania zdarzeń musi wykonać następujące działania
• "Niszczy" cegłę, która została trafiona przez piłkę
• Wylicza kąt odchylenia i przesunięcia pilki jeśli została trafiona rakietką w prawy lub lewy brzeg ekranu gry
Tworzenie systemu wykrywania zderzeń
Nie musimy tworzyć nowej kalsy dla systemu wykrywania zderzeń, raczej skorzystamy z metody w PBGameGRenderer. Ważne jest aby system wykrywania zderzeń uruchamiał się w każdej klatce renderowanej gry. Dlatwego też, metoda będzie wywoływana z onDrawFrame(). Tworzymy nową metodę w PBGameRenderer nazwaną detectCollisions(). Weźmy kod tej metody krok po korku. Podamy potem pełny kod PBGameRenderer aby można było ją zobaczyć w kontekście. Pierwszą rzczą jaką rob ta metoda to testowanie czy piłka jest poza dolną krawędzią ekranu. Jeśli tak jest, wskazuje scenariusz końca gry. Aby określić czy piłka jest poza brzegiem ekranu, po prostu testujemy pozycję osi y aby zobaczyć czy jest mniejsza niż 0.
if(ball.posY < = 0){
//GameOver
}
Następnie, testujemy każdą cegłę aby zobaczyć czy lub nie piłka miała kontakt z nią
for (int x = 0; x < wall.rows.length; x++)
{
for(int y = 0; y < wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
if (((ball.posY > wall.rows[x].bricks[y].
posY - .25f)
&& (ball.posY < wall.rows[x].bricks[y].posY)
&& (ball.posX + .25f > wall.rows[x].bricks[y].posX)
&& (ball.posX < wall.rows[x].bricks[y].
posX + 1.50f)))
{
wall.rows[x].bricks[y].isDestroyed = true;
PBGameVars.ballTargetY = PBGameVars.
ballTargetY * -1f;
if(PBGameVars.ballTargetX == ?2f){
PBGameVars.ballTargetX = 5f;
}else{
PBGameVars.ballTargetX = ?2f;
}
}
}
}
}
Zwróć uwagę ,na instrukcję testującą czy piłka dotknęła cegły. Po pierwsze testuje czy pozycję na osi y piłki (lewy górny róg) czy jest większa niż pozycja osi y cegły minus 0,25. Jest tak ponieważ pozycja osi y cegły jest lewym górnym rogiem cegły; ale chcemy zobaczyć czy piłka trafiła dół cegły, więc odejmujemy wysokość cegły od jej pozycji na osi y aby uyskać pozycję do lną na osi y. Jeśli piłka trafiła w cegłę, flaga isDestroyed cegły jest ustawiona na true. Jak widzieliśmy w metodzie drwaBricks(), dowolna cegła która ma ustawioną flagę isDestryed na true nie jest rysowana. Dlatego też, przy kolejnej iteracji pętli gry, cegła, która zostanie trafiona nie zostanie odrysowana i nie pojaw się na ekranie gry. Po zniszczeniu cegły, piłka odbija się od niej. Dlatego też musisz obliczyć kąt odbicia piłki. Szczęśliwie dla ciebie, wszystkie cegły leżące pod kątem 90 stopni a kąt ataku zawsze wyniesie 45 stopni. To daje nam czysty i zgodny kąt odbicia. W rzeczywistości, wszystko co musisz zrobić, aby odbić piłkę prawidłowo jest ustawienie wartości osi y na odwrotny znak. Na koniec, musimy ustawić cel dla piłki. Poprzez zastosowanie zmiennej ballTargetX , nadajesz piłce wartość osi x "dążącą do" przesunięcia. Pozwala to na łatwe utrzymanie jej kursu. Jeśli piłka przesuwa się w prawo, cel jest na dodatniej stronie osi x, gdy piłka porusza się w lewo, cel jest po ujemnej stronie osi x. Kiedy piłka ma zderzenie, będzie się odbijać; dlatego, cel musi obrócić piłkę ,aby przesuwała się w przeciwnym kierunku. Końcowa sekcja metosy wykrywania zderzeń testuje czy piłka trafiła w rakietkę gracza. Zauważ ,że testy są wykonywane ponownie na cegłach , jak również odbiciach , mają zastosowanie również do rakietki.
if((ball.posY - .25f < = .5f)
&& (ball.posX + .25f > PBGameVars.playerBankPosX )
&& (ball.posX < PBGameVars.playerBankPosX + 1.50f)){
PBGameVars.ballTargetY = PBGameVars.ballTargetY * -1f;
if(PBGameVars.ballTargetX == ?2f){
PBGameVars.ballTargetX = 5f;
}else{
PBGameVars.ballTargetX = ?2f;
}
}
if(ball.posX < 0 || ball.posX + .25f > 3.75f)
{
PBGameVars.ballTargetX = PBGameVars.ballTargetX * -1f;
}
}
Skończona PBGameRendered
Poniższy listing pokazuje ukończona PBGameRendered.
package com.jfdimarzio;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;
public class PBGameRenderer implements Renderer{
private PBBackground background = new PBBackground();
private PBPlayer player1 = new PBPlayer();
private PBBall ball = new PBBall();
private PBTextures textureLoader;
private int[] spriteSheets = new int[3];
private int numberOfRows = 4;
private PBWall wall;
private long loopStart = 0;
private long loopEnd = 0;
private long loopRunTime = 0 ;
private float bgScroll1;
@Override
public void onDrawFrame(GL10 gl) {
// TODO Auto-generated method stub
loopStart = System.currentTimeMillis();
// TODO Auto-generated method stub
try {
if (loopRunTime < PBGameVars.GAME THREAD FPS SLEEP){
Thread.sleep(PBGameVars.GAME THREAD FPS SLEEP -
loopRunTime);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
gl.glClear(GL10.GL COLOR BUFFER BIT | GL10.GL DEPTH BUFFER BIT);
drawBackground1(gl);
movePlayer1(gl);
drawBricks(gl);
moveBall(gl);
detectCollisions();
loopEnd = System.currentTimeMillis();
loopRunTime = ((loopEnd - loopStart));
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// TODO Auto-generated method stub
gl.glViewport(0, 0, width,height);
gl.glMatrixMode(GL10.GL PROJECTION);
gl.glLoadIdentity();
gl.glOrthof(0f, 1f, 0f, 1f, -1f, 1f);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
// TODO Auto-generated method stub
initializeBricks();
textureLoader = new PBTextures(gl);
spriteSheets = textureLoader.loadTexture(gl, PBGameVars.BRICK
SHEET, PBGameVars.context, 1);
gl.glEnable(GL10.GL TEXTURE 2D);
gl.glClearDepthf(1.0f);
gl.glEnable(GL10.GL DEPTH TEST);
gl.glDepthFunc(GL10.GL LEQUAL);
background.loadTexture(gl,PBGameVars.BACKGROUND, PBGameVars.context);
player1.loadTexture(gl,PBGameVars.PADDLE, PBGameVars.context);
}
private void drawBackground1(GL10 gl){
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(1f, 1f, 1f);
background.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
}
private void initializeBricks(){
wall = new PBWall(numberOfRows);
}
private void drawBricks(GL10 gl){
for (int x = 0; x < wall.rows.length; x++)
{
for(int y = 0; y < wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
switch (wall.rows[x].bricks[y].brickType){
case PBGameVars.BRICK BLUE:

gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.50f, 0.25f, 0.0f);
wall.rows[x].bricks[y].draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK BROWN:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.50f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK DARK GRAY:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.25f,0.25f , 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK GREEN:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.25f,0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK LITE GRAY:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.25f, 0.0f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK PURPLE:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.50f, 0.0f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
case PBGameVars.BRICK RED:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.0f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
default:
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
gl.glTranslatef(wall.rows[x].bricks[y].posX,
wall.rows[x].bricks[y].posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.0f, 0.0f);
wall.rows[x].bricks[y].draw(gl, spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
}
}
}
}
}
private void moveBall(GL10 gl){
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
ball.posX + = (float) ((PBGameVars.ballTargetX - ball.posX )/
(ball.posY / (PBGameVars.ballTargetY )));
ball.posY - = PBGameVars.ballTargetY * 3;
gl.glTranslatef(ball.posX, ball.posY, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
ball.draw(gl,spriteSheets);
gl.glPopMatrix();
gl.glLoadIdentity();
}
private void movePlayer1(GL10 gl){
gl.glMatrixMode(GL10.GL MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.25f, .25f, 1f);
if (PBGameVars.playerAction == PBGameVars.PLAYER MOVE LEFT 1
&&PBGameVars.playerBankPosX > 0)
{
PBGameVars.playerBankPosX = PBGameVars.playerBankPosX -
PBGameVars.PLAYER MOVE SPEED;
}
else if(PBGameVars.playerAction == PBGameVars.PLAYER MOVE RIGHT 1
&& PBGameVars.playerBankPosX < 2.5)
{
PBGameVars.playerBankPosX = PBGameVars.playerBankPosX +
PBGameVars.PLAYER MOVE SPEED;
}
gl.glTranslatef(PBGameVars.playerBankPosX, .5f, 0f);
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f,0.0f, 0.0f);
player1.draw(gl);
gl.glPopMatrix();
gl.glLoadIdentity();
}
private void detectCollisions(){
if(ball.posY < = 0){
//GameOver
}
for (int x = 0; x < wall.rows.length; x++)
{
for(int y = 0; y < wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
if (((ball.posY > wall.rows[x].bricks[y].posY - .25f)
&& (ball.posY < wall.rows[x].bricks[y].posY)
&& (ball.posX + .25f > wall.rows[x].bricks[y].posX)
&& (ball.posX < wall.rows[x].bricks[y].posX + 1.50f)))
{
wall.rows[x].bricks[y].isDestroyed = true;
PBGameVars.ballTargetY = PBGameVars.ballTargetY * -1f;
if(PBGameVars.ballTargetX == ?2f){
PBGameVars.ballTargetX = 5f;
}else{
PBGameVars.ballTargetX = ?2f;
}
}
}
}
}
if((ball.posY - .25f < = .5f)
&& (ball.posX + .25f > PBGameVars.playerBankPosX )
&& (ball.posX < PBGameVars.playerBankPosX + 1.50f)){
PBGameVars.ballTargetY = PBGameVars.ballTargetY * -1f;
if(PBGameVars.ballTargetX == ?2f){
PBGameVars.ballTargetX = 5f;
}else{
PBGameVars.ballTargetX = ?2f;
}
}
if(ball.posX < 0 || ball.posX + .25f > 3.75f)
{
PBGameVars.ballTargetX = PBGameVars.ballTargetX * -1f;
}
}
}
Przechowywanie wyników
Nauczułeś się już jak implementować podstawowy system wykrywania zderzen w swojej grze. System ten pozwala ci testować zderzenia między piłką a cegłami lub rakietką. Czas dołożyć końcowy dotyk do gry. Większość gier zręcznościowych zawiera element punktacji. Czy jest to bezpośredni wyni czy też ranking, który koreluje z tym jak poziom został ukończony, punktacja jest tym co pozwala graczowi poznać jak grał w porównaniu z innymi graczami. W tej sekcji, zapoznasz się z dwoma możliwymi sposobami zachowania punktacji w grze. Pierwsza metoda dodaje określoną ilość punktów do punktacji gracza dla każdej złamanej cegły. Druga metoda nagradza gracza liczbą punktów za każdy kompletny wyeliminowany wiersz cegieł.
Tworzenie metody punktowania
Aby zachować punktację, najpierw musimy dodać metodę do PBGameRenderer, która śledzi punkty gracza. Ta metoda posuwa punktację. Jak wynik będzie zapisany na ekranie? Tworzymy trzy małe wierzchołki sposobami jakie poznałeś wcześniej przy tworzeniu tła, cegieł , piłki i rakietki. Ponieważ przeszliśmy tą procedurę cztery razm, nie będziemy jej tu więcej powtarzać. Twortzymy nową klasę nazwaną PBScoreTile i i umieszczamy jej instamcję w górnym prawym rogu ekranu fry. Następnie dodajemy do projektu nowy spritesheet, który zawiera wszystkie cyfry potrzebne do zbudowania punktacji : Scoring. Każde miejsce powinno domyślnie zaczynać się od odrysowania 0. Jest to możliwe przez wykonanie glTranslatef() dla współrzędnych 0,0,0 w macierzy tekstury
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.0f, 0.0f);
Dla każdego punktu punktacji, używamy glTranslatef() do przesunięcia spritwesheeta do odpowiedniej cyfry. Najlepiej osiągnąć to w pętli , która trwa dodając 1 do bieżącej punktacji, przesunąć 9 z powrotem do 0 jeśli to konieczne, i przejście do kolejnej pozycji. Dlatego przejście punktacji do kolejnej liczby za kadym razem kiedy wywoływana jest metoda przez użycę glTranslatef() przesuwa do kolejnego przyrostu 0,25 w spritesheet. Poniższy przykład jest w pseudokodzie:
private void advanceScore(){
...
gl.glMatrixMode(GL10.GL TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.0f, 0.0f);
...
}
Kiedy mamy już skończoną metodę punktacji, czas ją wywołać
Punktacja za cegłę
Bardzo prosto można pozwolić na punktację za cegłę. Wszystko co trzeba zrobić to zmodyfikować metodę detectCollisions() dla przesunięcia punktacji kiedy cegła wypada z gry. Modyfikacja metody detectedCollisions() przesuwa punktację za każdym razem keidy gracz niszczy cegłę
private void detectCollisions(){
if(ball.posY <= 0){
//GameOver
}
for (int x = 0; x < wall.rows.length; x++)
{
for(int y = 0; y < wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
if (((ball.posY > wall.rows[x].bricks[y].posY - .25f)
&& (ball.posY < wall.rows[x].bricks[y].posY)
&& (ball.posX + .25f > wall.rows[x].bricks[y].posX)
&& (ball.posX < wall.rows[x].bricks[y].posX + 1.50f)))
{
wall.rows[x].bricks[y].isDestroyed = true;
advanceScore();
PBGameVars.ballTargetY = PBGameVars.
ballTargetY * -1f;
if(PBGameVars.ballTargetX == -2f){
PBGameVars.ballTargetX = 5f;
}else{
PBGameVars.ballTargetX = -2f;
}
}
}
}
}
...
}
Aby dodać troche różnorodności do punktacji, możesz również zaimplementować spoób nadawnia jażdej cegle innej wartości punktowej. Najpierw zmodyfikujemy metodę advanceScore() aby akceptowała wartość int reprezentującą liczbę punktów o jaką chcesz przesunąć licznik punktacji. Potem możesz po prostu przekazać brickType zniszczonej cegły jako liczbę punktów , jak pozkazno poniżej:
private void detectCollisions(){
if(ball.posY <= 0){
//GameOver
}
for (int x = 0; x < wall.rows.length; x++)
{
for(int y = 0; y < wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
if (((ball.posY > wall.rows[x].bricks[y].posY - .25f)
&& (ball.posY < wall.rows[x].bricks[y].posY)
&& (ball.posX + .25f > wall.rows[x].bricks[y].posX)
&& (ball.posX < wall.rows[x].bricks[y].posX + 1.50f)))
{
wall.rows[x].bricks[y].isDestroyed = true;
advanceScore(wall.rows[x].bricks[y]. brickType);
PBGameVars.ballTargetY = PBGameVars.
ballTargetY * -1f;
if(PBGameVars.ballTargetX == -2f){
PBGameVars.ballTargetX = 5f;
}else{
PBGameVars.ballTargetX = -2f;
}
}
}
}
}
...
}
Jeśli nie chcesz podliczać gracza za każdą złamaną cegłę, możesz zwiększać punktację za cały wiersz.
Punktacja za wiersz
Kiedy tworzysz klasę PBWall, jednąz funkcji , którą wbudujemy w nią jest możliwość określania liczby cegieł w rzędzie jakie chcesz aby użytkownik połamała. Możesz, teoretycznie, zacząć od setek rzędów i włączyć grę o większej wytrzymałości.. W ten sposób można śledzić liczbę rzędów jakie gracz pomyślnie wykończył. Dodamy nową właściwość numberOfBricksRemaining do PBRow. Ta właściowość śledzi liczbę cegieł w bieżącym rzędzie. Kiedy licznik osiągnie zero, możesz przesunąć licznik,jak poniżej:
public class PBRow {
public PBBrick[] bricks;
public int numberOfBricksRemaining = 0;
public boolean rowIsScored = false;
private Random brickType = new Random();
private boolean isRowOdd = false;
private int numberOfBricks = 0;
public PBRow(int rowNumber){
if(rowNumber 2 > 0)
{
numberOfBricks = 4;
numberOfBricksRemaining = 4;
isRowOdd = true;
}
else
{
numberOfBricks = 5;
numberOfBricksRemaining = 5;
isRowOdd = false;
}
bricks = new PBBrick[numberOfBricks];
for(int x = 0; x < numberOfBricks ; x++)
{
bricks[x] = new PBBrick((int) (brickType.nextFloat() * 7));
if(isRowOdd)
{
bricks[x].posX = x - 2f ;
bricks[x].posY = (rowNumber * .25f) + 1 ;
}
else
{
bricks[x].posX = x - 2.5f;
bricks[x].posY = (rowNumber * .25f) + 1 ;
}
}
}
}
To zanicjuje ustawienie liczby cegieł pozostających w rzędzie na całkowitą liczbę cegieł w rzędzie. Potem, jeśli iterujemy przez metodę wykrywania zderzeń, odejmujemy 1 od tej właściwości dla każdej zniszczonej cegły. Kiedy właściwość osiąga 0, można wywołać metodę punktacji
private void detectCollisions(){
if(ball.posY <= 0){
//GameOver
}
for (int x = 0; x < wall.rows.length; x++)
{
for(int y = 0; y < wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
if (((ball.posY > wall.rows[x].bricks[y].posY- .25f)
&& (ball.posY < wall.rows[x].bricks[y].posY)
&& (ball.posX + .25f > wall.rows[x].bricks[y].posX)
&& (ball.posX < wall.rows[x].bricks[y].posX + 1.50f)))
{
wall.rows[x].bricks[y].isDestroyed = true;
wall.rows[x].numberOfBricksRemaining -=;
PBGameVars.ballTargetY = PBGameVars.
ballTargetY * -1f;
if(PBGameVars.ballTargetX == -2f){
PBGameVars.ballTargetX = 5f;
}else{
PBGameVars.ballTargetX = -2f;
}
}
}
}
if(wall.rows[x]. numberOfBricksRemaining = 0 && wall.
rows[x].rowIsScored == false){
advanceScore();
wall.rows[x].rowIsScored = true;
}
}
...
}
Dodawanie nowych poziomów
Stworzyliśmy funkcjonalną grę zręcznościową. Może być ona uzyta jko szablon dla wielu innych gier, wiedza jaką nabyłeś pomoże ci w tworzeniu innych gier. Jednak nasza gra jest krótka. Pokótce wprowadzimy teorię dodawania nowych poziomów. Są dwa spsoby na dodanie nowych poziomów do gry . Możesz ustawić każdy poziom w grze (poziomowanie statyczne) albo możesz napisać kod gry w taki spoób, że będzie czytał informacje o poziomie z zewnątrz ,zmieniając źródło (poziomowanie dynamiczne)
Statycze dodawanie poziomów
Jeśli dodajemy skończoną liczbę pozioów do gry, tworzymy poziomy statyczne. Wiele z tego co trzeba aby utworzyć dodatkowe poziomyu jest już wbudowane w grę. Po pierwsze powinniśmy stworzyć i skompilować wiele teł dla projektu. Pozwoli to wywoływać nowe tło bezpośrednio z poziomu, który wybiera gracz. Aby uczynć życie łatwiejszym, tworzymy nowe zmienne w PBGameVars aby pomogły wywołać nowe obrazy tła. Następnie tworzymy nowy przycisk dla poziomów w menu. Dodajemy nowy przycisk i nasłuchiwanie dla niego. Kiedy gracz wybierze przycisk odpowiadający poziomowi na jakimchce grać, wywołuje aktywność gry jak robi to przycisk Start; ale również ustawia zmienną w PBGameVars podobnie do te:
PBGameVars.levelSelected = 5;
W PBGameRenderer możesz teraz zmienić ładowanie gry w oparciu o wybrany przez gracza poziom. W metodach ładowania gry możesz wymienić obrazek tła, ładowanie różnych liczb rzędów, lub zmienić piłkę odpowiadającą innemu poziomowi
Dodawanie poziomów dynamicznie
Drugą opcję jest modfyfikowanie kodu gry aby akceptowała dynamiczne tworzenie poziomów. Modyfikacja kodu dla używania dynamicznych poziomów daje graczowi wiele nowych doświadczeń, ponieważ może nieustannie dodawać coś do gry. Kluczem do zrobienia czegoś takiego jest użyci arkusza definicji poziomu opartego o XML. Tworzysz arkusz dewfinicji poziomu, jak na listingu
< prisonbreaklevel>
< levelnumber>5< /levelnumber>
< levelbackground>background5.png< /levelbackground>
< levelwall>
< numberofrows>6< /numberofrows>
< levelwall>
< levelball>
< type>normal< /type>
< / prisonbreaklevel >
który mówi kodowi jak załadować poziom. Potem używamu tego arkusza definicj aby moc tworzyć nieskończoną liczbę poziomów. Ten arkusz może być przechowywany na serwerze sieciowym, lub pobiernay do gry poprzez aktualizację i przechowywany w bazie danych Androida. Patrząc na informacje zawarte w arkuszu, powinno być oczywiste jak kod musiałby być zmodyfikowany w celu zastosowania. Kiedy ładujemy PBGameRenderer, kod otwiera i odczytuje arkusz definicji, który odpowiada poziomowi wybranemu przez gracza. Właściwe opcje arkusza definicji są potem ładowane do gry i tworzony jest poziom





Statystyka generowana przez Reggi-Stat - www.reggi.pl