Obiekty graficzne na przykładzie prostej gry- ruch obiektu graficznego. Część 4
W tej części zajmiemy się tworzeniem klasy symbolizującej graczy. Gracze będą reprezentowani przez dwa czołgi, które w kodzie programu są obsługiwani tak samo, różnią się tylko graficznym obrazem. Kod czwartej części bazuje na kodzie części poprzedniej, którą możesz pobrać z tego linku: (pobierz kod)
Założenia wstępne dla klasy Czolg
Na tę chwilę zadania stawiane przed klasą reprezentującą graczy to
- rysowanie bieżącej klatki
- zmiana kierunku, czyli obrót
- ruch w aktualnym kierunku
Sterowanie obiektami będzie się odbywać w głównym oknie aplikacji
Tworzymy klasę Czolg
Wybieramy menu funkcję Projekt/Dodaj klasę Wprowadzamy poniższy kod
Wskazówka:
namespace czolg_1
{
internal class Czolg
{
int[] klatki = new int[4];
static float maxV = 2.0f;
private int idKlatka;
private int wiersz,kolumna;//pozycja w klatkach swiata gry
private int wys, szer;//wysokośc i szerokośc klatki świata gry
private float x, y;//pozycja we współrzednych ekranowych
public int Wiersz
{//zwroc,ustaw pozycje w klatkach swiata gry
get { return wiersz; }
set { wiersz = value; }
}
public int Kolumna
{//zwroc,ustaw pozycje w klatkach swiata gry
get { return kolumna; }
set { kolumna = value; }
}
}
}
Konstruktor klasy powinien obsłużyć bieżącą aktywną klatkę obrazu oraz położenie w świecie gry. Plik z kodem klasy uzupełniamy o ten kod
Wskazówka:
//konstruktor
public Czolg(int _idKlatka,int _wiersz,int _kolumna,int _wys,int _szer)
{
idKlatka = _idKlatka;
Wiersz = _wiersz;
Kolumna = _kolumna;
wys =_wys;
szer = _szer;
//oblicz współrzedne startu
x = Kolumna * szer + szer/2;
y = Wiersz * wys + wys/2;
}
Wstępnie napiszemy kod funkcji rysującą obraz czołgu. Mając tą funkcję będziemy mogli przetestować aplikację czy prawidłowo tworzymy i odczytujemy obiekt, którym będziemy mogli grać. Dodajemy poniższy kod ładujący klatki animacji kierunków ruchu czołgu
Wskazówka:
public void LadujKlatkiKierunku(int _N,int _E,int _S,int _W)
{
klatki[0] = _N;//na północ
klatki[1] = _E;//na wschód
klatki[2] = _S;//na południe
klatki[3] = _W;//na zachód
}
Funkcja rysująca aktywną klatkę ma postać jak poniżej
Wskazówka:
public void Rysuj(Graphics g, List<Bitmap> bmp)
{
g.DrawImage(bmp[klatki[idKlatka]],
new Rectangle((int)x-szer/2, (int)y-wys/2, szer, wys),
new Rectangle(0, 0, szer, wys),
GraphicsUnit.Pixel);
}
Tworzymy obiekty graczy w głównym oknie aplikacji
Przechodzimy do pliku Form1.cs. W kodzie deklarujemy dwie zmienne typu Czolg o nazwie czolgA, czolgB.
Wskazówka:
public partial class Form1 : Form
{
bitmapy bmp;
Swiat swiat;
Czolg czolgA, czolgB;
Wskazówka:
public Form1()
{
InitializeComponent();
swiat = new Swiat("swiat1.txt");
//buduj swiat 20x15- 20 kolumn na 15 wierszy po 48x48 pikseli
bmp =new bitmapy(swiat.IleKlatekX,swiat.IleKlatekY,48,48);
//rob czolg zwrocony na południe
czolgA = new Czolg(2,0, 0, bmp.H, bmp.W);
czolgA.LadujKlatkiKierunku(0,1,2,3);
//rób czołg zwrócony na połnoc
czolgB = new Czolg(0,14, 19, bmp.H, bmp.W);
czolgB.LadujKlatkiKierunku(8, 9, 10, 11);
}
Modyfikujemy sposób rysowania świata gry. Od tej pory nie będziemy rysować ekranu gry w kontrolce panel1 lecz bezpośrednio w oknie formatki. Usuwamy kontrolkę panel1. Aktywujemy zdarzenie Paint dla formatki
Wprowadzamy poniższy kod
Wskazówka:
private void Form1_Paint(object sender, PaintEventArgs e)
{
bmp.PokazEkranGry(this.Handle, swiat, czolgA, czolgB);
}
Skompiluj program i sprawdź efekt działania. Jeżeli kod programu nie zawiera błędów, to ekran aplikacji powinien być podobny do poniższego
Obrót i ruch obiektu gracza
Klasę Czolg wzbogacamy o funkcję obracającą obiekt gracza oraz funkcje ruchu. Idea obrotu jak i ruchu jest prosta. W przypadku obrotu (lewo/ prawo) będzie to zmiana klatek ustawienia czołgu. Należy przewidzieć zabezpieczenie bieżącego indeksu klatki. Nie może być on mniejszy od zera i większy od maksymalnej wartości. W tym przypadku od 3.
W klasie Czolg dodajemy dwie publiczne zmienne z wiązane z prędkością i ruchem (float v i bool fRuch)
Wskazówka:
private float x, y;//pozycja we współrzednych ekranowych
public float v = maxV;//predkosc
public bool fRuch = false;
public int Wiersz
{//zwroc,ustaw pozycje w klatkach swiata gry
Do klasy Czolg dopisujemy ten kod
Wskazówka:
public void Obracaj(int i)
{
idKlatka += i;
if (idKlatka < 0) idKlatka = 3;
if (idKlatka > 3) idKlatka = 0;
}
Funkcja ruchu na tę chwilę ma jeden cel: przesunąć obiekt wzdłuż osi x lub y bez sprawdzania czy taki ruch jest możliwy. Dodajemy funkcję ruchu
Wskazówka:
public void Ruch()
{
if (!fRuch) return;
switch (idKlatka){
//na północ
case 0:y -= v;break;
//na wschód
case 1: x += v; break;
//na południe
case 2: y += v; break;
//na zachód
case 3: x -= v; break;
}
}
Skompiluj program i sprawdź czy nie zwiera błędów.
Klawisze sterujące
Przyjmujemy, że obroty obiektów graczy będziemy realizować klawiszami dla gracza A: klawisz A w lewo, klawisz D w prawo. Dla gracza B będą to odpowiednio klawisze: strzałka w lewo, to obrót w lewo, strzałka w prawo to obrót w prawo. Klawisze ruchu dla gracza A, to W- naprzód, S- wstecz. Dla gracza B, to strzałka w górę- naprzód , strzałka w dół- wstecz.
Do wywołania kontroli wciskanych klawiszy wykorzystamy zdarzenie KeyDown i KeyUp podpięte do okna głównej formatki.
Kod zdarzenia KeyDown
Wskazówka:
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
//klawiszologia
//dla gracza A
if (e.KeyCode == Keys.A) czolgA.Obracaj(-1);
if (e.KeyCode == Keys.D) czolgA.Obracaj(1);
if (e.KeyCode == Keys.W)
{
czolgA.fRuch = true;
czolgA.v= czolgA.Vmax;
}
if (e.KeyCode == Keys.S)
{
czolgA.fRuch = true;
czolgA.v = -czolgA.Vmax;
}
//dla gracza B
if (e.KeyCode == Keys.Left) czolgB.Obracaj(-1);
if (e.KeyCode == Keys.Right) czolgB.Obracaj(1);
if (e.KeyCode == Keys.Up)
{
czolgB.fRuch = true;
czolgB.v = czolgB.Vmax;
}
if (e.KeyCode == Keys.Down)
{
czolgB.fRuch = true;
czolgB.v = -czolgB.Vmax;
}
}
Zdarzenie KeyUp
Wskazówka:
private void Form1_KeyUp(object sender, KeyEventArgs e)
{
//klawiszologia
//dla gracza A
if (e.KeyCode == Keys.W ||
e.KeyCode == Keys.S)
czolgA.fRuch = false;
//dla gracza B
if (e.KeyCode == Keys.Up ||
e.KeyCode == Keys.Down)
czolgB.fRuch = false;
}
Skompiluj program i sprawdź efekt działania. Na tym etapie program pozwala obracać lewo/ prawo oba obiekty graczy
Zegar gry
Zegar gry jest sercem całej rozgrywki. W tym miejscu odbywa się odświeżanie ekranu gry, aktualizują się klatki obiektów, paski życia, kolizje itp. W programie wykorzystamy kontrolkę Timer, którą osadzamy w głównym oknie
Właściwość Enabled ustawiamy na true, a Interval na 5 milisekund
W zdarzeniu Tick kontrolki wprowadzamy wywołanie naszej funkcji ZegarGry
Wskazówka:
private void timer1_Tick(object sender, EventArgs e)
{
zegarGry();
}
private void zegarGry()
{
czolgA.Ruch();
czolgB.Ruch();
bmp.PokazEkranGry(this.Handle, swiat, czolgA, czolgB);
}
Skompiluj program i sprawdź efekt działania. W tej chwili można poruszać się czołgami po planszy
Poniżej podaję pełny kod klasy Czolg oraz kod głównej formatki tworzonej aplikacji desktopowej
Klasa Czolg.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace czolg_1
{
internal class Czolg
{
int[] klatki = new int[4];
static float maxV = 2.0f;
private int idKlatka;
private int wiersz,kolumna;//pozycja w klatkach swiata gry
private int wys, szer;//wysokośc i szerokośc klatki świata gry
private float x, y;//pozycja we współrzednych ekranowych
public float v = maxV;//predkosc
public bool fRuch = false;
public int Wiersz
{//zwroc,ustaw pozycje w klatkach swiata gry
get { return wiersz; }
set { wiersz = value; }
}
public int Kolumna
{//zwroc,ustaw pozycje w klatkach swiata gry
get { return kolumna; }
set { kolumna = value; }
}
//funkcja odczytu max predkosci
public float Vmax { get { return maxV; } }
//konstruktor
public Czolg(int _idKlatka,int _wiersz,int _kolumna,int _wys,int _szer)
{
idKlatka = _idKlatka;
Wiersz = _wiersz;
Kolumna = _kolumna;
wys =_wys;
szer = _szer;
//oblicz współrzedne startu
x = Kolumna * szer + szer/2;
y = Wiersz * wys + wys/2;
}
public void LadujKlatkiKierunku(int _N,int _E,int _S,int _W)
{
klatki[0] = _N;//na północ
klatki[1] = _E;//na wschód
klatki[2] = _S;//na południe
klatki[3] = _W;//na zachód
}
public void Rysuj(Graphics g, List<Bitmap> bmp)
{
g.DrawImage(bmp[klatki[idKlatka]],
new Rectangle((int)x-szer/2, (int)y-wys/2, szer, wys),
new Rectangle(0, 0, szer, wys),
GraphicsUnit.Pixel);
}
public void Obracaj(int i)
{
idKlatka += i;
if (idKlatka < 0) idKlatka = 3;
if (idKlatka > 3) idKlatka = 0;
}
public void Ruch()
{
if (!fRuch) return;
switch (idKlatka){
//na północ
case 0:y -= v;break;
//na wschód
case 1: x += v; break;
//na południe
case 2: y += v; break;
//na zachód
case 3: x -= v; break;
}
}
}
}
Klasa Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace czolg_1
{
public partial class Form1 : Form
{
bitmapy bmp;
Swiat swiat;
Czolg czolgA, czolgB;
public Form1()
{
InitializeComponent();
swiat = new Swiat("swiat1.txt");
//buduj swiat 20x15- 20 kolumn na 15 wierszy po 48x48 pikseli
bmp =new bitmapy(swiat.IleKlatekX,swiat.IleKlatekY,48,48);
//rob czolg zwrocony na południe
czolgA = new Czolg(2,0, 0, bmp.H, bmp.W);
czolgA.LadujKlatkiKierunku(0,1,2,3);
//rób czołg zwrócony na połnoc
czolgB = new Czolg(0,14, 19, bmp.H, bmp.W);
czolgB.LadujKlatkiKierunku(8, 9, 10, 11);
}
private void Form1_Load(object sender, EventArgs e)
{
//wczytaj bitmape na sztwyno z pliku
//zakładamy, że plik grafiki istnieje
//więc nie musimy sprawdzać czy istnieje
bmp.WczytajBmp("./g/czolg.png");
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
bmp.PokazEkranGry(this.Handle, swiat, czolgA, czolgB);
}
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
//klawiszologia
//dla gracza A
if (e.KeyCode == Keys.A) czolgA.Obracaj(-1);
if (e.KeyCode == Keys.D) czolgA.Obracaj(1);
if (e.KeyCode == Keys.W)
{
czolgA.fRuch = true;
czolgA.v= czolgA.Vmax;
}
if (e.KeyCode == Keys.S)
{
czolgA.fRuch = true;
czolgA.v = -czolgA.Vmax;
}
//dla gracza B
if (e.KeyCode == Keys.Left) czolgB.Obracaj(-1);
if (e.KeyCode == Keys.Right) czolgB.Obracaj(1);
if (e.KeyCode == Keys.Up)
{
czolgB.fRuch = true;
czolgB.v = czolgB.Vmax;
}
if (e.KeyCode == Keys.Down)
{
czolgB.fRuch = true;
czolgB.v = -czolgB.Vmax;
}
}
private void Form1_KeyUp(object sender, KeyEventArgs e)
{
//klawiszologia
//dla gracza A
if (e.KeyCode == Keys.W ||
e.KeyCode == Keys.S)
czolgA.fRuch = false;
//dla gracza B
if (e.KeyCode == Keys.Up ||
e.KeyCode == Keys.Down)
czolgB.fRuch = false;
}
private void timer1_Tick(object sender, EventArgs e)
{
zegarGry();
}
private void zegarGry()
{
czolgA.Ruch();
czolgB.Ruch();
bmp.PokazEkranGry(this.Handle, swiat, czolgA, czolgB);
}
}
}
Klasa Swiat.cs
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing;
namespace czolg_1
{
internal class Swiat
{
static int ileKlatekX = 20, ileKlatekY = 15;
int[,] plansza=new int[ileKlatekY,ileKlatekX];
public int IleKlatekX
{ //zwróc ilośc kolumn
get { return ileKlatekX; }
}
public int IleKlatekY
{ //zwróc ilośc wierszy
get { return ileKlatekY; }
}
//konstruktor
public Swiat(string plik)
{
//odczytaj wskazany plik z mapą świata gry
string[] txt=System.IO.File.ReadAllLines(plik);
for(int i= 0; i < txt.Count(); i++)
{
//odczytaj wiersze i ustaw znak separatora na spację
string[]wiersz=txt[i].Split(' ');
//czytaj liczby zapisane w wierszach
for (int j = 0; j < wiersz.Length; j++)
{
int k = int.Parse(wiersz[j]);
//przypisz odczyatne indeksy klatek do
//tablicy planszy mapy świata gry
plansza[i, j] = k;
}
}
}
public void RysujSwiat(Graphics g, List<Bitmap> bmp,int w,int h)
{
for(int i=0;i< ileKlatekY;i++)
for (int j = 0; j < ileKlatekX; j++)
{
int idKlatka = plansza[i,j];
g.DrawImage(bmp[idKlatka], new Rectangle(j*w, i*h, w, h), new Rectangle(0, 0, w, h), GraphicsUnit.Pixel);
}
}
}
}
Wskazówka:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace czolg_1
{
internal class bitmapy
{
public Bitmap zasobyBmp;
//lista klatek wycinanych bitmapek
private List<Bitmap> bitmaps = new List<Bitmap>();
private Bitmap bmpBuforEkranGry;//bufor graficzny
private Graphics gEkranGry;
private int w, h,//szerkość klatki
ileKolumn, ileWierszy;//ilosc kolumn i wierszy swiata gry
public int W {//zwroc,ustaw szerokość klatki
get { return w; }
set { w = value;}
}
public int H
{ //zwroc,ustaw wysokość klatki
get { return h; }
set { h = value; }
}
//konstruktor
public bitmapy(int ileK,int ileW,int _w,int _h)
{
this.w = _w;
this.h = _h;
this.ileKolumn = ileK;
this.ileWierszy = ileW;
//przygotuj bufor rysowania ekranu gry
bmpBuforEkranGry = new Bitmap(this.ileKolumn*this.w,
this.ileWierszy*this.h,
System.Drawing.Imaging.PixelFormat.Format32bppArgb);
//skojarz moduł graficzny z buforem rysowania ekranu gry
gEkranGry= Graphics.FromImage(bmpBuforEkranGry);
}
private void RysujSwiatGryBufor(Swiat swiat,Czolg czolgA,Czolg czolgB)
{
swiat.RysujSwiat(gEkranGry, bitmaps,W,H);
//na teraz testowe rysowania czolgu graczaA
czolgA.Rysuj(gEkranGry, bitmaps);
//na teraz testowe rysowania czolgu graczaB
czolgB.Rysuj(gEkranGry, bitmaps);
}
public void PokazEkranGry(IntPtr uchwyt, Swiat swiat, Czolg czolgA, Czolg czolgB)
{
Graphics g = Graphics.FromHwnd(uchwyt);
RysujSwiatGryBufor(swiat, czolgA, czolgB);
g.DrawImage(bmpBuforEkranGry, 0, 0);
g.Dispose();
}
public void WczytajBmp(string plik)
{
//funkcja wczytuje bitmapę z pliku graficznego
zasobyBmp = new Bitmap(plik);
//wczytany plik zasobów potnij na klatki o wymiarach W x H
//w programie przyjałem 48 x 48 pikseli
int ileKolumn = zasobyBmp.Width / W;
int ileWierszy= zasobyBmp.Height / H;
for (int i = 0; i < ileWierszy; i++)
for (int j = 0; j < ileKolumn; j++)
{
bitmaps.Add(new Bitmap(w, h, System.Drawing.Imaging.PixelFormat.Format32bppArgb));
Graphics g = Graphics.FromImage(bitmaps[bitmaps.Count-1]);
g.DrawImage(zasobyBmp, new Rectangle(0, 0, w, h),
new Rectangle(j*w, i*h, w, h),
GraphicsUnit.Pixel);
g.Dispose();
}
}
public void testPokazBmp(Graphics g, int x, int y, int w, int h)
{
//funkcja testowa, pokazuje pierwszą klatkę ze współrzednych (0,0,0+w,0+h)
g.DrawImage(zasobyBmp, new Rectangle(x,y,w,h), new Rectangle(0, 0, w, h), GraphicsUnit.Pixel);
}
public void pokazKlatke(Graphics g, int idKlatka, int x, int y)
{
//funkcja testowa, pokazuje dowolną klatkę
g.DrawImage(bitmaps[idKlatka], new Rectangle(x, y, w, h), new Rectangle(0, 0, w, h), GraphicsUnit.Pixel);
}
//destruktor
~bitmapy()
{
//zwolnij pamięc zajmowaną przez zasoby graficzne
zasobyBmp.Dispose();
foreach (Bitmap b in bitmaps) { b.Dispose(); }
//zwolnij bufor rysowania ekranu gry
bmpBuforEkranGry.Dispose();
gEkranGry.Dispose();
}
}
}