Grafikus C# – Windows Forms tanfolyam › 5.9 lecke

Windows Forms · 5.9 · záró mini projekt

Mini projekt: billentyűzettel vezérelt program – kígyó játék

Az 5. fejezet záró projektjében minden összekapcsolódik, amit az eseményekről tanultál: a billentyűzet (ProcessCmdKey), az egér, a változás-események (ValueChanged, CheckedChanged), az ablak életciklusa (Load, Deactivate, FormClosing, FormClosed, Settings) és a közös eseménykezelő. A program egy klasszikus kígyó játék: a kígyót a nyilakkal vagy a WASD billentyűkkel irányítod, ételt eszel, nő a hosszod és a sebességed. A játék kitűnő gyakorlat az állapotkezelésre is.

  • kb. 4 óra
  • haladó szint
  • záró projekt
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A projekt végére

  • állapotgépként modellezel egy játékot (készen áll, fut, szünet, vége);
  • a ProcessCmdKey segítségével a fókusztól függetlenül kezeled a nyilakat és a gyorsbillentyűket;
  • időzítővel (Timer) léptetsz, és a sebességet futás közben változtatod;
  • a játékteret Bitmap-ra rajzolod (4.5 lecke), villogás nélkül;
  • közös kezelővel kezeled a nehézségi gombokat, és a Settings segítségével megőrzöd a rekordot;
  • az ablak életciklusához kapcsolod a játékot (automatikus szünet, kilépés megerősítése).

Mielőtt elkezded

Ehhez a projekthez az 5.1–5.8 lecke és a 4.5 lecke (rajzolás Bitmap-ra) szükséges. Új Windows Forms projektet készíts, és tarts nyitva mellé egy-két korábbi leckét: itt a megoldásokat összerakod, nem újra felfedezed őket.

5.8 Közös eseménykezelő →

Terv: követelmények és állapotgép

RészKövetelményPont
Játékmenet (35)A kígyó időzítőre lép a 20 × 20-as pályán; a fej mozog, a farok követi.12
Étel véletlen helyen (sosem a kígyó testén); evéskor nő a kígyó, nő a pontszám.12
A játék véget ér, ha a kígyó a falnak vagy önmagának ütközik; a falátlépés opció (chkAtlep) a falat „átjárhatóvá” teszi.11
Vezérlés (25)Irányítás nyilakkal és WASD billentyűkkel, a fókusztól függetlenül; a kígyó nem fordulhat vissza 180 fokban, még gyors billentyűnyomásnál sem.10
Enter: új játék; Szóköz: szünet/folytatás; az egér bal gombja a pályán ugyanezt teszi.8
A sebesség (1–8) állítható futás közben (NumericUpDown), és három nehézségi választógomb (Könnyű, Közepes, Nehéz) közös kezelővel állítja be.7
Életciklus és adatok (25)A rekord megmarad a program újraindítása után (Settings).9
Ha az ablak elveszíti a fókuszt, a játék szünetel; a bezáráskor futó játéknál megerősítés kérdés jön (csak felhasználói bezárásnál).9
Állapotjelző feliratok (pont, rekord, állapot), a pályán felirat szünetnél és játék végén.7
Kód és szerkezet (15)Beszédes nevek, rövid metódusok, a játék állapota jól elkülönítve; nincs ismétlődő kód.15

Összesen: 100 pont.

Az állapotgép

A játék négy állapotban lehet, és a billentyűk hatása az állapottól függ. Ezt érdemes előre, papíron megtervezni (4.4 lecke):

ÁllapotMit jelent?EnterSzóközNyilak / WASDIdőzítő
Készen állA program indulása; még nincs játék.Új játék → Futnem csinál semmitnem csinál semmitáll
FutA kígyó mozog.Új játék (újraindítás)→ Szünetirányt váltfut
SzünetSzüneteltetve (kézzel vagy fókusvesztés miatt).Új játék→ Futnem csinál semmitáll
VégeA kígyó ütközött.Új játék → Futnem csinál semmitnem csinál semmitáll

Gondold végig!

A játék állapotát két bool jelzővel (fut, vege) vagy egyetlen enum értékkel is megadhatod. Melyik megoldás hibatűrőbb, és miért?

