蘇格拉底:“我知道我一無所知”
為了誰: 適合那些不關心所有開發人員並想玩他們的遊戲的 IT 人員!
關於什麼: 關於如何開始用 C/C++ 編寫遊戲,如果您突然需要它!
為什麼你應該讀這篇文章: 應用程序開發不是我的專業領域,但我每週都會嘗試編碼。 因為我愛遊戲!
你好我的名字是
電腦遊戲產業規模巨大,有傳言甚至比當今的電影產業還要大。 自計算機誕生以來,遊戲就已經被編寫出來,按照現代標準,使用複雜而基本的開發方法。 隨著時間的推移,具有已編程圖形、物理和聲音的遊戲引擎開始出現。 它們使您可以專注於開發遊戲本身,而不必擔心其基礎。 但隨著它們和引擎的出現,開發人員“失明”並退化。 遊戲的製作本身就被放到了傳送帶上。 產品的數量開始超過其質量。
與此同時,當我們玩別人的遊戲時,我們不斷受到別人想出的地點、情節、角色和遊戲機制的限制。 所以我意識到...
...是時候創造我自己的世界了,只受我管轄。 我是聖父、聖子和聖靈的世界!
我真誠地相信,通過編寫自己的遊戲引擎並在上面玩,您將能夠脫掉鞋子,擦窗戶併升級您的小屋,成為一名更有經驗和更全面的程序員。
在這篇文章中,我將嘗試告訴你我是如何開始用 C/C++ 編寫小遊戲的,開發過程是什麼樣的,以及我在繁忙的環境中如何找到時間來培養愛好。 它是主觀的,描述了個人開始的過程。 關於無知和信仰的材料,關於我個人對當前世界的看法。 換句話說,“政府不對你們的個人大腦負責!”
實踐
孔子“知而不行無益,行而不知則殆”
我的筆記本就是我的生命!
所以,在實踐中,我可以說對我來說一切都是從記事本開始的。 我不僅在那裡寫下我的日常任務,我還畫圖、編程、設計流程圖並解決問題,包括數學問題。 始終使用記事本並僅用鉛筆書寫。 恕我直言,它乾淨、方便、可靠。
我的(已經寫滿了)筆記本。 這就是他的樣子。 它包含日常任務、想法、繪圖、圖表、解決方案、黑簿、代碼等
在這個階段,我成功完成了三個項目(這是我對“完整性”的理解,因為任何產品都可以相對無休止地開發)。
- 項目0: 這是使用 Unity 遊戲引擎用 C# 編寫的 3D Architect 演示場景。 適用於 macOS 和 Windows 平台。
- 遊戲一 適用於 Windows 的控制台遊戲《Simple Snake》(大家都稱為“Snake”)。 用C寫成。
- 遊戲一 控制台遊戲《瘋狂坦克》(大家都稱為“坦克”),用 C++(使用類)編寫,也適用於 Windows。
項目 0. 架構師演示
3D 場景架構師演示
第一個項目不是用 C/C++ 實現的,而是使用 Unity 遊戲引擎用 C# 實現的。 該引擎對硬件的要求並不像
我在 Unity 中的目標不是開發遊戲。 我想創建一個帶有一些角色的 3D 場景。 他,或者更確切地說,她(我模仿了我所愛的女孩=)必須移動並與他周圍的世界互動。 重要的是要了解 Unity 是什麼、開發過程是什麼以及創建某些東西需要付出多少努力。 這就是 Architect Demo 項目的誕生(這個名字幾乎是憑空發明的)。 編程、建模、動畫、紋理花了我大約兩個月的日常工作。
我從 YouTube 上關於創建 3D 模型的教程視頻開始
對鎖骨和附加槓桿骨進行建模,使動畫看起來更自然。 學完這些課程後,您就會意識到動畫電影的創作者為了製作 30 秒的視頻需要付出多少努力。 但 3D 電影可以持續幾個小時! 然後我們離開電影院並說這樣的話:“那是一部糟糕的卡通/電影! 他們本可以做得更好……”傻瓜!
還有一件事是關於這個項目中的編程的。 事實證明,對我來說最有趣的部分是數學部分。 如果您運行場景(鏈接到項目描述中的存儲庫),您會注意到相機在球體中圍繞女孩角色旋轉。 為了對相機的這種旋轉進行編程,我必須首先計算圓(2D)上的位置點的坐標,然後計算球體(3D)上的位置點的坐標。 有趣的是,我在學校時討厭數學,但知道它的成績是 C-。 部分原因可能是因為在學校裡他們根本不會向你解釋這個數學到底是如何應用在生活中的。 但當你痴迷於你的目標、你的夢想時,你的頭腦就會變得清晰和開放! 你開始將困難的任務視為一次令人興奮的冒險。 然後你會想:“好吧,為什麼你最喜歡的數學家不能正常告訴你這些公式可以應用在哪裡?”
計算圓上和球上點坐標的公式的計算(來自我的筆記本)
遊戲 1. 簡單的貪吃蛇
簡單的貪吃蛇遊戲
3D 場景不是遊戲。 此外,對 3D 對象(尤其是角色)進行建模和動畫製作既耗時又困難。 在使用 Unity 後,我意識到我需要繼續,或者更確切地說,從基礎開始。 簡單而快速,但同時具有全局性的東西,以了解遊戲的結構。
什麼是簡單又快捷? 沒錯,控制台和 2D。 更準確地說,甚至是控制台和符號。 我再次在互聯網上尋找靈感(總的來說,我認為互聯網是XNUMX世紀最具革命性和危險性的發明)。 我挖出了一個製作控制台俄羅斯方塊的程序員的視頻。 為了模仿他的遊戲,我決定製作一條“蛇”。 從視頻中我了解了兩個基本知識 - 遊戲循環(具有三個基本功能/部分)和輸出到緩衝區。
遊戲循環可能看起來像這樣:
int main()
{
Setup();
// a game loop
while (!quit)
{
Input();
Logic();
Draw();
Sleep(gameSpeed); // game timing
}
return 0;
}
該代碼立即呈現了整個 main() 函數。 遊戲週期在適當的評論後開始。 循環中有三個基本函數:Input()、Logic()、Draw()。 首先輸入數據Input(主要是按鍵控制),然後對輸入的數據進行邏輯處理,然後輸出到屏幕上——Draw。 每一幀都是如此。 這就是動畫的創建方式。 就像動畫片裡那樣。 通常,處理輸入的數據需要花費最多的時間,並且據我所知,決定了遊戲的幀速率。 但這裡 Logic() 函數執行得非常快。 因此,你必須使用Sleep()函數和gameSpeed參數來控制幀速率,它決定了這個速度。
遊戲週期。 在記事本中編寫一條“蛇”
如果您正在開發基於角色的控制台遊戲,您將無法使用常規的“cout”流輸出將數據輸出到屏幕 - 它非常慢。 因此,輸出必鬚髮送到屏幕緩衝區。 這要快得多,並且遊戲運行不會出現任何故障。 老實說,我不太明白屏幕緩衝區是什麼以及它是如何工作的。 但我會在這裡給出一個示例代碼,也許有人可以在註釋中澄清情況。
獲取屏幕緩衝區(可以這麼說):
// create screen buffer for drawings
HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0,
NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
DWORD dwBytesWritten = 0;
SetConsoleActiveScreenBuffer(hConsole);
直接顯示某字符串scoreLine(分數顯示行):
// draw the score
WriteConsoleOutputCharacter(hConsole, scoreLine, GAME_WIDTH, {2,3}, &dwBytesWritten);
從理論上講,這個遊戲沒有什麼複雜的;我認為這是一個很好的入門級例子。 該代碼編寫在一個文件中,並格式化為多個函數。 沒有類,沒有繼承。 您可以訪問 GitHub 上的存儲庫,親自查看遊戲源代碼中的所有內容。
遊戲2.瘋狂坦克
- 平台: Windows(在 Windows 7、10 上測試)
- 語言: C + +中
- 遊戲引擎: Windows 控制台
- 靈感: книга
通過遊戲編程開始 C++ - 存儲庫:
GitHub上
遊戲瘋狂坦克
將角色打印到控制台可能是您可以將其變成遊戲的最簡單的事情。 但是這樣就出現了一個問題:符號的高度和寬度不同(高度大於寬度)。 這樣,一切都會看起來不成比例,向下或向上移動會比向左或向右移動快得多。 這種效果在《貪吃蛇》(遊戲 1)中非常明顯。 “坦克”(遊戲 2)沒有這個缺點,因為那裡的輸出是通過用不同顏色繪製屏幕像素來組織的。 你可以說我寫了一個渲染器。 確實,這有點複雜,但更有趣。
對於這個遊戲,描述我在屏幕上顯示像素的系統就足夠了。 我認為這是遊戲的主要部分。 你可以自己想出其他一切。
因此,您在屏幕上看到的只是一組移動的彩色矩形。
一組矩形
每個矩形由一個填充數字的矩陣表示。 順便說一句,我可以強調一個有趣的細微差別 - 遊戲中的所有矩陣都被編程為一維數組。 不是二維的,是一維的! 一維數組使用起來更加容易和快捷。
遊戲坦克矩陣示例
將游戲坦克矩陣表示為一維數組
將矩陣表示為一維數組的更直觀示例
但是對數組元素的訪問發生在雙循環中,就好像它不是一維數組,而是二維數組一樣。 這樣做是因為我們仍然使用矩陣。
在雙循環中遍歷一維數組。 Y - 行標識符,X - 列標識符
請注意:我使用標識符 x 和 y,而不是通常的矩陣標識符 i、j。 在我看來,這樣看起來更賞心悅目,也更容易被大腦理解。 此外,這種表示法使得可以方便地將所使用的矩陣投影到二維圖像的坐標軸上。
現在關於像素、顏色和屏幕輸出。 StretchDIBits 函數用於輸出(頭文件:windows.h;庫:gdi32.lib)。 除其他外,該函數接收以下信息:顯示圖像的設備(在我的例子中,它是 Windows 控制台)、圖像顯示的起始坐標、其寬度/高度以及圖像本身位圖的形式,由字節數組表示。 位圖作為字節數組!
StretchDIBits() 函數的實際應用:
// screen output for game field
StretchDIBits(
deviceContext,
OFFSET_LEFT, OFFSET_TOP,
PMATRIX_WIDTH, PMATRIX_HEIGHT,
0, 0,
PMATRIX_WIDTH, PMATRIX_HEIGHT,
m_p_bitmapMemory, &bitmapInfo,
DIB_RGB_COLORS,
SRCCOPY
);
使用 VirtualAlloc() 函數預先為此位圖分配內存。 也就是說,保留所需的字節數來存儲所有像素的信息,然後將這些信息顯示在屏幕上。
創建 m_p_bitmapMemory 位圖:
// create bitmap
int bitmapMemorySize = (PMATRIX_WIDTH * PMATRIX_HEIGHT) * BYTES_PER_PIXEL;
void* m_p_bitmapMemory = VirtualAlloc(0, bitmapMemorySize, MEM_COMMIT, PAGE_READWRITE);
粗略地說,位圖由像素的集合組成。 數組中的每四個字節就是一個 RGB 像素。 每個紅色值 XNUMX 個字節,每個綠色值 (G) XNUMX 個字節,每個藍色值 (B) XNUMX 個字節。 另外,還剩下一個字節用於縮進。 這三種顏色 - 紅/綠/藍 (RGB) - 以不同的比例相互混合以創建最終的像素顏色。
現在,每個矩形或遊戲對像都由數字矩陣表示。 所有這些遊戲對像都放置在一個集合中。 然後將它們放在比賽場地上,形成一個大的數值矩陣。 我將矩陣中的每個數字與特定的顏色相關聯。 例如,數字8對應藍色,數字9對應黃色,數字10對應深灰色,等等。 因此,我們可以說我們有一個比賽場地矩陣,其中每個數字都是一種顏色。
因此,我們的一側有整個比賽場地的數字矩陣,另一側有用於顯示圖像的位圖。 到目前為止,位圖是“空的”——它還不包含有關所需顏色的像素的信息。 這意味著最後一步將是根據比賽場地的數字矩陣使用每個像素的信息填充位圖。 下圖是這種轉變的一個明顯例子。
使用基於比賽場地數字矩陣的信息填充位圖(像素矩陣)的示例(顏色索引與遊戲中的索引不匹配)
我還將展示遊戲中的一段真實代碼。 循環每次迭代時的變量 colorIndex 都會被分配來自遊戲場數字矩陣 (mainDigitalMatrix) 的值(顏色索引)。 然後根據索引將顏色變量設置為顏色本身。 然後將所得顏色按紅、綠、藍 (RGB) 的比例進行劃分。 並且與pixelPadding一起,這些信息被一遍又一遍地寫入像素,在位圖中形成彩色圖像。
該代碼使用指針和按位運算,這可能很難理解。 因此,我建議您單獨閱讀此類結構的工作原理。
根據比賽場地的數字矩陣填充位圖信息:
// set pixel map variables
int colorIndex;
COLORREF color;
int pitch;
uint8_t* p_row;
// arrange pixels for game field
pitch = PMATRIX_WIDTH * BYTES_PER_PIXEL; // row size in bytes
p_row = (uint8_t*)m_p_bitmapMemory; //cast to uint8 for valid pointer arithmetic
(to add by 1 byte (8 bits) at a time)
for (int y = 0; y < PMATRIX_HEIGHT; ++y)
{
uint32_t* p_pixel = (uint32_t*)p_row;
for (int x = 0; x < PMATRIX_WIDTH; ++x)
{
colorIndex = mainDigitalMatrix[y * PMATRIX_WIDTH + x];
color = Utils::GetColor(colorIndex);
uint8_t blue = GetBValue(color);
uint8_t green = GetGValue(color);
uint8_t red = GetRValue(color);
uint8_t pixelPadding = 0;
*p_pixel = ((pixelPadding << 24) | (red << 16) | (green << 8) | blue);
++p_pixel;
}
p_row += pitch;
}
根據上述方法,在《瘋狂坦克》遊戲中,在Draw()函數中形成一張圖片(幀)並顯示在屏幕上。 在Input()函數中註冊擊鍵並在Logic()函數中進行後續處理後,形成一個新的圖片(幀)。 確實,遊戲對象可能已經在比賽場地上具有不同的位置,因此,被繪製在不同的位置。 這就是動畫(運動)的發生方式。
理論上(如果我沒有忘記任何事情的話),理解第一個遊戲(“貪吃蛇”)的遊戲循環和第二個遊戲(“坦克”)在屏幕上顯示像素的系統就是你編寫任何遊戲所需的全部內容。 Windows 下的 2D 遊戲。 無聲無息! 😉 其餘部分只是幻想而已。
當然,《坦克》這款遊戲比《貪吃蛇》複雜得多。 我已經使用了C++語言,即我用類來描述不同的遊戲對象。 我創建了自己的集合 - 代碼可以在 headers/Box.h 中查看。 順便說一句,該集合很可能存在內存洩漏。 使用的指針。 憑記憶工作。 我必須說這本書對我幫助很大 通過遊戲編程開始 C++。 對於 C++ 初學者來說,這是一個很好的開始。 它很小,很有趣,而且組織得很好。
開發這款遊戲大約花了六個月的時間。 我主要在工作時的午餐和小吃期間寫作。 他坐在辦公室的廚房裡,一邊吃食物一邊寫代碼。 或者在家吃晚飯的時候。 所以我結束了這些“廚房戰爭”。 一如既往,我積極使用筆記本,所有概念性的東西都誕生在其中。
為了完成實踐部分,我將掃描我的筆記本。 為了展示我到底寫下、畫下、計算過、設計過什麼……
設計坦克的圖像。 並確定每個坦克應在屏幕上佔據多少像素
罐體繞軸旋轉的算法和公式的計算
我的收藏方案(最有可能存在內存洩漏的方案)。 集合是根據Linked List類型創建的
而這些都是將人工智能附加到遊戲中的徒勞嘗試
理論
“行萬里路,始於足下”(中國古人智慧)
讓我們從實踐轉向理論! 如何找到時間從事自己的愛好?
- 確定你真正想要什麼(唉,這是最難的部分)。
- 設定優先事項。
- 為了更高的優先級而犧牲一切“額外”的東西。
- 每天都朝著目標前進。
- 不要指望有兩三個小時的空閒時間可以花在愛好上。
一方面,您需要確定自己想要什麼並確定優先順序。 另一方面,可以放棄一些活動/項目以支持這些優先事項。 換句話說,你將不得不犧牲“額外”的一切。 我在哪裡聽說過,生活中最多應該有三項主要活動。 然後您將能夠以最高的質量完成它們。 額外的項目/方向將開始超載。 但這可能都是主觀的和個人的。
有一條黃金法則:永遠不要有 0% 的日子! 我在一位獨立開發者的文章中了解到了這一點。 如果你正在做一個項目,每天都做一些事情。 你做了多少並不重要。 寫一個字或一行代碼,觀看一個教程視頻或在板上釘一顆釘子 - 只是做某事。 最難的是開始。 一旦開始,你最終可能會做得比你想要的多一點。 這樣你就會不斷地朝著你的目標前進,相信我,很快。 畢竟,所有事情的主要障礙就是拖延。
重要的是要記住,您不應該低估和忽視5、10、15分鐘時間的免費“木屑”,等待一些持續一兩個小時的大“日誌”。 你在排隊嗎? 為你的項目考慮一些事情。 乘坐自動扶梯? 在記事本上寫下一些東西。 您乘坐公共汽車旅行嗎? 太好了,讀一些文章。 充分利用每一個機會。 別再在 YouTube 上看貓和狗了! 別污染你的大腦!
最後一件事。 如果讀完本文後,您喜歡不使用遊戲引擎來創建遊戲的想法,那麼請記住 Casey Muratori 這個名字。 這傢伙有
凱西還解釋說,通過開發自己的遊戲引擎,您將對任何現有引擎有更好的了解。 在每個人都試圖實現自動化的框架世界中,您學習的是創建而不是使用。 您了解計算機的本質。 而且你也將成為一名更加聰明和成熟的程序員——一名專業人士。
祝你在選擇的道路上好運! 讓我們讓世界變得更加專業。
作者: 格蘭金·安德烈 、開發運營
來源: www.habr.com