Grafikus C# – Windows Forms tanfolyam › 5.9 lecke Windows Forms · 5.9 · záró mini projekt Az 5. fejezet záró projektjében minden összekapcsolódik, amit az eseményekről tanultál: a billentyűzet (Mini projekt: billentyűzettel vezérelt program – kígyó játék
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.
A projekt végére
- állapotgépként modellezel egy játékot (készen áll, fut, szünet, vége);
- a
ProcessCmdKeysegí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
Settingssegí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.
Terv: követelmények és állapotgép
| Rész | Követelmény | Pont |
|---|---|---|
| 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):
| Állapot | Mit jelent? | Enter | Szóköz | Nyilak / WASD | Időzítő |
|---|---|---|---|---|---|
| Készen áll | A program indulása; még nincs játék. | Új játék → Fut | nem csinál semmit | nem csinál semmit | áll |
| Fut | A kígyó mozog. | Új játék (újraindítás) | → Szünet | irányt vált | fut |
| Szünet | Szüneteltetve (kézzel vagy fókusvesztés miatt). | Új játék | → Fut | nem csinál semmit | áll |
| Vége | A kígyó ütközött. | Új játék → Fut | nem csinál semmit | nem 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 |
|---|---|---|
| Form | Form1 | Text: Kígyó; FormBorderStyle: FixedSingle; MaximizeBox: False; StartPosition: CenterScreen |
| PictureBox | pbPalya | Size: 400 × 400; SizeMode: Normal; TabStop: False |
| Label ×3 | lblPont, lblRekord, lblAllapot | pontszám, rekord, állapot (pl. „Szünet”) |
| NumericUpDown | nudSebesseg | Minimum: 1; Maximum: 8; Value: 4 |
| RadioButton ×3 | rbKonnyu, rbKozepes, rbNehez | Tag: 2, 4, 7 (a kezdősebesség); a rbKozepes bejelölt; mindhárom a Nehezseg_CheckedChanged kezelőt hívja |
| CheckBox | chkAtlep | Text: Falátlépés |
| Timer | tmrJatek | Interval: 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.
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. Feladat: A Támpont: A cella mérete 20 képpont. Az Feladat: A Támpont: A fej Feladat: Írd felül a Támpont: Két irányváltozó kell: az Feladat: Az Támpont: Feladat: Fal- és önmagába ütközés esetén a játék véget ér ( Támpont: Átlépésnél a koordinátát a pálya méretével modulozd: Feladat: A Támpont: A szünet az időzítő leállítása és indítása. A feliratot a Feladat: A Támpont: A két vezérlő egymást is változtatja, ezért kell a Feladat: A rekord a Támpont: A kérdés előtt állítsd szünetre a játékot, hogy ne fusson a kérdés alatt.1. lépés: a pálya és a kígyó kirajzolása (állóképként)
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.(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
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).kigyo[0]. Az új fej: new Point(fej.X + irany.X, fej.Y + irany.Y).3. lépés: irányítás
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.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
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.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
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).(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
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.Graphics.DrawString rajzolja.7. lépés: sebesség és nehézség
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.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
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.
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)
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
| # | Teszt | Várt eredmény |
|---|---|---|
| 1 | A program indul. | Üres pálya a kígyóval és az étellel, „Nyomj Entert!” felirat, a rekord megjelenik. |
| 2 | Enter indítás. | A kígyó jobbra indul, az állapot „Fut”. |
| 3 | Nyilak és WASD minden irányba. | A kígyó követi az irányt, mindkét billentyűkészlet működik. |
| 4 | Gyors visszafordulás: jobbra haladva gyorsan Fel, majd Bal. | A kígyó nem megy át önmagán, a Bal nem hajtódik végre. |
| 5 | Az é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. |
| 7 | Falá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. |
| 9 | Szóköz és bal egérgomb a pályán. | Szünet és folytatás, a felirat is látszik. |
| 10 | Nyilak szünetben. | Nincs hatásuk (nem változik az irány). |
| 11 | A sebesség állítása futás közben (nudSebesseg). | A kígyó azonnal gyorsabb, illetve lassabb. |
| 12 | Nehé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. |
| 13 | Másik ablakra kattintás játék közben. | Automatikus szünet (Deactivate). |
| 14 | Bezá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. |
| 15 | Bezárás és újraindítás. | A rekord megmarad. |
| 16 | Fó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 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
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. A véletlen cella nincs ellenőrizve. Használj Ok és javítás
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. Az Ok és javítás
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ányzikHiba é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. A rajzolás után az Ok és javítás
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:
Tag alapján dolgozik, nincs duplázott feliratkozás.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 A lblHossz felirat mutassa a kígyó aktuális hosszát. Melyik metódusban frissíted?Megoldás megnyitása
Frissit() metódusban, amelyet minden változás után meghívunk: a hossz a kigyo.Count.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? A Megoldás megnyitása
System.Media.SystemSounds előre definiált rendszerhangokat ad (Beep, Asterisk, Hand). Külön hangfájl nélkül is működik.// 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 ( A program általi értékadás is kiváltja a nudSebesseg) egy szinttel nőjön, a 8-as szintig. Mire kell vigyázni a ValueChanged eseménnyel?Megoldás megnyitása
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.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? Egy második Megoldás megnyitása
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.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? Egy új, negyedik állapot (Megoldás megnyitása
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.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 A naplózás egyetlen 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
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.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. Mert a fókuszban lévő vezérlő (például a 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. Mert a nehézségi választógomb és a sebességszám egymást változtatja ( Pontosan meghatározza, melyik állapotban melyik esemény mit tesz, így nincs értelmetlen viselkedés (például nyilak szünetben). Mert így villogásmentes, és a kép megmarad. A A A 1. Miért a ProcessCmdKey kezeli a nyilakat?
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?
3. Miért kell a frissites jelző a sebességnél?
ValueChanged), és a jelző megakadályozza a visszacsatolást.4. Mi az állapotgép előnye?
5. Miért a Bitmapra rajzolunk?
PictureBox csak megjeleníti.6. Hova kerül a takarítás és a mentés?
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?
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ésEventArgs. - 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.