Egy lehetséges válasz

Az enum (Allapot { KeszenAll, Fut, Szunet, Vege }) hibatűrőbb: egyszerre pontosan egy állapot lehet, nincs értelmetlen kombináció. Két bool négy kombinációt enged (a fut és a vege egyszerre igaz is lehet, ami nem lehetséges állapot), és minden új állapot (például visszaszámlálás) újabb jelzőt és újabb feltételeket kíván. Az enum (felsorolás típus) a tanfolyam későbbi részében kerül sorra, ezért ebben a projektben az egyszerűség kedvéért három bool jelzőt használunk. Ha az enum már ismerős, bátran használd helyettük.

A felület

Vezérlő(Name)Beállítás
FormForm1Text: Kígyó; FormBorderStyle: FixedSingle; MaximizeBox: False; StartPosition: CenterScreen
PictureBoxpbPalyaSize: 400 × 400; SizeMode: Normal; TabStop: False
Label ×3lblPont, lblRekord, lblAllapotpontszám, rekord, állapot (pl. „Szünet”)
NumericUpDownnudSebessegMinimum: 1; Maximum: 8; Value: 4
RadioButton ×3rbKonnyu, rbKozepes, rbNehezTag: 2, 4, 7 (a kezdősebesség); a rbKozepes bejelölt; mindhárom a Nehezseg_CheckedChanged kezelőt hívja
CheckBoxchkAtlepText: Falátlépés
TimertmrJatekInterval: 150 (a kód állítja); Enabled: False

A beállítás (Settings)

Add hozzá a Properties → Settings.settings fájlhoz a Rekord beállítást (int, User hatókör, alapérték: 0), ahogyan az 5.6 leckében láttad.

📷 Képernyőkép helye

Mit tegyél ide: a tervezőnézet: a pálya a bal oldalon, jobbra a pont, a rekord, az állapot, a sebesség, a nehézségi választógombok és a falátlépés jelölőnégyzet.Javasolt fájlnév: winforms-5-9-felulet.webp · Alt szöveg: A kígyó játék felülete a tervezőben

Építés lépésenként

A projektet kis, egymásra épülő lépésekben érdemes megépíteni: minden lépés után indítsd el, és ellenőrizd, hogy működik. Az egyes lépéseknél csak a feladatot és a támpontot látod, a teljes kód a következő szakaszban van.

1. lépés: a pálya és a kígyó kirajzolása (állóképként)

Feladat: A Form1_Load hozzon létre egy 400 × 400-as Bitmap-ot, és jelenítse meg a pbPalya vezérlőben. Egy Rajz() metódus fesse sötétre a hátteret, és rajzoljon három cellányi kígyót (List<Point>) és egy ételt (kör) a megadott cellákon.

Támpont: A cella mérete 20 képpont. Az (x, y) cella bal felső sarka (x * 20, y * 20). A rajzolás után hívd az pbPalya.Invalidate() metódust. A Graphics és az ecset using blokkban éljen.

2. lépés: léptetés időzítővel

Feladat: A tmrJatek_Tick a kígyót az irany irányában egy cellával mozgassa: új fej a lista elejére (Insert(0, ...)), a farok eltávolítása (RemoveAt). Indítsd a játékot az Enter billentyűvel (később).

Támpont: A fej kigyo[0]. Az új fej: new Point(fej.X + irany.X, fej.Y + irany.Y).

3. lépés: irányítás

Feladat: Írd felül a ProcessCmdKey metódust: a nyilak és a WASD irányt váltanak, de csak futó játéknál. A 180 fokos visszafordulás tilos.

Támpont: Két irányváltozó kell: az irany (amerre éppen mozog) és a kovetkezoIrany (amit a billentyű kért). A tick elején irany = kovetkezoIrany. A visszafordulás ellenőrzése az aktuális irany-hoz képest: ha a két irány összege nulla vektor (irany.X + dx == 0 && irany.Y + dy == 0), tiltsd.

4. lépés: étel, evés, növekedés

