Hej Flash Developers, välkommen till den andra delen av min Tower Defense Game tutorial. I den första delen utvecklade vi den grundläggande mekanismen för att skapa torn och göra dem skjutna mot musklickets punkt. Men det är inte vad torn är för! I denna del kommer vi att förlänga spelet för att inkludera fiender, grundläggande artificiell intelligens (AI) i torn och några fler spelelement. Är du redo?
Detta är spelet vi ska skapa i denna handledning:
Klicka på orange cirklarna för att placera torn. De röda cirklarna är fiender, och siffran på var och en representerar sina träffpunkter.
I den tidigare handledningen utvecklade vi ett spel som hade platshållare för tornen. Vi kunde distribuera torn genom att klicka på dessa platshållare, och tornen riktade mot muspekaren och skottkulor mot den punkt där användaren klickade.
Vi slutade med a Huvudsaklig
klass som hade spelet loop och spel logik. Bortsett från det hade vi Turret
klass som inte hade något annat än uppdatering
funktion som gjorde tornet roterande.
Vi skapade tidigare kulorna i Huvudsaklig
klass och fäst en ENTER_FRAME
lyssnare att flytta den. Kulan hade inte tillräckligt många egenskaper tidigare för att överväga att göra en separat klass. Men i ett sådant spel kan kulor ha många sorter som hastighet, skada osv. Så det är en bra idé att dra ut kollkoden och inkapslera den i en separat Kula
klass. Vi gör det.
Skapa en ny klass som heter Kula
, förlängning av Sprite
klass. Grundkoden för den här klassen ska vara:
paket import flash.display.Sprite; offentlig klass Bullet utökar Sprite public function Bullet ()
Därefter sätter vi koden för att rita kolumnbilden, taget från Huvudsaklig
, i Kula
. Som vi gjorde med Turret
klass skapar vi en funktion som heter dra
i Kula
klass:
privat funktionstegning (): void var g: Graphics = this.graphics; g.beginFill (0xEEEEEE); g.drawCircle (0, 0, 5); g.endFill ();
Och vi kallar den här funktionen från Kula
konstruktör:
allmän funktion Bullet () draw ();
Nu lägger vi till några egenskaper i kulan. Lägg till fyra variabler: fart
, speed_x
, speed_y
och skada
, Innan Kula
konstruktör:
privat varhastighet: nummer; privat var speed_x: Number; privat var speed_y: Number; allmänhet var skada: int;
Vad är dessa variabler för?
fart
: Denna variabel lagrar kulsens hastighet.speed_x
och speed_y
: Dessa lagrar hastighetens x respektive y-komponenter, så att beräkningen av att hastigheten bryts in i dess komponenter behöver inte göras igen och igen. skada
: Det här är mängden skada som kulan kan göra mot en fiende. Vi håller denna variabel offentlig eftersom vi kommer att kräva detta i vår spelslinga i Huvudsaklig
klass. Vi initierar dessa variabler i konstruktören. Uppdatera din Kula
konstruktör:
allmän funktion Bullet (vinkel: Number) speed = 5; skada = 1; speed_x = Math.cos (vinkel * Math.PI / 180) * hastighet; speed_y = Math.sin (vinkel * Math.PI / 180) * hastighet; dra();
Lägg märke till vinkel
variabel vi får i konstruktören. Detta är riktningen (i grader) där kula kommer att röra sig. Vi bryter bara fart
in i dess x och y komponenter och cache dem för framtida användning.
Det sista som finns kvar i Kula
klassen är att ha en uppdatering
funktion som kommer att ringas från spelslingan för att uppdatera (flytta) kullen. Lägg till följande funktion i slutet av Kula
klass:
public function update (): void x + = speed_x; y + = speed_y;
Bingo! Vi är färdiga med vårt Kula
klass.
Vi flyttade mycket kolumnkod från Huvudsaklig
klass till sin egen Kula
klass, så mycket kod kvarstår oanvänd i Huvudsaklig
och mycket behöver uppdateras.
Först, radera createBullet ()
och moveBullet ()
funktioner. Ta även bort bullet_speed
variabel.
Gå sedan till skjuta
funktionen och uppdatera den med följande kod:
privat funktionskott (e: MouseEvent): tomrum för varje (var turret: Turret i torn) var new_bullet: Bullet = new Bullet (turret.rotation); new_bullet.x = turret.x + Math.cos (turret.rotation * Math.PI / 180) * 25; new_bullet.y = turret.y + Math.sin (turret.rotation * Math.PI / 180) * 25; addChild (new_bullet);
Vi använder inte längre createBullet
funktion för att skapa kula använder snarare Kula
konstruktör och passera tornet rotation
till det som är riktningen för kullens rörelse och så behöver vi inte lagra den i kula rotation
egendom som vi gjorde tidigare. Vi bifogar inte heller någon lyssnare till kullen eftersom kullen kommer att uppdateras från inuti spelet slingan nästa.
Nu när vi behöver uppdatera kulorna från spelslingan behöver vi en hänvisning av dem att lagras någonstans. Lösningen är densamma som för tornen: skapa en ny Array
som heter kulor
och skjut kulorna på det som de är skapade.
Förklara först en matris strax under torn
arraydeklaration:
privat var ghost_turret: Turret; privata vartorn: Array = []; privata var kulor: Array = [];
Nu för att fylla i denna array. Vi gör det när vi skapar en ny kula - så i skjuta
fungera. Lägg till följande innan du lägger till kullen på scenen:
var new_bullet: Bullet = ny Bullet (turret.rotation); new_bullet.x = turret.x + Math.cos (turret.rotation * Math.PI / 180) * 25; new_bullet.y = turret.y + Math.sin (turret.rotation * Math.PI / 180) * 25; bullets.push (new_bullet); addChild (new_bullet);
Precis som hur vi uppdaterar tornen spelet loop, vi kommer att uppdatera kulorna också. Men den här gången, istället för att använda a för varje
loop, vi använder en grundläggande för
slinga. Innan detta måste vi lägga till två variabler överst i spelslingan så att vi vet vilka variabler som används inom spelslingan och kan sätta dem fritt för sopkollektion.
var turret: Turret; var bullet: Bullet;
Fortsätt och lägg till följande kod i slutet av spelet loop:
för (var i: int = kulor. längd - 1; i> = 0; i--) bullet = kulor [i]; om (! bullet) fortsätt; bullet.update ();
Här kryssar vi över alla kulor på scenen varje ram och kallar deras uppdatering
funktion som får dem att röra sig. Notera här att vi iterera kulor
array i omvänd. Varför? Vi ser detta framåt.
Nu när vi har en torn
variabel deklarerad utanför redan behöver vi inte deklarera det igen inuti för varje
loop av torn. Ändra det till:
för varje (turret i torn) turret.update ();
Slutligen lägger vi till gränskontrolltillståndet; Detta var tidigare i kulan ENTER_FRAME
men nu kontrollerar vi det i spelslingan:
om (bullet.x < 0 || bullet.x > stage.stageWidth || bullet.y < 0 || bullet.y > stage.stageHeight) bullets.splice (jag, 1); bullet.parent.removeChild (bullet); Fortsätta;
Vi kontrollerar om kula är borta från scenens gräns, och i så fall tar vi bort sin referens från kulor
array med hjälp av splitsa
funktionen och ta bort kula från scenen och fortsätt med nästa iteration. Så här ser spelet på dig:
privat funktion gameLoop (e: Event): void var turret: Turret; var bullet: Bullet; för varje (turret i torn) turret.update (); för (var i: int = kulor. längd - 1; i> = 0; i--) bullet = kulor [i]; om (! bullet) fortsätt; bullet.update ();
Om du nu kör spelet, ska du ha samma funktion som i Del 1, med kod som är mycket renare och organiserad.
Nu lägger vi till en av de viktigaste elementen i spelet: fienden. Första är att skapa en ny klass som heter Fiende
förlängning av Sprite
klass:
paket import flash.display.Sprite; offentlig klass Enemy utökar Sprite public function Enemy ()
Nu lägger vi till några egenskaper i klassen. Lägg till dem före din Fiende
konstruktör:
privat var speed_x: Number; privat var speed_y: Number;
Vi initierar dessa variabler i Fiende
konstruktör:
Officiell funktion Enemy () speed_x = -1.5; speed_y = 0;
Nästa skapar vi dra
och uppdatering
funktioner för Fiende
klass. Dessa liknar dem mycket från Kula
. Lägg till följande kod:
privat funktionstegning (): void var g: Graphics = this.graphics; g.beginFill (0xff3333); g.drawCircle (0, 0, 15); g.endFill (); public function update (): void x + = speed_x; y + = speed_y;
I vårt spel behöver vi ha många händelser som äger rum vid vissa tillfällen eller upprepade gånger med vissa intervall. Sådan tidpunkt kan uppnås med användning av en tidräknare. Räknaren är bara en variabel som ökar när tiden går i spelet. Det viktiga här är när och av hur mycket belopp som ska öka räknaren. Det finns två sätt på vilka timing i allmänhet görs i spel: tidsbaserad och rambaserad.
Skillnaden är att enheten för steg i tidsbaserat spel är baserat på realtid (dvs antal millisekunder passerade), men i ett rambaserat spel är stegetheten baserat på ramenheter (dvs antalet ramar som passerat).
För vårt spel ska vi använda en rambaserad räknare. Vi har en räknare som vi ökar med en i spelet slingan, som kör varje ram, och så kommer det i grunden att ge oss antalet ramar som har gått sedan spelet startade. Gå vidare och förklara en variabel efter de andra variabla deklarationerna i Huvudsaklig
klass:
privat var ghost_turret: Turret; privata vartorn: Array = []; privata var kulor: Array = []; privat var global_time: Number = 0;
Vi ökar denna variabel i spelet slingan överst:
global_time ++;
Nu bygger vi på denna räknare vi kan göra saker som att skapa fiender, vilket vi gör nästa.
Vad vi vill göra nu är att skapa fiender på fältet efter vartannat sekund. Men vi har att göra med ramar här, kom ihåg? Så efter hur många ramar ska vi skapa fiender? Tja, vårt spel körs vid 30 FPS, vilket ökar global_time
motverka 30 gånger varje sekund. En enkel beräkning berättar att 3 sekunder = 90 ramar.
I slutet av spelet slinga lägg till följande om
blockera:
om (global_time% 90 == 0)
Vad handlar det om? Vi använder modulo (%) operatören, vilket ger resten av en division - så global_time% 90
ger oss resten när global_time
är uppdelad med 90
. Vi kontrollerar om resten är 0
, eftersom detta bara kommer att vara fallet när global_time
är en multipel av 90
- det vill säga villkoret återkommer Sann
när global_time
är lika med 0
, 90
, 180
och så vidare ... På så sätt uppnår vi en utlösare vid var 90: e eller 3 sekunder.
Innan vi skapar fienden, förklara en annan grupp som heter fiender
strax under torn
och kulor
array. Detta kommer att användas för att lagra referenser till fiender på scenen.
privat var ghost_turret: Turret; privata vartorn: Array = []; privata var kulor: Array = []; privata var fiender: Array = []; privat var global_time: Number = 0;
Angiv också en fiende
variabel högst upp i spelslingan:
global_time ++; var turret: Turret; var bullet: Bullet; var fiende: fiende
Slutligen lägg till följande kod inuti om
blockera vi skapade tidigare:
fiende = ny fiend (); fiende.x = 410; enemy.y = 30 + Math.random () * 370; enemies.push (fiende); addChild (fiende);
Här skapar vi en ny fiende, placera den slumpmässigt till höger om scenen, tryck den i fiender
array och lägg till den till scenen.
Precis som vi uppdaterar kulorna i spelsling uppdaterar vi fienderna. Sätt följande kod under tornet för varje
slinga:
för (var j: int = enemies.length - 1; j> = 0; j--) fiende = fiender [j]; enemy.update (); om (fiende.x < 0) enemies.splice(j, 1); enemy.parent.removeChild(enemy); continue;
Precis som vi gjorde en gränskontroll för kulor, kontrollerar vi också för fiender. Men för fiender kontrollerar vi bara om de gick ut från scenens vänstra sida, eftersom de bara rör sig åt höger mot vänster. Du borde se fiender från höger om du kör spelet nu.
Varje fiende har lite liv / hälsa och det kommer också vårt. Vi kommer också att visa den återstående hälsan på fienderna. Låt oss förklara några variabler i Fiende
klass för hälsa saker:
privat var health_txt: TextField; privat var hälsa: int; privat var speed_x: Number; privat var speed_y: Number;
Vi initierar hälsa
variabel i konstruktorn nästa. Lägg till följande i Fiende
konstruktör:
hälsa = 2;
Nu initierar vi hälsotextvariabeln för att visa i centrum av fienden. Vi gör det i dra
fungera:
health_txt = nytt TextField (); health_txt.height = 20; health_txt.width = 15; health_txt.textColor = 0xffffff; health_txt.x = -5; health_txt.y = -8; health_txt.text = hälsa + ""; addChild (health_txt);
Allt vi gör är att skapa en ny Textfält
, Ställ in sin färg, placera den och sätt dess text till det aktuella värdet av hälsa
Slutligen lägger vi till en funktion för att uppdatera fiendens hälsa:
public function updateHealth (mängd: int): int hälsa + = mängd; health_txt.text = hälsa + ""; återvända hälsa;
Funktionen accepterar ett heltal för att lägga till hälsan, uppdaterar hälsoteksten och returnerar den slutliga hälsan. Vi ringer den här funktionen från vår spelslinga för att uppdatera varje fiendens hälsa och upptäcka om den fortfarande lever.
Först kan vi ändra vår skjuta
fungera lite. Ersätt det befintliga skjuta
funktion med följande:
privata funktionsskott (turret: Turret, fiende: Enemy): void varvinkel: Number = Math.atan2 (enemy.y - turret.y, enemy.x - turret.x) / Math.PI * 180; turret.rotation = vinkel; var new_bullet: Bullet = ny Bullet (vinkel); new_bullet.x = turret.x + Math.cos (turret.rotation * Math.PI / 180) * 25; new_bullet.y = turret.y + Math.sin (turret.rotation * Math.PI / 180) * 25; bullets.push (new_bullet); addChild (new_bullet);
De skjuta
funktionen accepterar nu två parametrar. Den första är en hänvisning till ett torn som kommer att göra skjutningen; den andra är en hänvisning till en fiende mot vilken den kommer att skjuta.
Den nya koden här liknar den som finns i Turret
klassens uppdatering
funktion, men istället för musens position använder vi nu fiendens cordinates. Så nu kan du ta bort all kod från uppdatering
funktion av Turret
klass.
Nu hur man gör tornen skjuta på fiender? Jo logiken är enkel för vårt spel. Vi gör alla tornen skjuta den första fienden i fiender
array. Vad? Låt oss ange en kod och försöka förstå. Lägg till följande rader i slutet av för varje
slinga som används för att uppdatera tornen:
för varje (turret i torn) turret.update (); för varje (fiende i fiender) skott (torn, fiende); ha sönder;
För varje torn uppdaterar vi nu den, sedan iterera fiender
array, skjuta den första fienden i array och bryta från loopen. Så väsentligen skottar varje torn den tidigast skapade fienden som den alltid är i början av matrisen. Försök att köra spelet och du bör se torn som skjuter fienderna.
Men vänta, vad stämmer den här kulan? Ser ut att de skjuter för fort. Låt oss se varför.
Som vi vet går spelslingan varje ram, det vill säga 30 gånger i sekundet i vårt fall, så den skytteförklaring vi lade till i föregående steg kallas i takt med vår spelslinga och därmed ser vi en ström av kulor som flyter. Det verkar som om vi behöver en tidsmekanism inuti tornen också. Byt till Turret
klass och lägg till följande kod:
privat var local_time: Number = 0; privat var reload_time: int;
lokal tid
: Vår räknare heter lokal tid
i motsats till global_time
i Huvudsaklig
klass. Detta är av två anledningar: För det första, eftersom denna variabel är lokal för Turret
klass; För det andra, för det går inte alltid fram som vår global_time
variabel - det kommer att återställas många gånger under spelet. omladdningstid
: Det här är den tid som tornet behöver för att ladda om efter att ha tagit en kula. I grund och botten är det tidsskillnaden mellan två kula skott av en torn. Kom ihåg att alla tidsenheter i vårt spel gäller ramar. Öka lokal tid
variabel i uppdatering
funktion och initiera omladdningstid
i konstruktören:
public function update (): void local_time ++;
allmän funktion Turret () reload_time = 30; dra();
Lägg sedan till följande två funktioner i slutet av Turret
klass:
allmän funktion isReady (): Boolean return local_time> reload_time; återställning av allmän funktion (): void local_time = 0;
är redo
returnerar sant endast när det aktuella lokal tid
är större än omladdningstid
, d.v.s. när tornet har laddats om. Och den återställa
funktionen återställer helt enkelt lokal tid
variabel, för att starta omladdning igen.
Nu tillbaka i Huvudsaklig
klass, ändra skjutkoden i spelet slingan vi lade till i föregående steg till följande:
för varje (turret i torn) turret.update (); om (! turret.isReady ()) fortsätter; för varje (fiende i fiender) skott (torn, fiende); turret.reset (); ha sönder;
Så om nu är tornet inte klart (är redo()
avkastning falsk
) fortsätter vi med nästa iteration av turret slingan. Du kommer att se att tornen eldar med ett intervall på 30 bilder eller 1 sekund nu. Häftigt!
Fortfarande något inte rätt. Tornen skjuter på fiender oavsett avståndet mellan dem. Vad som saknas här är räckvidd av ett torn. Varje torn bör ha sitt eget sortiment inom vilket det kan skjuta en fiende. Lägg till en annan variabel till Turret
klass kallas räckvidd
och ställ den till 120
inuti konstruktören:
privat var reload_time: int; privat var local_time: Number = 0; privat varavstånd: int;
allmän funktion Turret () reload_time = 30; intervall = 120; dra();
Lägg också till en funktion som heter canShoot
i slutet av klassen:
Officiell funktion canShoot (fiende: Enemy): Boolean var dx: Number = enemy.x - x; var dy: Nummer = fiende.y - y; om (Math.sqrt (dx * dx + dy * dy) <= range) return true; else return false;
Varje torn kan bara skjuta en fiende när den uppfyller vissa kriterier. Du kan till exempel låta tornet bara skjuta röda fiender med mindre än hälften av livet och inte mer än 30 px borta. All sådan logik för att avgöra om tornet kan skjuta en fiende eller inte kommer att gå in i canShoot
funktion som returnerar Sann
eller falsk
enligt logiken.
Vår logik är enkel. Om fienden är inom räckvidden Sann
; returnera annars falskt. Så när avståndet mellan tornet och fienden (Math.sqrt (dx * dx + dy * dy)
) är mindre än eller lika med räckvidd
, det återvänder Sann
. Lite mer modifiering i spelsegmentet i spelet loop:
för varje (turret i torn) turret.update (); om (! turret.isReady ()) fortsätter; för varje (fiende i fiender) if (turret.canShoot (fiende)) shoot (turret, fiende); turret.reset (); ha sönder;
Nu är det bara om fienden är inom tornet, kommer tornet att skjuta.
En mycket viktig del av varje spel är kollisionsdetektering. I vårt spel kollisionskontroll görs mellan kulor och fiender. Vi lägger till kollisionsdetekteringskoden inuti för varje
slinga som uppdaterar kulorna i spelslingan.
Logiken är enkel. För varje kula korsar vi fiender
array och kontrollera om det finns en kollision mellan dem. Om så är fallet tar vi bort kula, uppdaterar fiendens hälsa och bryter ut ur slingan för att kontrollera andra fiender. Låt oss lägga till en kod:
för (i = kullar. längd - 1; i> = 0; i--) kula = kula [i]; // om kulan inte är definierad fortsätter du med nästa iteration om (! bullet) fortsätter; bullet.update (); om (bullet.x < 0 || bullet.x > stage.stageWidth || bullet.y < 0 || bullet.y > stage.stageHeight) bullets.splice (jag, 1); bullet.parent.removeChild (bullet); Fortsätta; för (var k: int = enemies.length - 1; k> = 0; k--) fiende = fiender [k]; om (bullet.hitTestObject (fiende)) bullets.splice (jag, 1); bullet.parent.removeChild (bullet); om (fiende.updateHealth (-1) == 0) enemies.splice (k, 1); enemy.parent.removeChild (fiende); ha sönder;
Vi använder ActionScript hitTestObject
funktion för att kontrollera kollision mellan kula och fiende. Om kollisionen uppträder, avlägsnas kulan på samma sätt som när den lämnar scenen. Fiendens hälsa uppdateras sedan med hjälp av updateHealth
metod, till vilken kula
's skada
egendom är godkänd. Om updateHealth
funktionen returnerar ett heltal mindre än eller lika med 0
, det betyder att fienden är död och så tar vi bort det på samma sätt som kulan.
Och vår kollisionsdetektering är klar!
Kom ihåg att vi korsar fienderna och kulorna i omvänd ordning i vår spelslinga. Låt oss förstå varför. Låt oss anta att vi använde en stigande för
slinga. Vi är på index i = 3
och vi tar bort en kula från matrisen. Vid borttagning av föremålet vid position 3
, dess utrymme fylls av objektet då i position 4
. Så nu föremålet tidigare i position 4
är på 3
. Efter iterationen jag
steg med 1
och blir 4
och så objekt på plats 4
är kontrollerad.
Oj, ser du vad som hände just nu? Vi missade bara objektet nu på plats 3
som skiftes tillbaka som ett resultat av splicing. Och så använder vi en omvänd för
slinga som tar bort detta problem. Du kan se varför.
Låt oss lägga till några extra saker för att få spelet att se bra ut. Vi lägger till funktionalitet för att visa ett tornets intervall när musen svävar på den. Byt till Turret
klass och lägg till några variabler till den:
privat varavstånd: int; privat var reload_time: int; privat var local_time: Number = 0; privat var kropp: Sprite; privat var range_circle: Sprite;
Nästa uppdatering av dra
funktion till följande:
privatfunktionsdrag (): void range_circle = new Sprite (); g = range_circle.graphics; g.beginFill (0x00D700); g.drawCircle (0, 0, intervall); g.endFill (); range_circle.alpha = 0.2; range_circle.visible = false; addChild (range_circle); body = new Sprite (); var g: Graphics = body.graphics; g.beginFill (0xD7D700); g.drawCircle (0, 0, 20); g.beginFill (0x800000); g.drawRect (0, -5, 25, 10); g.endFill (); addChild (kropp);
Vi bryter tornets grafik i två delar: kroppen och intervallet grafik. Vi gör detta för att ge beställning till de olika delarna av tornet. Här behöver vi range_circle
att ligga bakom tornets kropp, och så lägger vi först till scenen. Slutligen lägger vi till två muslyttare för att växla intervallet grafik:
privat funktion onMouseOver (e: MouseEvent): void range_circle.visible = true; privat funktion onMouseOut (e: MouseEvent): void range_circle.visible = false;
Fäst nu lyssnarna på respektive händelser i slutet av konstruktören:
body.addEventListener (MouseEvent.MOUSE_OVER, onMouseOver); body.addEventListener (MouseEvent.MOUSE_OUT, onMouseOut);
Om du kör spelet och försöker distribuera ett torn, kommer du att se en flimmer när du svävar på platshållarna. Varför är det så?
Kom ihåg att vi ställer in mouseEnabled
ghosttornets egenskap till falsk
? Vi gjorde det för att spöketornet fångade mushändelser genom att komma in mellan musen och platshållaren. Samma situation har kommit igen eftersom tornet själv har två barn nu - dess kropp och spridningsintervallet - som fångar mushändelserna emellan.
Lösningen är densamma. Vi kan ställa in deras individer mouseEnabled
egenskaper till falsk
. Men en bättre lösning är att ställa in spök tornet mousechildren
egendom till falsk
. Vad det här gör är att begränsa alla spökdräktens barn från att ta emot mushändelser. Snyggt va? Gå vidare och sätt på det falsk
i Huvudsaklig
konstruktör:
ghost_turret = new Turret (); ghost_turret.alpha = 0.5; ghost_turret.mouseEnabled = false; ghost_turret.mouseChildren = false; ghost_turret.visible = false; addChild (ghost_turret);
Problemet löst.
Vi kan förlänga denna demo för att inkludera mycket mer avancerade funktioner och göra det till ett spelbart spel. Vissa av dessa kan vara:
Låt oss se vad du kan komma med från denna grundläggande demo. Jag kommer vara glad att höra om dig tornförsvarsspel, och dina kommentarer eller förslag till serien.