Кретање више објеката¶
У овој лекцији ћемо показати још неколико примера анимираног покрета. Подсећамо вас још једном да на почетној страни можете да преузмете комплетан изворни код свих примера.
Кугле¶
Овај пример је врло сличан примеру „Билијар” из лекције о кретању цртежа. Кретање и одбијање је решено на исти начин, а разлика је у томе што се сада анимира кретање више кугли. Зато ћемо овде увести једноставну класу Kugla, чији објекти ће садржати све потребне информације о кугли:
положај кугле (координате центра x, y)
брзина кугле (померај по свакој координати од претходног до текућег фрејма, dx, dy)
величина кугле (њен полупречник r)
боја кугле, тачније четка која боји изабраном бојом (cetka)
class Kugla { public int x, y, dx, dy, r; public Brush cetka; }
Дефинисаћемо низ кугли као члацију класе формулара:
private Kugla[] Kugle;
Користићемо и генератор случајних бројева, као што смо то урадили у примеру „Воћњак” да бисмо постигли мала померања јабука. Помоћу овог генератора бирамо насумично боје, величине, почетне положаје и брзине кугли.
Након иницијализације, у преостале две функције (Form1_Paint и timer1_Tick) све тече као у примеру „Билијар”, смо што се померање , односно цртање кугле налази у петљи. Следи комплетан код програма:
using System;
using System.Drawing;
using System.Windows.Forms;
namespace Kugle
{
public partial class Form1 : Form
{
class Kugla { public int x, y, dx, dy, r; public Brush cetka; }
const int BrojKugli = 10;
private Kugla[] Kugle;
Random Rnd = new Random();
public Form1()
{
InitializeComponent();
ClientSize = new Size(500, 300);
Text = "Kugle";
BackColor = Color.DarkGray;
Color[] boje = { Color.Red, Color.Yellow, Color.Blue, Color.Cyan, Color.Green, Color.Purple };
Kugle = new Kugla[BrojKugli];
for (int i = 0; i < BrojKugli; i++)
{
int r = Rnd.Next(10, 30);
int x = Rnd.Next(r, ClientSize.Width - r);
int y = Rnd.Next(r, ClientSize.Height - r);
Color boja = boje[Rnd.Next(boje.Length)];
int dx = 0, dy = 0;
while (dx == 0 && dy == 0) // ne zelimo lopte koje stoje
{
dx = Rnd.Next(-8, 9);
dy = Rnd.Next(-8, 9);
}
Kugle[i] = new Kugla() {
x = x, y = y, dx = dx, dy = dy, r = r, cetka = new SolidBrush(boja)
};
}
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
foreach (var k in Kugle)
g.FillEllipse(k.cetka, k.x - k.r, k.y - k.r, 2 * k.r, 2 * k.r);
}
private void timer1_Tick(object sender, EventArgs e)
{
for (int i = 0; i < BrojKugli; i++)
{
Kugle[i].x += Kugle[i].dx;
Kugle[i].y += Kugle[i].dy;
if (Kugle[i].x - Kugle[i].r < 0 || Kugle[i].x + Kugle[i].r > ClientSize.Width)
Kugle[i].dx = -Kugle[i].dx;
if (Kugle[i].y - Kugle[i].r < 0 || Kugle[i].y + Kugle[i].r > ClientSize.Height)
Kugle[i].dy = -Kugle[i].dy;
}
Invalidate();
}
}
}
Скакутање лопти¶
Написати програм који приказује лопте које скакућу. Лопте се одбијају о ивице прозора, али њихове путање нису праволинијске. Овај пут замишљамо лопте које се крећу у вертикалној равни, тако да имају и убрзање усмерено наниже услед гравитације.
(Ова анимација понавља само првих пар секунди кретања лопти, због чега лопте наизглед повремено скоче, али у програму се то не дешава.)
Овај пример је надоградња претходног. Класа Lopta је суштински иста као раније дефниисана класа Kugla. Функције Form1_Paint и Form1() (конструктор формулара у коме иницијализујемо променљиве) су скоро исте као у претходном примеру. Разлика је у томе што се почетне позиције лопти бирају у горњем делу екрана (да би могле лепо да одскачу).
Значајније се разликује само функција timer1_Tick, која ажурира фрејм. Зато ћемо њу детаљније објаснити.
Из физике знамо да убрзање представља промену брзине. Под утицајем гравитације, све лопте имају убрзање усмерено наниже. То значи да је у координатном систему прозора вектор убрзања облика \((0, a)\) за неко позитивно a. Када ажурирамо брзину лопте \((V_x, V_y)\), вектору брзине треба додати промену брзине, то јест вектор убрзања. Другим речима, y координати вектора брзине треба додати неку позитивну вредност. Ми смо изабрали да у проргаму то буде број 1.
Након ажурирања брзине, треба да ажурирамо и положај лопте. То ћемо обавити тако што вектору положаја додамо вектор брзине.
Остаје још да обрадимо одбијања. Као и раније, када лопта бар мало „утоне” у неку од ивица, потребно је одговарајућој компоненти брзине (x за леву и десну, y за горњу и доњу ивицу) променити смер. Пошто се у овом примеру мења и интензитет брзине, сада треба додатно водити рачуна о још једном детаљу. Наиме, након што лопта мало утоне у доњу ивицу прозора и промени смер, интензитет брзине ће се смањити због гравитације, па би се могло догодити да се до следећег фрејма лопта не извуче из ивице прозора у потпуности. Због тога би алгоритам (грешком) могао поново да промени смер кретања лопте, овај пут на доле. Тиме би кретање лопте постало неприродно и она би остала заробљена уз доњу ивицу прозора, померајући се незнатно горе - доле.
Да се то не би дешавало, нови положај лопте ћемо израчунати мало коректније. Нека је \(Y_{max}\) највећа y координата до које y координата \(Y_L\) центра лопте може да иде (у програму је yMax = ClientSize.Height - lopta.R). Замислимо да је лопта у неком тренутку између два приказана фрејма додирнула доњу ивицу, то јест y координата центра лопте је достигла вредност \(Y_{max}\). Требало би да се лопта у том случају одмах одбије и промени смер кренувши (косо) навише, а ми смо наредбом lopta.Y += lopta.Vy подразумевали да је наставила да се креће (косо) наниже. Тако, уместо да се центар лопте већ налази неколико пиксела изнад линије \(Y_{max}\), његова тренутна вредност је исто толико пиксела испод ове линије. Зато уводимо поправку y координате центра лопте и смањујемо \(Y_L\) за двоструку разлику \(Y_L - Y_{max}\). Међутим, \(Y_L - 2 \cdot (Y_L - Y_{max}) = 2 \cdot Y_{max} - Y_L\), па у програму корекцију пишемо као lopta.Y = 2 * yMax - lopta.Y;
Слично се долази и до поправки код одбијања у односу на остале ивице (мада код осталих ивица не бисмо имали проблем ни без ових корекција).
Ево како изгледа функција timer1_Tick:
class Lopta
{
public float X, Y, Vx, Vy, R; // polozaj, brzina i poluprecnik lopte
public Brush Cetka; // cetka kojom cemo iscrtavati loptu
}
// ...
private void timer1_Tick(object sender, EventArgs e)
{
foreach (var lopta in Lopte)
{
lopta.Vy += 1; // gravitacija
lopta.X += lopta.Vx;
lopta.Y += lopta.Vy;
float xMin = lopta.R, xMax = ClientSize.Width - lopta.R;
float yMin = lopta.R, yMax = ClientSize.Height - lopta.R;
if (lopta.X < xMin) { lopta.Vx = -lopta.Vx; lopta.X = 2 * xMin - lopta.X; }
else if (lopta.X > xMax) { lopta.Vx = -lopta.Vx; lopta.X = 2 * xMax - lopta.X; }
if (lopta.Y < yMin) { lopta.Vy = -lopta.Vy; lopta.Y = 2 * yMin - lopta.Y; }
else if (lopta.Y > yMax) { lopta.Vy = -lopta.Vy; lopta.Y = 2 * yMax - lopta.Y; }
}
Invalidate();
}
Дајемо још два примера за самостално проучавање. Идеје које су коришћене су врло сличне онима у претходним примерима, тако да ћемо само кратко прокоментарисати нове детаље. Покушајте да разумете те идеје директно из кода.
Кретање кроз васиону¶
При ажурирању фрејма за сваку звезду израчунавамо њен нови положај и величину. Изабрали смо
да се звезда помера од центра слике за стоти део њеног тренутног растојања од центра слике.
да нови полупречник буде за један посто већи од претходног.
На тај начин се звезде повећавају размичу пред нама (и то све брже), тако да се постиже ефекат познат из филмова или видео игара, као да се крећемо напред између звезда. Напомињемо да формуле које су употребљене у програму за померање и повећавање кругова - звезда не одговарају теорији. Овде смо само хтели да добијемо жељени ефекат помоћу што једноставнијих формула. Примена теоријски исправних формула би резултирала нешто сложенијим програмом.
Киша¶
У овом примеру се (први пут) користи једна нова контрола, а то је оквир за слику, или у оригиналу PictureBox. Зато ћемо прво укратко описати ову контролу.
Оквир за слику (контрола PictureBox) се налази међу уобичајеним контролама кутије за алат (Toolbox \(\to\) Common Controls) и служи за приказивање слике. Додајемо га као и све друге контроле, кликом на контролу па на формулар.
Најважније својство ове контроле је својство Image, помоћу кога задајемо слику која ће се појавити у оквиру. Удобан и једноставан начин постављања слике је да прво убацимо припремљену слику у ресурсе програма, а онда у својство Image упишемо назив ресурса (у нашем примеру Kisa.Properties.Resources.Voda).
Осим својства Image, често се користи и својство Dock, које смо објаснили у примеру „Налажење фајлова” из лекције Практични примери . Пошто желимо да поставимо контролу преко целог формулара, својство Dock ћемо тако и подесити (кликом у централно поље или уписивањем речи Fill).
Оквир за слику има свој догађај Paint и овде смо користили функцију која одговара на догађај Paint ове контроле. Програмирање овог догађаја је исто као и код догађаја Paint формулара. Осим очигледне разлике у томе што сам догађај бирамо са списка догађаја контроле pictureBox1 и што се функција зове pictureBox1_Paint, треба још водити рачуна да у коду уместо Invalidate(); пишемо pictureBox1.Invalidate(); када је потребно поновно исцртавање.
Пре самог кода, поменимо и неке детаље битне за разумевање имплементације.
У овом програму користимо мале класе Kap и Kolut. Кап је задата положајем (x, y), дужином цртице којом је представљена (d пиксела) и завршним положајем (yMax). Колут је задат положајем (x, y), тренутном величином (хоризонтална полуоса r елипсе) и завршном величином (rMax).
У сваком новом фрејму капи се померају по 5 пиксела на ниже, а колутови (тј. њихове хоризонталне полуосе) се повећавају за по 5 пиксела. Када кап достигне свој најнижи положај, она нестаје а уместо ње се на истом месту појављује колут. Низ капи стално допуњавамо новим капима, али не одједном, јер би онда капи падале „у таласима”. Првом наредбом у сегменту
// dodaj nove kapi
int br_dodatih_kapi = Math.Min(5, MAX_BR_KAPI - Kapi.Count);
for (int i = 0; i < br_dodatih_kapi; i++)
{
int x = Rnd.Next(MARGINA, ClientWidth - MARGINA);
int y = 0;
int y_max = Rnd.Next(HORIZONT_Y + MARGINA, ClientHeight - MARGINA);
int d = Rnd.Next(5, 15);
noveKapi.Add(new Kap() { x = x, y = y, yMax = y_max, d = d });
}
ограничавамо број додатих капи на 5. Тиме постижемо да се „недостајуће” капи додају у листу мало по мало, тако да имамо капи на свим висинама.
На крају приче о овом примеру и једна занимљивост из времена писања овог програма:
Анегдота 😎
Овај програм је садржао скривену грешку и мало је недостајало да буде и објављен са том грешком. Наиме, програм је пуцао при минимизирању прозора (али то није примећено јер прозор није био минимизиран). Испоставило се да је разлог био управо у делу кода издвојеном горе. Конкретније, у наредбама које користе генератор случајних бројева за постављање вредности x и y_max је стајало ClientSize.Width и ClientSize.Height уместо садашњих константи ClientWidth и ClientHeight. Да ли вам је синула лампица? Проблем настаје зато што су вредности ClientSize.Width и ClientSize.Height код минимизираног прозора 0, што доводи до неисправног позива функције Rnd.Next. Да до овог проблема не би долазило, увели смо поменуте константе.
Поента ове анегдоте је да је тешко (а нарочито ако сте почетник) размишљати унапред о свему што може да се догоди у програму и предупредити све могуће проблеме. Ваши програми ће врло вероватно такође имати разне грешке, али то не треба да вас обесхрабри. За почетак - навикните се на грешке, а затим - навикните се да тестирате ваше програме да бисте те грешке откривали и исправљали.
Следи комплетан програм.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace Kisa
{
public partial class Form1 : Form
{
class Kap { public int x, y, yMax, d; };
class Kolut { public int x, y, r, rMax; };
Random Rnd = new Random();
List<Kap> Kapi = new List<Kap>();
List<Kolut> Kolutovi = new List<Kolut>();
const int MAX_BR_KAPI = 50;
const int MARGINA = 30;
const int HORIZONT_Y = 270;
const int ClientWidth = 640;
const int ClientHeight = 480;
Pen OlovkaZaKisu = new Pen(Color.FromArgb(46, 99, 113));
public Form1()
{
InitializeComponent();
Text = "Kiša";
BackColor = Color.Blue;
ClientSize = new Size(ClientWidth, ClientHeight);
}
private void timer1_Tick(object sender, EventArgs e)
{
List<Kap> noveKapi = new List<Kap>();
List<Kolut> noviKolutovi = new List<Kolut>();
// spusti kapi
foreach (var kap in Kapi)
{
if (kap.y + 20 <= kap.yMax)
noveKapi.Add(new Kap() { x = kap.x, y = kap.y + 20, yMax = kap.yMax, d = kap.d });
else
noviKolutovi.Add(new Kolut() { x = kap.x, y = kap.yMax, r = 10, rMax = 4 * kap.d });
}
// dodaj nove kapi
int br_dodatih_kapi = Math.Min(5, MAX_BR_KAPI - Kapi.Count);
for (int i = 0; i < br_dodatih_kapi; i++)
{
int x = Rnd.Next(MARGINA, ClientWidth - MARGINA);
int y = 0;
int y_max = Rnd.Next(HORIZONT_Y + MARGINA, ClientHeight - MARGINA);
int d = Rnd.Next(5, 15);
noveKapi.Add(new Kap() { x = x, y = y, yMax = y_max, d = d });
}
// povecaj kolutove
foreach (var kolut in Kolutovi)
{
if (kolut.r + 5 < kolut.rMax)
noviKolutovi.Add(new Kolut() { x = kolut.x, y = kolut.y, r = kolut.r + 5, rMax = kolut.rMax });
}
Kapi = noveKapi;
Kolutovi = noviKolutovi;
pictureBox1.Invalidate();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
foreach (var kap in Kapi)
g.DrawLine(OlovkaZaKisu, kap.x, kap.y, kap.x, kap.y + kap.d);
foreach (var kolut in Kolutovi)
{
int ra = kolut.r, rb = kolut.r / 3;
g.DrawEllipse(OlovkaZaKisu, kolut.x - ra, kolut.y - rb, 2 * ra, 2 * rb);
}
}
}
}
Надамо се да ћете после ових примера добити идеје за нове пројекте прављења анимација, који би вама били занимљиви.