Feladat: Az EtelHelyez() véletlen, szabad cellára teszi az ételt. Evéskor a farok nem törlődik (a kígyó nő), a pontszám nő, és új étel jön.

Támpont: do { ... } while (kigyo.Contains(p)); ciklus keres szabad cellát. Evéskor ne hívd a RemoveAt metódust.

5. lépés: ütközés és játék vége

Feladat: Fal- és önmagába ütközés esetén a játék véget ér (Vege()): az időzítő leáll, a rekord frissül, a pályán felirat jelenik meg. A chkAtlep bejelölt állapotában a fal átjárható (a kígyó a túloldalon jön ki).

Támpont: Átlépésnél a koordinátát a pálya méretével modulozd: (x + 20) % 20 (a +20 a negatív érték miatt kell). Önmagába ütközésnél a farok éppen elmozduló végét ne számítsd ütközésnek, ha a kígyó nem eszik.

6. lépés: állapotok, szünet, egér

Feladat: A Szóköz és a pályán a bal egérgomb szünetet vált. Az Enter új játékot indít. A Deactivate esemény szünetelteti a játékot. A pályán szünetnél és játék végén felirat látszik.

Támpont: A szünet az időzítő leállítása és indítása. A feliratot a Graphics.DrawString rajzolja.

7. lépés: sebesség és nehézség

Feladat: A nudSebesseg értéke határozza meg az időzítő intervallumát (240 - szint * 25), futás közben is. A három választógomb közös kezelője a Tag értékét tölti a nudSebesseg-be.

Támpont: A két vezérlő egymást is változtatja, ezért kell a frissites jelző (5.5 lecke). A RadioButton.CheckedChanged kétszer fut: vizsgáld a Checked értéket.

8. lépés: rekord és bezárás

Feladat: A rekord a Load-ban betöltődik, a FormClosed és a Vege() menti (Save()). A FormClosing futó játéknál megerősítést kér (csak UserClosing esetén); Nem válasznál a játék szüneteljen és az ablak maradjon.

Támpont: A kérdés előtt állítsd szünetre a játékot, hogy ne fusson a kérdés alatt.

Mintamegoldás

Előbb próbáld meg önállóan!

A mintamegoldás csak az ellenőrzésre való. Egy játékot sokféleképpen meg lehet írni, a tiéd lehet más is. Először a lépéseket próbáld meg a támpontok alapján, és csak elakadásnál nézd meg a kódot.

