V tomto diely série tutoriálov na konzolovú hru Had sa pozrieme na tvorbu logiky, ovládania a vykresľovanie samotného hada.
Úvod
Dnes si vytvoríme postavu hada, čo bude oproti predošlým dielom mierne zložitejšie. Budeme tiež potrebovať prototyp prvej scény v hre. V hre budeme mať niekoľko tried, ktoré nazveme scény a budú slúžiť na jednoduché prepínanie medzi časťami hry (ako hlavná hra, menu, pauza…).
Scéna
Prvá scéna (ja ju mám pomenovanú GameScene) bude fungovať ako hlavná obrazovka hry. Scény ukladám do zložky Scenes, čo pomáha sprehľadniť kód. Každá scéna bude mať v sebe metódy Update a Draw, čo sú hlavné metódy používané v celej hre.
Zatiaľ do tejto triedy dajte len jednu premennú a to bool MultiPlayer = false. Táto bude určovať či sa jedná o hru dvoch hráčov.
Had
Enumerácie
Teraz sa presunieme k triede Snake, ktorá bude slúžiť ako ústredná postava našej hry. Táto trieda bude obsahovať dva enumeračné objekty, pričom jeden bude určovať stranu a druhý príčinu smrti.
public enum Side { Left, Right, Up, Down }; public enum DeathCause { CollisionSelf, CollisionOppenent, CollisionWall, TooShort };
Premenné, vlastnosti a definície eventov
Naša trieda bude obsahovať niekoľko premenných, ktoré budeme používať. Ako prvú definujeme premennú Parent, ktorá bude typu GameScene a bude obsahovať práve objekt hernej scény. Toto bude dôležité na zistenie nastavení hry.
Ako ďalší pridáme zoznam miest na ktorých je chvost – List<Vector2> Tail, pozíciu hlavy hada – Vector2 Position; Časovač – int Timer; smer, ktorým práve ideme – Side Direction; smer, ktorým pôjdeme nabudúce – Side NextDirection; informáciu o tom, či si nechávame chvost – bool LeaveTail a informáciu, či sme hráč jedna – bool playerOne.
Ďalej potrebujeme premenné typu int – Speed = 1 a NeedToComplete = 50.
Ako poslednú premennú pridáme slovník obsahujúci ovládanie – Dictionary<Side, ConsoleKey> controls.
GameScene Parent; private List<Vector2> Tail = new List<Vector2>(); private Vector2 Position; private int Timer = 0; private Side Direction; private Side NextDirection; private bool LeaveTail = false; private bool playerOne; public int Speed = 1; public int NeedToComplete = 50; public Dictionary<Side, ConsoleKey> controls = new Dictionary<Side, ConsoleKey>();
Okrem toho budeme potrebovať sedem eventov. Died, ktorého argumenty budú typu DeathCause; GotLive; GotScore – argumenty typu int a Complete. Ďalšie tri eventy budú mať argumenty typu Vector2 – AtePoint, AteLive, AtePoison.
public event EventHandler<DeathCause> Died; public event EventHandler GotLive; public event EventHandler<int> GotScore; public event EventHandler Complete; public event EventHandler<Vector2> AtePoint; public event EventHandler<Vector2> AtePoision; public event EventHandler<Vector2> AteLive;
A na záver potrebujeme jednu vlastnosť typu int – Size. Tá vráti veľkosť premennej Tail alebo pridá/odstráni chvosty.
public int Size { get { return Tail.Count; } set { while (value < Tail.Count) { Tail.RemoveAt(0); } while (value > Tail.Count + 1) { Vector2 pos = new Vector2(Position.X, Position.Y); switch (NextDirection) { case Side.Down: pos.Y--; break; case Side.Up: pos.Y++; break; case Side.Left: pos.X++; break; case Side.Right: pos.X--; break; } Tail.Add(pos); } } }
Pomocné metódy
Teraz si vytvoríme tri pomocné metódy. Prvé dve z nich budú určené na kontrolu kolízií – jedna z argumentom typu Vector2 a druhá Rectangle. Obe budú mať návratovú hodnotu typu bool.
Metódy najprv porovnajú kolíziu z hlavou hada a potom z každým bodom chvosta. Porovnávanie triedy Vector2 prebehne pomocou klasického použitia znamienok rovnosti a pri triede Rectangle použijeme metódu Intersects.
public bool CheckCollisionFull(Vector2 pos) { if (pos == Position) return true; foreach (Vector2 position in Tail) { if (position == pos) return true; } return false; } public bool CheckCollisionFull(Rectangle rec) { if (rec.Intersects(Position)) return true; foreach (Vector2 position in Tail) { if (rec.Intersects(position)) return true; } return false; }
Druhá metóda bude slúžiť na ovládanie hada a bude mať argument typu ConsoleKeyInfo. Keďže máme zoznám kláves ovládania prepojený na enumeráciu Side, môžeme ľahko kontrolovať ovládanie.
Vždy kontrolujeme, či je stlačené tlačidlo a zároveň sa nepohybujeme v protismere. Vďaka tomu sa vždy budeme môcť otočiť len o 90°.
public void Control(ConsoleKeyInfo pressed) { if ((Direction != Side.Up) && (pressed.Key == controls[Side.Down])) NextDirection = Side.Down; else if ((Direction != Side.Down) && (pressed.Key == controls[Side.Up])) NextDirection = Side.Up; else if ((Direction != Side.Left) && (pressed.Key == controls[Side.Right])) NextDirection = Side.Right; else if ((Direction != Side.Right) && (pressed.Key == controls[Side.Left])) NextDirection = Side.Left; }
Konštruktor
Následne si pripravíme konštruktor triedy. Ten bude mať osem argumentov – rodičovskú triedu GameScene parent, informáciu, či sme hráč jedna – bool playerOne, štartovaciu pozíciu – Vector2 StartPosition, potrebnú veľkosť na splnenie kola – int neededSize, počiatočnú rýchlosť – int startSpeed, počiatočnú veľkosť – int startSize, počiatočný smer – Side startDirection a slovník ovládania – Dictionary<Side, ConsoleKey> controls.
Konštruktor podľa potreby priradí všetky premenné a simuluje chvost hada.
public Snake(GameScene Parent, bool PlayerOne, Vector2 startPosition, int neededSize, int startSpeed, int startSize, Side startDirection, Dictionary<Side, ConsoleKey> controls) { this.Parent = Parent; this.controls = controls; playerOne = PlayerOne; Position = new Vector2(startPosition.X, startPosition.Y); Speed = startSpeed; Direction = startDirection; NextDirection = startDirection; NeedToComplete = neededSize; for (int i = startSize; i > 1; i--) { Vector2 pos = new Vector2(startPosition.X, startPosition.Y); switch(startDirection) { case Side.Left: pos.X += (i-1); break; case Side.Right: pos.X -= (i - 1); break; case Side.Up: pos.Y += (i - 1); break; case Side.Down: pos.Y -= (i - 1); break; } Tail.Add(pos); } }
Poznámka : Vytvorenie nového objektu typu Vector2 je pre Position nevyhnutné – pri manipulácií z argumentom pri simulácií chvostu by došlo k zmene pôvodných údajov.
Metóda Update
Nasleduje štandardná metóda Update, ktorá bude mať päť argumentov. Tri z nich budú zoznamy typu Vector2 – Lives, Points a Poisons. Okrem toho bude mať jeden zoznam typu Rectangle – Colliders a druhý objekt hada Snake oponnent označujúci druhého hráča.
Na začiatku metódy inkrementujeme Timer a začneme podmienku Timer==Speed. To spôsobí čakanie pri nižšej rýchlosti. Všetko ďalšie metódy Update sa bude odohrávať vnútri tejto podmienky.
Ako prvé nulujeme Timer a urobíme si kópiu premennej Poisition. Túto pridáme do zoznamu chvostov. Tiež si skopírujeme nasledujúci smer do aktuálneho smeru.
Timer = 0; Vector2 pos = new Vector2(Position.X, Position.Y); Tail.Add(pos); Direction = NextDirection;
Podľa aktuálneho smeru posunieme pozíciu hada.
switch (Direction) { case Side.Left: Position.X -= 1; break; case Side.Right: Position.X += 1; break; case Side.Up: Position.Y -= 1; break; case Side.Down: Position.Y += 1; break; }
Teraz si vytvoríme z pozície hlavy nový Rectangle a podľa podmienky LeaveTail ponecháme alebo odstránime chvost. Ak je jej hodnota true, nastavíme ju na false.
Rectangle head = ((Rectangle)Position); if (!LeaveTail) { if (Tail.Count > 0) Tail.RemoveAt(0); } else LeaveTail = false;
Teraz sú na rade kolízie. Najprv skontrolujeme kolízie zo všetkými bodmi.
Ak sa jedná o život, pridáme 1 život a zavoláme eventy AteLive s argumentom bodu z ktorým sme kolidovali a potom ešte zavoláme event GotLive.
Ak sa jedná o klasické bod, zavoláme event AtePoint, ktorého argument opäť bude bod. Ďalej zavoláme event GotScore, ktorého argument bude číslo 100.
Ak sa jedná o jed, zavoláme event AtePoison z argumentom kolidujúceho bodu. Ďalej zavoláme event GotScore s argumentom -100. Po tomto skontrolujeme, či je chvost dlhší ako 0. Ak áno, zmenšíme ho o jedna, inak zavoláme event Died s argumentom DeathCause.TooShort.
foreach (Vector2 live in Lives) { if (head.Intersects(live)) { if (AteLive != null) AteLive(this, live); if (GotLive != null) GotLive(this, null); } } foreach (Vector2 point in Points) { if (head.Intersects(point)) { LeaveTail = true; if (AtePoint != null) AtePoint(this, point); if (GotScore != null) GotScore(this, 100); } } foreach (Vector2 poison in Poisons) { if (head.Intersects(poison)) { if (AtePoision != null) AtePoision(this, poison); if (GotScore != null) GotScore(this, -100); if (Tail.Count > 0) Tail.RemoveAt(0); else if (Died != null) Died(this, DeathCause.TooShort); } }
Podobne skontrolujeme aj kolízie zo stenami, pričom ak kolízia bude zavoláme event Died s argumentom DeathCause.CollisionWall.
foreach (Rectangle collider in Colliders) { if (head.Intersects(collider)) { if (Died != null) Died(this, DeathCause.CollisionWall); } }
To isté spravíme aj z kolíziou s vlastným chvostom a spoluhráčom, ak sme v multiplayeri.
foreach (Vector2 tail in Tail) { if (head.Intersects(tail)) { if (Died != null) Died(this, DeathCause.CollisionSelf); break; } } if (Parent.MultiPlayer && playerOne && (oponnent.CheckCollisionFull(Position))) { if (Died != null) Died(this, DeathCause.CollisionOppenent); }
Teraz ešte spravíme presun na druhú stranu obrazovky, ak sme ju presiahli. Pri vertikálnom posune si musíme dávať pozor na horné dva riadky, ktoré budú zobrazovať HUD.
if (Position.X >= Renderer.WindowSize.X) Position.X = 0; else if (Position.X < 0) Position.X = Renderer.WindowSize.X; if (Position.Y >= Renderer.WindowSize.Y) Position.Y = 2; else if (Position.Y < 2) Position.Y = Renderer.WindowSize.Y;
A na záver skontrolujeme, či sme dosiahli dostatočnú dĺžku hada. Ak hrajú dvaja hráči, overuje to len had hráča 1 a v tom prípade je nutné dosiahnuť dvojnásobnú dĺžku. Dĺžka oboch hráčov sa spočítava. Po dosiahnutí je zavolaný event Complete.
if (playerOne) { if (!Parent.MultiPlayer) { if (Tail.Count >= NeedToComplete) if (Complete != null) Complete(this, null); } else { if (Tail.Count+oponnent.Tail.Count >= (NeedToComplete*2)) if (Complete != null) Complete(this, null); } }
Draw metóda
Teraz už len stačí napísať metódu Draw, ktorá bude pomerne krátka.
Na začiatku vykreslíme chvost (žltou farbou, ak sme hráč jedna, inak tyrkysovou)
foreach (Vector2 pos in Tail) { if (playerOne) Renderer.DrawPoint(pos, ConsoleColor.Yellow); else Renderer.DrawPoint(pos, ConsoleColor.Cyan); }
Podoboným spôsobom vykreslíme aj hlavu hada – farby: tmavožltá alebo modrá.
if (playerOne) Renderer.DrawPoint(Position, ConsoleColor.DarkYellow); else Renderer.DrawPoint(Position, ConsoleColor.Blue);
Na hlavu podľa smeru hada ešte vykreslíme oči alebo ústa tmavočervenou farbou.
if (Direction == Side.Up) Renderer.DrawString("▲", Position, ConsoleColor.DarkRed); else if (Direction == Side.Down) Renderer.DrawString("▼", Position, ConsoleColor.DarkRed); else if (Direction == Side.Left) Renderer.DrawString(":", Position, ConsoleColor.DarkRed); else if (Direction == Side.Right) Renderer.DrawString(":", Position, ConsoleColor.DarkRed);
Tým sme napísali celú triedu pre postavu Snake a pred prácami na triede GameScene sa nabudúce vrhneme na prácu zo súbormi.