Win Form图形编程实践——打砖块
Win Form圖形編程實踐——打磚塊
引言
本項目學習自際為軟件事務所的C#實現一個打磚塊游戲 Step By Step,特此鳴謝。
最初,博主只是在簡單學習了Win Form之后寫一個有圖形化界面的小游戲來鍛煉一下自己的圖形化編程技巧,思前想后選擇了打磚塊這個游戲。相較于貪吃蛇一類的小游戲,打磚塊還是很有難度的,因為要控制球的運動軌跡以及球和磚塊、擋板的碰撞問題,設計起來較為繁瑣。幸好有上文提到的教程,在OOP方面提供了一個很好的思路,再次感謝。
另:為避免版權問題,游戲中所有涉及到的圖片及圖標等均為個人繪制。
思路
在尋找教程之前我自己寫了一個導圖來整理這個游戲的大體思路。如下:
整合網絡教程以及我個人的思路,實現這個打磚塊游戲的具體過程如下:
- 設計開始界面,具體如導圖所示
- 標題
- 操作提示
- [開始游戲]按鈕
- 開始界面背景圖
- 設計游戲界面,主要參照教程進行:
- 游戲界面背景圖
- 繪制擋板以及實現擋板的移動
- 繪制小球以及實現小球的移動
- 繪制磚塊以及實現墻體集合
- 重新使用雙緩沖技術實現繪圖操作
- 實現小球與磚塊和擋板的碰撞檢測
- 設計展示界面,具體如導圖所示:
- 計分板
- 游戲進行時間
- 排行榜
- 設計游戲結束界面,具體如導圖所示:
- 標題
- 判斷分數
- 如果進入前三名:記錄玩家姓名并更新排行榜后展示[結束游戲]按鈕
- 如果沒進入前三名:直接展示[結束游戲]按鈕
- [結束游戲]按鈕
實現過程
開始界面
設置窗口格式
將AutoSize設置為False,Size為450x620.
設置窗口圖標。
將Locked設置為True。
設置窗口內默認字體,這樣在添加Label或者TextBox時就不用重新設置字體了,但有可能仍需要重新調整字體大小。
設置背景圖片
將圖片文件夾放置在所建項目文件夾中的bin\Debug中便于檢索文件。
將這個操作放置在[Form]BricksBreaker的Load事件中,具體代碼如下:
private void BricksBreaker_Load(object sender, EventArgs e){Welcome();string backgroundImagePath = Application.StartupPath;backgroundImagePath += @"\imgs\Welcome\BackGround.png";this.BackgroundImage = Image.FromFile(backgroundImagePath);this.BackgroundImageLayout = ImageLayout.Stretch;}Systems.Windows.Forms.Application.StartupPath是運行時.exe文件的位置,采用相對路徑可以方便后續實現安裝的操作。
Welcome()函數
最開始是為了便于操作主界面以及實現[再來一局]按鈕創造的函數,將各個組件的初始化放在了這個函數中,但是進行到左后發現其實并不能簡化而且難以在復雜的Welcome()函數中分離出適合于第二局游戲的語句,便擱置在此。
private void Welcome(){GameStart.Visible = true;GameTitle.Visible = true;Hint.Visible = true;GameBox.Visible = false;Suggestion.Visible = false;ScoreBoard.Visible = false;ScoreRec.Visible = false;PlayersTitle.Visible = false;BestPlayers.Visible = false;GameTime.Visible = false;GameOverTitle.Visible = false;NameRecTitle.Visible = false;NameRec.Visible = false;NameConfirm.Visible = false;Close.Visible = false;}[Label]GameStart為[開始游戲]按鈕,[Label]GameTitle為標題,[Label]Hint為操作提示。
其他為后續實現過程中逐漸添加的語句,故不在此敘述。
[Label]GameStart的Click事件
private void GameStart_Click(object sender, EventArgs e){GameStartFunc();this.GameBox.Refresh();}private void GameStartFunc(){GameStart.Visible = false;GameTitle.Visible = false;Hint.Visible = false;string gamePageImagePath = Application.StartupPath;gamePageImagePath += @"\imgs\GamePage\GamePage.png";GameBox.BackgroundImage = Image.FromFile(gamePageImagePath);GameBox.BackgroundImageLayout = ImageLayout.Stretch;GameBox.Visible = true;Suggestion.Visible = true;ScoreBoard.Visible = true;ScoreRec.Visible = true;PlayersTitle.Visible = true;BestPlayers.Visible = true;GameTime.Visible = true;objectList.Add(board);objectList.Add(ball);objectList.Add(bricks);string nameListPath = Application.StartupPath;nameListPath += @"\data\PlayersNameList.txt";System.IO.StreamReader streamReader = new System.IO.StreamReader(nameListPath);BestPlayers.Text = streamReader.ReadToEnd();streamReader.Close();players.Add(new Player(BestPlayers.Lines[0]));players.Add(new Player(BestPlayers.Lines[1]));players.Add(new Player(BestPlayers.Lines[2]));}同樣的GameStartFunc()也是為了方便[再來一局]按鈕的事件而做出來的冗余函數(lll¬ω¬)。
部分語句為后續添加。
游戲界面
建立[PictureBox]GameBox
[PictureBox]GameBox的相關屬性:
Dock: True
Size: 432x573
Locked: True
GameStartFunc()內相關初始化語句為:
string gamePageImagePath = Application.StartupPath; gamePageImagePath += @"\imgs\GamePage\GamePage.png"; GameBox.BackgroundImage = Image.FromFile(gamePageImagePath); GameBox.BackgroundImageLayout = ImageLayout.Stretch; GameBox.Visible = true;選擇PictureBox作為圖形的載體,在[PictureBox]GameBox的Paint事件中置入各個組件的繪制語句。
private void GameBox_Paint(object sender, PaintEventArgs e){Bitmap bitmap = new Bitmap(GameBox.Width, GameBox.Height);foreach(Object @object in objectList){// @object.Draw(e.Graphics, this.GameBox);@object.Draw(Graphics.FromImage(bitmap), this.GameBox);}e.Graphics.DrawImage(bitmap, 0, 0);}其中[List]objectList為各個Object的集合,通過foreach簡化代碼。
重繪[PictureBox]GameBox可以用GameBox.Refresh()實現。
(注釋部分為直接繪制,未注釋部分為雙緩沖操作)
添加一個計時器[Timer]Action,利用其Tick事件來刷新圖形以及控制刷新頻率:
private void Action_Tick(object sender, EventArgs e){ball.Run(GameBox, board, bricks, ref score);if (ball.touchedBound){Action.Stop();GameOver();}GameBox.Refresh();}同時Tick事件中還可以添加其他不同功能的語句,將會在后續過程中解釋。
在[PictureBox]GameBox中繪制圖形
Object類
在編寫Board類和Ball類后將相似代碼提取出來構造一個基類Object類。
class Object{public int xPos { get; set; }public int yPos { get; set; }public Rectangle rectangle { get; set; }public bool isDelete = false;public virtual void Draw(Graphics g, PictureBox GameBox) { }}xPos, yPos為Ball類和Board類的相似變量,使用方法也大致相同。
另附:C#值Get、Set用法(作者:雨夜瀟瀟)
[Rectangle]rectangle為繪制圖形時所需的變量
為Object類構建一個Draw虛方法之后可以在[PictureBox]GameBox的Paint事件中達到簡化代碼的作用,即不必每個組件依次使用一條語句來調用各自類中不同的Draw()方法。
Board類
Board類有幾個基本參數:橫坐標、縱坐標、寬度、高度以及移動速度。
class Board : Object{public int boardWidth, boardHeight;public int speedX { get; set; }public const double collsion_MaxAngleIncrement = 0.25;public enum BoardDirection{Left, Right, None}public Board(int x, int y, int width, int height, int speedx=8){this.xPos = x;this.yPos = y;this.speedX = speedx;boardWidth = width;boardHeight = height;}public override void Draw(Graphics g, PictureBox GameBox){SolidBrush solidBrush = new SolidBrush(Color.BurlyWood);Pen pen = new Pen(Color.SaddleBrown, 2);rectangle = new Rectangle(GameBox.Left + xPos, GameBox.Top + yPos, boardWidth, boardHeight);g.DrawRectangle(pen, rectangle);g.FillRectangle(solidBrush, rectangle);}public void Move(BoardDirection direction, PictureBox GameBox){switch (direction){case BoardDirection.Left:if(xPos - speedX > BricksBreaker.BorderWidth){xPos -= speedX;}else{xPos = BricksBreaker.BorderWidth;}break;case BoardDirection.Right:if(xPos + boardWidth + BricksBreaker.BorderWidth + speedX < GameBox.Width){xPos += speedX;}else{xPos = GameBox.Width - boardWidth - BricksBreaker.BorderWidth;}break;}}}其中的Move()方法利用Board類中的枚舉變量確定擋板的移動方向。BricksBreaker.BorderWidth為[Form]BricksBreaker中定義的常量,表示游戲界面中邊框的寬度。
Move()方法的調用在[Form]BricksBreaker的KeyDown事件中:
private void BricksBreaker_KeyDown(object sender, KeyEventArgs e){if (GameBox.Visible){switch (e.KeyData){case Keys.A:if (gameStarted){board.Move(Board.BoardDirection.Left, GameBox);}break;case Keys.D:if (gameStarted){board.Move(Board.BoardDirection.Right, GameBox);}break;case Keys.Space:if (!gameStarted){gameStarted = true;Suggestion.Visible = false;Action.Interval = 100;Action.Start();}break;default:break;}// GameBox.Refresh();}}同時這個KeyDown事件中還實現了[開始游戲]按鈕的后續——按空格鍵發射小球,使用gameStarted判斷游戲是否已經正式開始。
起初擋板的繪制語句是在KeyDown事件中的,在擋板坐標改變后直接在[PictureBox]GameBox中繪制擋板,在實現雙緩沖時將語句移動到其Load事件中。
Ball類
Ball類的參數與Board類相似但略有不同:橫坐標、縱坐標、半徑、速度方向以及速度大小。
采用向量而不是坐標增量的方式表示速度有利于控制小球的方向。因為在此之前有設想過擋板的不同位置對小球的速度影響不同,即距離擋板中心越遠速度偏離越大,若是使用坐標增量的方式表示速度可能計算比較復雜或者不好控制之類的,而使用向量的方式表示速度則可以直接在代表速度方向的變量上加減。
class Ball : Object{public const int radius = 8;public double speedAngle { get; set; }public int speedDis { get; set; }public bool touchedBound = false;private const int speedIncrement = 1;public Ball(int x, int y, double angle=0.5, int dis = 5){this.xPos = x;this.yPos = y;this.speedAngle = angle;this.speedDis = dis;}public void SpeedUpdate(){this.speedDis += speedIncrement;}public override void Draw(Graphics g, PictureBox GameBox){SolidBrush solidBrush = new SolidBrush(Color.LightGoldenrodYellow);Pen pen = new Pen(Color.SaddleBrown, 2);rectangle = new Rectangle(GameBox.Left + xPos - radius, GameBox.Top + yPos - radius, 2 * radius, 2 * radius);g.DrawEllipse(pen, rectangle);g.FillEllipse(solidBrush, rectangle);}public void Run(PictureBox GameBox, Board board, Bricks bricks, ref int score){int xDis = (int)(speedDis * Math.Cos(Math.PI * speedAngle));int yDis = -(int)(speedDis * Math.Sin(Math.PI * speedAngle));xPos += xDis;yPos += yDis;// Hit(board, bricks, ref score);if (xPos + radius + BricksBreaker.BorderWidth >= GameBox.Width){xPos = GameBox.Width - BricksBreaker.BorderWidth - radius;speedAngle = 1.0 - speedAngle;}if(xPos - radius <= BricksBreaker.BorderWidth){xPos = BricksBreaker.BorderWidth + radius;speedAngle = 1.0 - speedAngle;}if(yPos + radius >= board.yPos + board.boardHeight){yPos = board.yPos + board.boardHeight - radius;speedAngle = -speedAngle;this.touchedBound = true;}if(yPos - radius <= BricksBreaker.BorderWidth){yPos = BricksBreaker.BorderWidth + radius;speedAngle = -speedAngle;}}}其中Ball類的Draw()方法與Board類類似,只是顏色有所改變。
SpeedUpdate()方法是為后續設計關卡準備的。
Run()方法為小球的基本移動方法,具體有坐標移動和檢測與邊界的碰撞。
在后面會實現Ball類與擋板以及磚塊的碰撞檢測方法。
Brick類/Bricks類
Brick類與Board類基本一致。
class Brick : Object{public const int brickWidth = 67, brickHeight = 20;public const int score = 5;public Brick() { }public Brick(int x, int y){xPos = x;yPos = y;}public override void Draw(Graphics g, PictureBox GameBox){SolidBrush solidBrush = new SolidBrush(Color.FromArgb(181, 99, 0));Pen pen = new Pen(Color.SaddleBrown, 4);rectangle = new Rectangle(GameBox.Left + xPos, GameBox.Top + yPos, brickWidth, brickHeight);g.DrawRectangle(pen, rectangle);g.FillRectangle(solidBrush, rectangle);}}對磚塊的集合單獨創建一個Bricks類,以便在[List]objectList中調用Draw()方法。
初始化函數用來創建一個磚墻。
Draw()方法中分別調用每一個磚塊的Draw()方法。
class Bricks : Object{private const int _width = 402+BricksBreaker.BorderWidth, _height = 140+BricksBreaker.BorderWidth;public const int width = 6, height = 7;public List<Brick> bricks { get; set; }public Bricks(){MakeBrickWall();}private void MakeBrickWall(){bricks = new List<Brick>();for (int y = 15; y < _height; y += Brick.brickHeight) {for (int x = 15;x < _width;x += Brick.brickWidth){Brick brick = new Brick(x, y);bricks.Add(brick);}}}public override void Draw(Graphics g, PictureBox GameBox){foreach(Brick brick in bricks){if(brick.isDelete){continue;}brick.Draw(g, GameBox);}}}雙緩沖技術
雙緩沖技術:
即在內存中創建一個與屏幕繪圖區域一致的對象,先將圖形繪制到內存中的這個對象上,再一次性將這個對象上的圖形拷貝到屏幕上,這樣能大大加快繪圖的速度。
雙緩沖技術在C#中的實現過程:
先創建一個Bitmap實例bitmap,大小與[PictureBox]GameBox相同。
Bitmap bitmap = new Bitmap(GameBox.Width, GameBox.Height);將本來要繪制在[PictureBox]GameBox中的圖形繪制到[Bitmap]bitmap中。
foreach(Object @object in objectList){// @object.Draw(e.Graphics, this.GameBox);@object.Draw(Graphics.FromImage(bitmap), this.GameBox);}用DrawImage將[Bitmap]bitmap繪制到[PictureBox]GameBox中。
整體代碼如下:
private void GameBox_Paint(object sender, PaintEventArgs e){Bitmap bitmap = new Bitmap(GameBox.Width, GameBox.Height);foreach(Object @object in objectList){@object.Draw(Graphics.FromImage(bitmap), this.GameBox);}e.Graphics.DrawImage(bitmap, 0, 0);}Ball類的碰撞檢測
Ball類的碰撞檢測分為兩個部分,一是和擋板的碰撞,二是和磚塊的碰撞。兩種碰撞分別會產生不同的效果。
對磚塊的碰撞會造成磚塊的刪除和分數的增加,同時小球方向改變。
對擋板的碰撞會使小球方向改變,且改變的多少隨碰撞位置的變化而變化。
具體代碼如下:
private void Hit(Board board, Bricks brickset, ref int score){for (int i = 0; i < brickset.bricks.Count; i++){if (brickset.bricks[i].isDelete){continue;}int leftBound = brickset.bricks[i].xPos;int rightBound = leftBound + Brick.brickWidth;int upBound = brickset.bricks[i].yPos;int downBound = upBound + Brick.brickHeight;if(xPos < leftBound && xPos + radius >= leftBound){if(yPos > upBound && yPos < downBound){xPos = leftBound - radius;speedAngle = 1.0 - speedAngle;brickset.bricks[i].isDelete = true;score += Brick.score;}if(yPos == downBound){xPos = leftBound - radius;speedAngle = 1.0 - speedAngle;brickset.bricks[i].isDelete = true;score += Brick.score;if (i + Bricks.width < brickset.bricks.Count && !brickset.bricks[i + Bricks.width].isDelete){brickset.bricks[i + Bricks.width].isDelete = true;score += Brick.score;}}}else if(xPos > rightBound && xPos - radius <= rightBound){if(yPos > upBound && yPos < downBound){xPos = rightBound + radius;speedAngle = 1.0 - speedAngle;brickset.bricks[i].isDelete = true;score += Brick.score;}if (yPos == downBound){xPos = rightBound + radius;speedAngle = 1.0 - speedAngle;brickset.bricks[i].isDelete = true;score += Brick.score;if (i + Bricks.width < brickset.bricks.Count && !brickset.bricks[i + Bricks.width].isDelete){brickset.bricks[i + Bricks.width].isDelete = true;score += Brick.score;}}}else if(yPos < upBound && yPos + radius >= upBound){if(xPos > leftBound && xPos < rightBound){yPos = upBound - radius;speedAngle = -speedAngle;brickset.bricks[i].isDelete = true;score += Brick.score;}if(xPos == rightBound){yPos = upBound - radius;speedAngle = -speedAngle;brickset.bricks[i].isDelete = true;score += Brick.score;if(i % Bricks.width < 5 && !brickset.bricks[i + 1].isDelete){brickset.bricks[i + 1].isDelete = true;score += Brick.score;}}}else if(yPos > downBound && yPos - radius <= downBound){if (xPos > leftBound && xPos < rightBound){yPos = downBound + radius;speedAngle = -speedAngle;brickset.bricks[i].isDelete = true;score += Brick.score;}if (xPos == rightBound){yPos = downBound + radius;speedAngle = -speedAngle;brickset.bricks[i].isDelete = true;score += Brick.score;if (i % Bricks.width < 5 && !brickset.bricks[i + 1].isDelete){brickset.bricks[i + 1].isDelete = true;score += Brick.score;}}}}if (yPos < board.yPos && yPos + radius >= board.yPos && xPos >= board.xPos && xPos <= board.xPos + board.boardWidth){yPos = board.yPos - radius - 1;speedAngle = -speedAngle;int dis = board.xPos + board.boardWidth / 2 - xPos;double angleIncrement = (double)dis / (double)board.boardWidth * Board.collsion_MaxAngleIncrement;speedAngle += angleIncrement;}}判斷過程比較繁瑣,與對邊界的碰撞判斷相同,為了防止出現圖像穿模的情況使用類似:
if(*** <= **){*** = **; }的方式強制使圖形不會越界。
展示界面
展示界面分別使用[Label]ScoreRec, [Label]GameTime, [TextBox]BestPlayers來分別表示當前分數,游戲時間,排行榜。
分數實時顯示
分數的實時顯示在[Timer]Action的Tick事件中實現:
timeScore = (timeScore + 1) % timeScoreLoop; if(timeScore == 0) {score++; }ScoreRec.Text = score.ToString("D5");其中score、timeScore和timeScoreLoop為[Form]BricksBreaker中定義的整型變量:
int score = 0; private int timeScore = 0; private const int timeScoreLoop = 15;由于游戲規則設定為分數隨游戲時間的增加和磚塊數量的減少而增加,而每次如果調用Tick()事件時都使score增加會降低不少游戲難度,所以設置timeScoreLoop來控制score隨游戲時間增加的速度。
游戲時間展示
游戲時間利用每次調用Tick事件時的絕對時間和游戲正式開始時的絕對時間做減法得到一個相對時間,最后將其轉換為可以用來展示的格式。
具體實現過程如下:
在[Form]BricksBreaker中定義一個[DateTime]beginTime用來表示游戲正式開始時的絕對時間:
DateTime beginTime = new DateTime();在KeyDown事件中,將[DateTime]beginTime設置為當前時間:
case Keys.Space:if (!gameStarted){gameStarted = true;Suggestion.Visible = false;beginTime = System.DateTime.Now;Action.Interval = 100;Action.Start();}break;在Tick事件中,利用[DateTime]nowTime與[DateTime]beginTime的差值,與DateTime中的最小時間相加將[TimeSpan]gameTime轉換為可以轉換為表示時間的字符串的[DateTime]類型。
DateTime nowTime = System.DateTime.Now; TimeSpan gameTime = nowTime - beginTime; GameTime.Text = System.DateTime.MinValue.AddMilliseconds(gameTime.TotalMilliseconds).ToLongTimeString();排行榜界面
在GameStartFunc函數中添加如下語句:
string nameListPath = Application.StartupPath; nameListPath += @"\data\PlayersNameList.txt"; System.IO.StreamReader streamReader = new System.IO.StreamReader(nameListPath); BestPlayers.Text = streamReader.ReadToEnd(); streamReader.Close();其中nameListPath表示排行榜文件所在位置。
結束界面
當Tick事件中檢測到[Ball]ball的touchedBound為True時,結束[Timer]Action的計時,并啟動GameOver函數:
private void GameOver(){GameOverTitle.Visible = true;}為了便于判斷玩家是否進入排行榜,進行以下操作:
創建一個Player類并重載它的ToString()方法:
class Player{public string name;public int score;public Player(string line){string[] info = line.Split(' ');this.name = info[1];this.score = Convert.ToInt32(info[2]);}public Player(string nam, int scor){this.name = nam;this.score = scor;}public override string ToString(){return name + " " + score.ToString("D5");}}在[Form]BricksBreaker中定義一個[List]players:
List<Player> players = new List<Player>();在GameStartFunc函數中添加如下語句進行初始化:
players.Add(new Player(BestPlayers.Lines[0])); players.Add(new Player(BestPlayers.Lines[1])); players.Add(new Player(BestPlayers.Lines[2]));在[Form]BricksBreaker中定義一個變量playerRank用來表示玩家的排名:
private int playerRank = -1;在GameOver函數中添加如下語句:
int index = 0; for(;index < players.Count; index++) {if(score >= players[index].score){for(int i = players.Count - 1;i > index; i--){players[i] = players[i - 1];}playerRank = index;NameRecTitle.Visible = true;NameRec.Visible = true;NameConfirm.Visible = true;break;} } if(playerRank == -1) {GameOverButtons(); }如果玩家沒有進入排行榜則直接展示[結束游戲]按鈕。
private void GameOverButtons(){NameRecTitle.Visible = false;NameRec.Visible = false;NameConfirm.Visible = false;Close.Visible = true;}[結束游戲]按鈕(即[Button]Close)的Click事件:
private void Close_Click(object sender, EventArgs e){this.Close();}如果玩家進入排行榜則展示[Label]NameRecTitle、[TextBox]NameRec和[Button]NameConfirm,進行玩家名稱的輸入。
在[Button]NameConfirm的Click事件中添加如下語句:
private void NameConfirm_Click(object sender, EventArgs e){string playerName = NameRec.Text;players[playerRank] = new Player(playerName, score);string[] lines = new string[players.Count];for(int i = 0;i < players.Count; i++){lines[i] = (i + 1).ToString() + ". " + players[i].ToString();}string nameListPath = Application.StartupPath;nameListPath += @"\data\PlayersNameList.txt";System.IO.File.WriteAllLines(nameListPath, lines);System.IO.StreamReader streamReader = new System.IO.StreamReader(nameListPath);BestPlayers.Text = streamReader.ReadToEnd();streamReader.Close();GameOverButtons();}將新輸入的玩家名稱保存到排行榜文件中同時更新排行榜界面。
制作安裝包
利用Visual Studio 2019中的Microsoft Visual Studio Installer Projects拓展制作游戲安裝包。
首先,新建一個Setup Wizard項目,項目名稱即為最后生成的安裝包的名稱。
然后在Application Folder中添加所安裝的應用程序以及相關文件,注意應和程序調用文件時使用的的相對路徑保持一致。在添加應用程序文件之前可以使用Resource Hacker更換一下圖標文件。
最后在解決方案資源管理器中右鍵項目,然后選擇“生成”,便大功告成了!
Github
Github代碼地址
總結
以上是生活随笔為你收集整理的Win Form图形编程实践——打砖块的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 全球与中国带灯轻触开关市场现状及未来发展
- 下一篇: DM365 dvsdk_4_02_00_