Megoldás: Form1.cs (teljes kód)
Form1.csC#
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace Kigyo
{
public partial class Form1 : Form
{
private const int Cella = 20;
private const int Meret = 20; // a pálya 20 x 20 cella

private readonly List<Point> kigyo = new List<Point>();
private Point irany = new Point(1, 0);
private Point kovetkezoIrany = new Point(1, 0);
private Point etel;
private readonly Random veletlen = new Random();
private Bitmap kep;

private int pont = 0;
private int rekord = 0;
private bool fut = false;
private bool szunet = false;
private bool vege = false;
private bool frissites = false;

public Form1()
{
InitializeComponent();
}

// ---- életciklus ----

private void Form1_Load(object sender, EventArgs e)
{
kep = new Bitmap(Meret * Cella, Meret * Cella);
pbPalya.Image = kep;

rekord = Properties.Settings.Default.Rekord;

KigyoAlap();
EtelHelyez();
Rajz();
Frissit();
}

private void Form1_Deactivate(object sender, EventArgs e)
{
if (fut && !szunet && !vege)
{
SzunetValt();
}
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (!fut || vege || e.CloseReason != CloseReason.UserClosing)
{
return;
}

if (!szunet)
{
SzunetValt();
}

if (MessageBox.Show("A játék még tart. Biztosan kilépsz?", "Kilépés", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No)
{
e.Cancel = true;
}
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
RekordMent();
tmrJatek.Stop();
kep.Dispose();
}

// ---- játékmenet ----

private int Szint
{
get { return (int)nudSebesseg.Value; }
}

private int Intervallum()
{
return 240 - Szint * 25;
}

private void KigyoAlap()
{
kigyo.Clear();
kigyo.Add(new Point(10, 10));
kigyo.Add(new Point(9, 10));
kigyo.Add(new Point(8, 10));
irany = new Point(1, 0);
kovetkezoIrany = new Point(1, 0);
}

private void UjJatek()
{
KigyoAlap();
pont = 0;
vege = false;
szunet = false;
fut = true;

EtelHelyez();
tmrJatek.Interval = Intervallum();
tmrJatek.Start();

Rajz();
Frissit();
}

private void EtelHelyez()
{
if (kigyo.Count >= Meret * Meret)
{
Vege(); // a pálya megtelt: a játékos nyert
return;
}

Point p;

do
{
p = new Point(veletlen.Next(Meret), veletlen.Next(Meret));
}
while (kigyo.Contains(p));

etel = p;
}

private void tmrJatek_Tick(object sender, EventArgs e)
{
irany = kovetkezoIrany;

Point fej = kigyo[0];
Point uj = new Point(fej.X + irany.X, fej.Y + irany.Y);

if (chkAtlep.Checked)
{
uj = new Point((uj.X + Meret) % Meret, (uj.Y + Meret) % Meret);
}
else if (uj.X < 0 || uj.Y < 0 || uj.X >= Meret || uj.Y >= Meret)
{
Vege();
return;
}

bool eszik = uj == etel;

// evéskor a farok nem mozdul, ezért az is ütközik; egyébként a farok éppen elhagyja a helyét
int vizsgalat = eszik ? kigyo.Count : kigyo.Count - 1;

for (int i = 0; i < vizsgalat; i++)
{
if (kigyo[i] == uj)
{
Vege();
return;
}
}

kigyo.Insert(0, uj);

if (eszik)
{
pont = pont + Szint;

if (pont > rekord)
{
rekord = pont;
}

EtelHelyez();
}
else
{
kigyo.RemoveAt(kigyo.Count - 1);
}

Rajz();
Frissit();
}

private void Vege()
{
tmrJatek.Stop();
fut = false;
vege = true;
RekordMent();
Rajz();
Frissit();
}

private void SzunetValt()
{
if (!fut || vege)
{
return;
}

szunet = !szunet;

if (szunet)
{
tmrJatek.Stop();
}
else
{
tmrJatek.Start();
}

Rajz();
Frissit();
}

private void IranyValt(int dx, int dy)
{
// a visszafordulás ellenőrzése az éppen érvényes irányhoz képest
if (irany.X + dx == 0 && irany.Y + dy == 0)
{
return;
}

kovetkezoIrany = new Point(dx, dy);
}

private void RekordMent()
{
Properties.Settings.Default.Rekord = rekord;
Properties.Settings.Default.Save();
}

// ---- vezérlés ----

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Enter)
{
UjJatek();
return true;
}

if (keyData == Keys.Space)
{
SzunetValt();
return true;
}

if (fut && !szunet && !vege)
{
switch (keyData)
{
case Keys.Up:
case Keys.W:
IranyValt(0, -1);
return true;

case Keys.Down:
case Keys.S:
IranyValt(0, 1);
return true;

case Keys.Left:
case Keys.A:
IranyValt(-1, 0);
return true;

case Keys.Right:
case Keys.D:
IranyValt(1, 0);
return true;
}
}

return base.ProcessCmdKey(ref msg, keyData);
}

private void pbPalya_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left)
{
return;
}

if (!fut || vege)
{
UjJatek();
}
else
{
SzunetValt();
}
}

// ---- sebesség és nehézség ----

private void nudSebesseg_ValueChanged(object sender, EventArgs e)
{
if (frissites)
{
return;
}

if (tmrJatek.Enabled)
{
tmrJatek.Interval = Intervallum(); // futás közben is érvényesül
}
}

private void Nehezseg_CheckedChanged(object sender, EventArgs e)
{
RadioButton rb = (RadioButton)sender;
int kezdo;

if (!rb.Checked || rb.Tag == null || !int.TryParse(rb.Tag.ToString(), out kezdo))
{
return;
}

frissites = true;

try
{
nudSebesseg.Value = kezdo;
}
finally
{
frissites = false;
}

if (tmrJatek.Enabled)
{
tmrJatek.Interval = Intervallum();
}
}

// ---- megjelenítés ----

private void Frissit()
{
lblPont.Text = "Pont: " + pont;
lblRekord.Text = "Rekord: " + rekord;

if (vege)
{
lblAllapot.Text = "Vége";
}
else if (szunet)
{
lblAllapot.Text = "Szünet";
}
else if (fut)
{
lblAllapot.Text = "Fut";
}
else
{
lblAllapot.Text = "Nyomj Entert!";
}
}

private void Felirat(Graphics g, string szoveg)
{
using (Font betu = new Font("Segoe UI", 20, FontStyle.Bold))
using (SolidBrush ecset = new SolidBrush(Color.White))
{
SizeF meret = g.MeasureString(szoveg, betu);
g.DrawString(szoveg, betu, ecset, (kep.Width - meret.Width) / 2, (kep.Height - meret.Height) / 2);
}
}

private void Rajz()
{
using (Graphics g = Graphics.FromImage(kep))
{
g.Clear(Color.FromArgb(24, 24, 32));

using (SolidBrush etelEcset = new SolidBrush(Color.OrangeRed))
{
g.FillEllipse(etelEcset, etel.X * Cella + 2, etel.Y * Cella + 2, Cella - 4, Cella - 4);
}

for (int i = kigyo.Count - 1; i >= 0; i--)
{
Color szin = i == 0 ? Color.LimeGreen : Color.SeaGreen;

using (SolidBrush ecset = new SolidBrush(szin))
{
g.FillRectangle(ecset, kigyo[i].X * Cella + 1, kigyo[i].Y * Cella + 1, Cella - 2, Cella - 2);
}
}

if (vege)
{
Felirat(g, "Játék vége");
}
else if (szunet)
{
Felirat(g, "Szünet");
}
else if (!fut)
{
Felirat(g, "Nyomj Entert!");
}
}

pbPalya.Invalidate();
}
}
}
Megoldás: miért így épül fel?

A játék állapotát három jelző (fut, szunet, vege) adja, és a viselkedés az állapottól függ (a ProcessCmdKey csak futó játéknál kezeli az irányokat). A ProcessCmdKey a fókusztól függetlenül elkapja a nyilakat, ezért a NumericUpDown vagy a választógombok fókuszánál is működik a játék. Az irany és a kovetkezoIrany szétválasztása teszi lehetővé, hogy egy időzítőütem alatt két billentyűnyomás se fordíthassa vissza a kígyót. A rajz a Bitmap-ra történik, a PictureBox csak megjeleníti, így nincs villogás. A közös kezelő (Nehezseg_CheckedChanged) a Tag értékéből dolgozik, és a frissites jelző védi a hurkot (5.5 és 5.8 lecke).

Tesztlista

#TesztVárt eredmény
1A program indul.Üres pálya a kígyóval és az étellel, „Nyomj Entert!” felirat, a rekord megjelenik.
2Enter indítás.A kígyó jobbra indul, az állapot „Fut”.
3Nyilak és WASD minden irányba.A kígyó követi az irányt, mindkét billentyűkészlet működik.
4Gyors visszafordulás: jobbra haladva gyorsan Fel, majd Bal.A kígyó nem megy át önmagán, a Bal nem hajtódik végre.
5Az étel megevése.A kígyó nő eggyel, a pont nő a sebesség szintjével, új étel jelenik meg, sosem a kígyó testén.
6Ütközés a fallal (falátlépés kikapcsolva).Játék vége, felirat, a rekord frissül, ha magasabb.
7Falátlépés bekapcsolva, a kígyó a falnak megy.A túloldalon jön ki, a játék folytatódik.
8Ütközés önmagával.Játék vége.
9Szóköz és bal egérgomb a pályán.Szünet és folytatás, a felirat is látszik.
10Nyilak szünetben.Nincs hatásuk (nem változik az irány).
11A sebesség állítása futás közben (nudSebesseg).A kígyó azonnal gyorsabb, illetve lassabb.
12Nehézségi választógomb váltása.A sebesség értéke a gomb Tag értékére áll; a játék közben is érvényesül.
13Másik ablakra kattintás játék közben.Automatikus szünet (Deactivate).
14Bezárás futó játéknál.Megerősítés; a Nem válaszra az ablak marad, és a játék szünetel.
15Bezárás és újraindítás.A rekord megmarad.
16Fókusz a nudSebesseg mezőn, majd nyilak játék közben.A nyilak a kígyót irányítják, nem a számmezőt.

Gyakori hibák és önértékelés

Ezeken szoktak elcsúszni

Gondold végig, nálad előfordul-e valamelyik.

1. A nyilak a számmezőt állítják

Ha a nudSebesseg mezőn van a fókusz, a nyilak a számot változtatják, nem a kígyót.

Ok és javítás

A KeyDown esemény a fókuszban lévő vezérlőhöz érkezik, és a NumericUpDown a nyilakat saját használatra veszi. A ProcessCmdKey már előbb elkapja a billentyűt (5.4 lecke), ezért a játék futásakor ott kell kezelni, és return true az eredmény.

2. A kígyó önmagába fordul gyors billentyűnyomásra

// a visszafordulás tiltása a kovetkezoIrany-hoz képest
if (kovetkezoIrany.X + dx == 0 && kovetkezoIrany.Y + dy == 0) { return; }
Hiba és javítás

A gyors Fel, majd Bal billentyűnél a kovetkezoIrany már Fel, így a Bal nem tűnik visszafordulásnak, de a tick idején a kígyó még jobbra haladt, és a Bal irány a fejnek a testébe fordulna. A visszafordulás ellenőrzése az éppen érvényes irany értékhez kell, amely csak a tick elején változik.

3. Az étel a kígyó testén jelenik meg

Az új étel néha a kígyóra kerül, és nem látszik.

Ok és javítás

A véletlen cella nincs ellenőrizve. Használj do ... while (kigyo.Contains(p)) ciklust, amely addig keres, amíg szabad cellát talál. Vigyázz: ha a kígyó az egész pályát betölti, a ciklus végtelen lenne, ezért előbb ellenőrizd, hogy van-e szabad cella.

4. A sebességállítás nem hat futás közben

A sebességet csak az új játék indulásakor veszi figyelembe a program.

Ok és javítás

Az Interval értékét csak az UjJatek állítja. A nudSebesseg.ValueChanged kezelőben is frissítsd az időzítőt (if (tmrJatek.Enabled) tmrJatek.Interval = Intervallum();).

5. A rekord nem marad meg

Properties.Settings.Default.Rekord = rekord;   // a Save() hiányzik
Hiba és javítás

A beállítás csak a memóriában változott. Hívd a Properties.Settings.Default.Save() metódust a játék végén és az ablak bezárásakor (5.6 lecke).

6. A kép nem frissül, vagy villog

A kirajzolt kígyó nem látszik, vagy a pálya villog.

Ok és javítás

A rajzolás után az Invalidate() meghívása kell, hogy a PictureBox újrarajzolja magát. A villogást a közvetlen CreateGraphics rajzolás okozza: a Bitmap-ra rajzolva és a PictureBox-szal megjelenítve nincs villogás (4.5 lecke).

Az értékeléshez ellenőrizd az alábbi szempontokat is:

MűködésA tesztlista mind a 16 pontja teljesül.
ÁllapotkezelésMinden billentyű és esemény hatása az állapottól függ, és nincs értelmetlen kombináció.
EseményekAz eseménykezelők rövidek, a közös kezelő a Tag alapján dolgozik, nincs duplázott feliratkozás.
ErőforrásokA Graphics, az ecsetek és a Bitmap felszabadul.

Bővítési ötletek, ha kész vagy

Ha maradt időd, próbáld ki:

  • Akadályok: véletlen falak a pályán (ütközés velük).
  • Gyorsulás: minden 5. étel után a sebesség (nudSebesseg) eggyel nő.
  • Hang: rövid hangjelzés evéskor és a játék végén (System.Media.SystemSounds).
  • Szín: a kígyó színének választása (ColorDialog).
  • Dicsőséglista: a legjobb öt eredmény névvel és dátummal (SQLite vagy fájl).
  • Visszaszámlálás: a szünetből folytatás előtt 3-2-1 számlálás.

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. A kész játékot bővíted. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Hossz kijelzése

Egy lblHossz felirat mutassa a kígyó aktuális hosszát. Melyik metódusban frissíted?

Megoldás megnyitása

A Frissit() metódusban, amelyet minden változás után meghívunk: a hossz a kigyo.Count.

Form1.cs (a Frissit metódusban)C#
lblHossz.Text = "Hossz: " + kigyo.Count;

2. Hangjelzés evéskor

Evéskor adjon a program egy rövid hangot, a játék végén egy másikat. Mit használsz?

Megoldás megnyitása

A System.Media.SystemSounds előre definiált rendszerhangokat ad (Beep, Asterisk, Hand). Külön hangfájl nélkül is működik.

Form1.csC#
// evéskor, az eszik ágban:
System.Media.SystemSounds.Asterisk.Play();

// a Vege() metódusban:
System.Media.SystemSounds.Hand.Play();

Közepes szint

3. Gyorsulás

Minden ötödik étel után a sebesség (nudSebesseg) egy szinttel nőjön, a 8-as szintig. Mire kell vigyázni a ValueChanged eseménnyel?

Megoldás megnyitása

A program általi értékadás is kiváltja a ValueChanged eseményt, amely frissíti az időzítőt: ez itt kívánatos. A felső határt a Maximum tulajdonság ellenőrzi, a kódnak nem kell kivételt kezelnie, ha előbb a határt vizsgálja.

Form1.csC#
private int megevett = 0;

// az eszik ágban:
megevett = megevett + 1;

if (megevett % 5 == 0 && nudSebesseg.Value < nudSebesseg.Maximum)
{
nudSebesseg.Value = nudSebesseg.Value + 1; // a ValueChanged frissíti az időzítőt
}

// az UjJatek metódusban: megevett = 0;

4. Akadályok

Új játék indításakor 5 véletlen akadály kerül a pályára. Az akadályba ütközés játékvéget jelent, és sem a kígyó, sem az étel nem kerülhet akadályra. Hol tárolod az akadályokat?

Megoldás megnyitása

Egy második List<Point> (akadalyok) tárolja őket. Az étel és az akadály elhelyezése is szabad cellát keres (nem kígyó, nem akadály, nem étel). A kígyó kezdőpozíciója körüli cellák maradjanak szabadon, különben azonnal ütközne.

Form1.csC#
private readonly List<Point> akadalyok = new List<Point>();

private void AkadalyokHelyez()
{
akadalyok.Clear();

while (akadalyok.Count < 5)
{
Point p = new Point(veletlen.Next(Meret), veletlen.Next(Meret));

bool kozel = Math.Abs(p.X - 10) < 5 && Math.Abs(p.Y - 10) < 3; // a kígyó kezdőterülete szabad

if (!kozel && !akadalyok.Contains(p) && !kigyo.Contains(p))
{
akadalyok.Add(p);
}
}
}

// az EtelHelyez ciklusfeltétele: while (kigyo.Contains(p) || akadalyok.Contains(p));
// a tick-ben az ütközés vizsgálata: if (akadalyok.Contains(uj)) { Vege(); return; }
// a Rajz-ban: szürke négyzetek az akadályok helyén

Kihívás szint

5. Visszaszámlálás szünet után

A szünetből való folytatás előtt a pályán jelenjen meg 3, 2, 1 egy-egy másodpercig, és csak utána induljon újra a kígyó. Hogyan szervezed az állapotokat?

Megoldás megnyitása

Egy új, negyedik állapot (szamlalas) és egy második Timer (tmrSzamlalo, 1000 ms) kell. A számláló alatt a billentyűk (a szüneten kívül) nem hatnak, a játékidőzítő áll. A számláló végén a szunet hamis, a játékidőzítő indul. Az állapotfüggő ProcessCmdKey kezeléséhez a szamlalas jelzőt is figyelembe kell venni.

Form1.csC#
private bool szamlalas = false;   // negyedik állapot: visszaszámlálás
private int szam = 0;

private void SzunetValt()
{
if (!fut || vege || szamlalas)
{
return;
}

if (!szunet)
{
szunet = true;
tmrJatek.Stop();
}
else
{
szunet = false;
szamlalas = true;
szam = 3;
tmrSzamlalo.Start(); // Interval: 1000
}

Rajz();
Frissit();
}

private void tmrSzamlalo_Tick(object sender, EventArgs e)
{
szam = szam - 1;

if (szam <= 0)
{
tmrSzamlalo.Stop();
szamlalas = false;
tmrJatek.Start(); // a számlálás végén indul újra a kígyó
}

Rajz();
}

// a Rajz-ban: if (szamlalas) { Felirat(g, szam.ToString()); }
// a ProcessCmdKey-ben az irányok feltétele: fut && !szunet && !vege && !szamlalas

6. Részletes eseménynapló hibakereséshez

Egy rejtett lstNaplo ListBox (a F12 billentyűvel kapcsolható) naplózza a játék eseményeit (billentyűk, állapotváltások, evés, ütközés). Hogyan oldod meg úgy, hogy a naplózás ne lassítsa a játékot, és a lista ne nőjön végtelenre?

Megoldás megnyitása

A naplózás egyetlen Naplo(string) metódus, amely csak akkor ír, ha a lista látható (lstNaplo.Visible). A lista hosszát korlátozzuk (például 100 sor): a legrégebbi sort töröljük. A F12 kapcsolót a ProcessCmdKey kezeli.

Form1.csC#
private void Naplo(string szoveg)
{
if (!lstNaplo.Visible)
{
return;
}

lstNaplo.Items.Insert(0, DateTime.Now.ToString("HH:mm:ss.fff") + " – " + szoveg);

while (lstNaplo.Items.Count > 100)
{
lstNaplo.Items.RemoveAt(lstNaplo.Items.Count - 1);
}
}

// a ProcessCmdKey-ben:
if (keyData == Keys.F12)
{
lstNaplo.Visible = !lstNaplo.Visible;
return true;
}

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Miért a ProcessCmdKey kezeli a nyilakat?

Mert a fókuszban lévő vezérlő (például a NumericUpDown) a nyilakat saját használatra veszi, a ProcessCmdKey viszont előbb látja a billentyűt, és a fókusztól függetlenül kezelheti.

2. Miért két irányváltozó az irany és a kovetkezoIrany?

Hogy egy időzítőütem alatt több billentyű se fordíthassa vissza a kígyót: a tiltás az éppen érvényes irányhoz képest történik, az új irány csak a következő tick elején lép életbe.

3. Miért kell a frissites jelző a sebességnél?

Mert a nehézségi választógomb és a sebességszám egymást változtatja (ValueChanged), és a jelző megakadályozza a visszacsatolást.

4. Mi az állapotgép előnye?

Pontosan meghatározza, melyik állapotban melyik esemény mit tesz, így nincs értelmetlen viselkedés (például nyilak szünetben).

5. Miért a Bitmapra rajzolunk?

Mert így villogásmentes, és a kép megmarad. A PictureBox csak megjeleníti.

6. Hova kerül a takarítás és a mentés?

A FormClosed eseménybe (az időzítő leállítása, a rekord mentése, a Bitmap felszabadítása). A FormClosing csak a kérdés (megszakítható).

7. Mi történik a fókusz elvesztésekor?

A Deactivate esemény szünetelteti a játékot, hogy a felhasználó ne veszítsen miatta.

Mit tudsz már? (az 5. fejezet végén)

  • Az eseménykezelést mélyen érted: delegáltak, feliratkozás és leiratkozás, sender és EventArgs.
  • Egeret, billentyűzetet és változás-eseményeket kezelsz, és az ablak életciklusát tudatosan használod.
  • Közös eseménykezelőt írsz, és állapotgépként modellezel egy játékot.
  • Elkészítettél egy teljes, billentyűzettel vezérelt programot.

Következő lecke: Form mérete, helye, címe és ikonja

Az 5. fejezet kész. A 6. fejezetben a felülettervezés kerül sorra: az ablak megjelenése, az elrendezés és a méretezhető űrlapok.