天天看點

OpenGL2D小遊戲——是男人就下100層

是男人就下100層想必大家一定都玩過,在這裡給大家簡單介紹一下遊戲規則。

遊戲規則:

遊戲人物從螢幕上方按一定速率下落,同時台階從螢幕下方随機産生,産生之後向上按一定速率垂直上升。人物踩到台階後會跟随台階一起上升,但是人物觸碰到頂部尖刺或者跌入底部會死亡,進而停止遊戲。 

這裡台階一共會有五種。

OpenGL2D小遊戲——是男人就下100層

其中接觸到一般台階會回複三分之一的生命值,加滿為止,碰到尖刺台階會減少三分之一的生命值,減完為止,崩潰台階隻要一接觸就會消失,進而人物繼續下降,但是不會少血,電梯台階有兩種,一種向左,一種向右,人物落上去即使不用方向鍵操作,也會快速向左向右移動。所有台階位置都是随機産生的。

從以上遊戲玩法中我們可以看出制作這個遊戲所要解決的核心問題有以下這麼幾點:

1.如何在随機的位置産生台階。

2.如何區分台階種類并分别進行貼圖。

3.如何進行碰撞檢測,當然,這是最複雜的一個問題,因為一共有五種台階,接觸之後會産生不同反應,但是,實作的方式并沒有想象中的那麼複雜,一會兒我将會為大家揭曉謎底。

4.如何控制血量。

5.如何結束遊戲,遊戲結束的條件。

首先我們先不要急于解決這些問題,因為這是一個遊戲,是遊戲,就會有許多實體産生,比如遊戲人物,台階,邊框,背景,血槽等,所有這些物體,必須找到承載他們的空間,就好比做飯,不論你是做何等的山珍海味,都必須用到鍋碗瓢盆,大米白面這些最基本的東西。

好了,先來看看我們要用到的頭檔案:

随後是一些基本常數設定,比如視窗大小,台階長度,台階寬度,有了這些常數,我們就可以一次性更改代碼裡所有的關于這些常量的部分。

然後我們就開始定義我們的遊戲中出現的物體了,首先是坐标結構體,有了它,表示坐标和渲染圖形時會很友善。這裡注意我們将該結構體定義為兩種資料類型,一種是該結構體本身的類型,另一種是它的指針類型。

然後定義台階專屬的資料類型

這裡的台階是用坐标表示的,是以用上面定義的表示坐标的Point類型來建造大小為4的一個數組,分别表示左上,右上,左下,右下四個點,還有一個實數來表示該台階的類型,這個實數由随機函數産生,好的,五種台階如何表示這個問題我們我們已經解決了,就是用這簡簡單單的一個數字,但是不能小瞧這一數字,它在以後區分五種碰撞效果,以及對五種台階分别進行貼圖時,起着至關重要的作用。

分割線來一條:

其實在我公布結果之前,你腦中也許已經有了一種方案來表示台階的種類了,以前我還想過用C++裡的vector動态數組,也就是在代碼開頭加上#include<vector>,然後聲明五條vector連結清單,用push_back()來進行添加,這種方法最後破産了,因為我不會删除連結清單裡的元素(呵呵~),也就是說我無法使得已經到達頂端的台階删除,但我不可能放任台階無限增加,那樣記憶體會占用越來越多,最後導緻崩潰,當然這樣的遊戲表面看起來并沒有什麼不同,即使不删除台階,當它們上升到頂端超出視窗範圍時,他們也會消失在你視線裡,但是它們所占用的記憶體并沒有被傳回,這樣它也就稱不上是一款完整的遊戲。

在這裡,我其實想說(當然我不是告訴大家要細心,這個上課老師會強調的)寫代碼就如同寫文章,或者是繪畫,一千個人動筆就會有一千種結果,這就是程式的魅力所在,用你自己所創造的方法,得到滿意結果的那一瞬間,不知道大家是不是,反正我是有無比的成就感的,就好像你玩了一年的英雄聯盟賬号終于升到了王者一樣。

言歸正傳,下面我們生命一個結構體用來表示遊戲人物,這就要簡單多了,因為遊戲人物隻有一個,為了友善起見,我們還是把它聲明為普通資料類型和對應的指針類型,與遊戲人物相同性質的血量(這裡的相同性質指的是他們在遊戲中的數量都是一)也用這個結構體來定義

其中Point類型的數組表示人物(矩形)和生命值(矩形)的四個頂點。

當然之定義結構體是沒用的,要想在程式中應用這些結構體,必須用結構體變量類型生成實體,也就是結構體變量。

從名字我們可以1清楚知道他們分别代表台階,人物和血量,第四行是貼圖編号,現在大家可以忽略。現在我們寫一下基本的main函數,想必大家在課堂上都學習過這些代碼的内涵了吧,無非就是最基本的視窗大小,背景顔色,回調函數(call back function)的登入(或者叫注冊?),随着程式的深入,這個main函數會不斷完善。

現在我們要進行這個遊戲的重頭戲之一————生成台階了,玩過是男人就下100層的朋友可能知道,台階是從遊戲視窗底部開始生成,生成之後不斷上升,直到上升到視窗頂端消失,當然,生成台階時位置必須是随機的。

首先來看随機函數,這裡我們所要生成的東西其實就是台階的一個點而已(暫定為左下點,命名為初始點),因為其餘的點都可以用這個初始點外加台階的長和寬來得到(這個已經在前面聲明成了常量),關于随機函數這裡不再贅述,分為真随機和假随機函數,網絡上相關資源很多,這裡我們為了示範和說明的便利,采用假随機函數(假随機函數每次打開遊戲所生成的台階位置和上一次打開時一樣,是固定不變的,真随機則每次都會不同),當然,我們在生成初始點時必須考慮到今後要制作的遊戲邊框範圍以及台階的寬度,也就是說我們生成的初始點必須在這個範圍之内。

由這個函數我們可以知道随機函數産生了一個最大範圍為螢幕寬度減去台階寬度,最小範圍為從左開始200像素(也就是我們準備繪制邊框的範圍),這樣我們繪制的台階才不至于跑到邊框外面。

之後要構造台階生成/銷毀裝置,我們管他叫台階管理器。

上面這幅圖就是我們的台階管理器它的大小為20,具體運作方式是,每一個元素存儲一個台階資訊(位置坐标,台階類型)的指針,将它放置在渲染函數裡,并将所有指針指向NULL(初始化), 每次調用這個管理器函數時,程式都會從頭到尾檢查該指針數組,看是否有空元素(指向NULL的元素),如果有,利用malloc函數為其配置設定記憶體,生成一個台階,當台階上升到最頂端時,再用free()函數釋放其記憶體,并使其重新指向NULL,這樣在下一次循環中,該元素又可以被重新利用來存放新台階資訊。在這個管理器中,台階“自動生成”,“自動銷毀”,無限循環,直到遊戲結束,并且台階所占記憶體被限制在20個指針數組中,所有元素都會被再利用,達到資源最優的效果。

以上就是我們的台階管理器函數的代碼和其初始化代碼,細心的朋友已經發現,我在函數最一開始加了兩行這樣的代碼:

"density"顧名思義,是控制台階産生密度的一個變量,如果沒有該代碼,在每次調用該管理函數時,程式會一次性檢查出所有指針數組的空位,20個台階會完全鋪滿螢幕,像這樣:

OpenGL2D小遊戲——是男人就下100層

這顯然是不行的,是以,我們設定一個随機函數,産生0~9十個整數,當且僅當該數字等于2時,生成台階,這樣,台階生成就稀疏好多:

OpenGL2D小遊戲——是男人就下100層

下面考慮該把這個函數放在哪裡才能達到讓其不間斷的被調用,這裡,我們将它放置在渲染函數裡,因為渲染函數将會在時間函數中重複調用,是以,将它放在渲染函數中最合适不過了,下面我們就來寫渲染函數與時間函數:

其中還有好多我們現在還沒有涉及的函數,大家自動忽略就行,那些我以後會将,不要着急,現在你隻需要知道,我們的台階管理器已經被放置在了時間函數裡了,這就足夠了!

接下來我們就要根據台階管理器來對台階進行渲染了,這裡我們要用到Texture mapping的一些知識,很簡單,全都是一些固定的函數而已,所謂Texture mapping 并不像它的名字那樣高大上,它的原理有點類似于我們小時候玩的模型玩具,比如高達模型,在拼裝完機器人主體時 一些收藏玩家會在模型上貼上随包裝附帶的貼紙,使自己的模型看上去更加帥氣(我個人不怎麼喜歡貼貼紙)

OpenGL2D小遊戲——是男人就下100層
OpenGL2D小遊戲——是男人就下100層

就像上圖所顯示的那樣,每一張貼紙都對應模型上的一個部位,隻有貼對位置才會出效果,并且我們發現,每一個貼紙上都是有編号的 ,模型愛好者們通過這些編号在說明書上找到貼紙應該貼的位置,這與OpenGL中的Texture Mapping的原理如出一轍!所有用于Texture Mapping的BMP圖檔都是要通過一定的函數來進行編号的!

首先我們需要一個能讀取BMP圖檔的函數,這個在網上或者學校上課的講義中都應該講過的,這裡不多餘贅述:

如果大家在了解方面有問題的話,就直接放棄吧,對,沒錯,有時候我們并不需要糾結于次要的東西,我們隻需要知道,調用這個家夥時,需要給予它三個參數,分别是檔案(圖檔)名稱,圖檔長以及圖檔寬。

要注意,這裡的圖檔長和圖檔寬不是使用者自定義的,而是在函數讀取到BMP圖檔資訊後,程式會将正在讀取的BMP圖檔的長和寬的資訊賦在事先你定義好的空變量裡,好了,也許這麼說會有些繞口,給大家舉個例子就知道了。

如圖,該函數的傳回值是unsigned char指針類型,我們就聲明一個pImage來接收圖像,然後在ReadBmpFile裡第一個·參數是圖檔名,注意加上位址,然後在項目檔案夾裡添加上你的圖檔檔案夾,這裡,我的圖檔檔案夾是叫“”UI",圖檔名稱為"pic1.bmp",圖檔的長與高的資訊被存放在事先聲明好的整數變量w與h中。

之後,就是給我們在制作這個遊戲所用到的圖檔進行編号,我們就為它起一個名字叫“InitOpenGL()”吧這個函數僅需要在main函數中調用一次即可,我們先看函數,後為大家詳細解釋:

第一眼看上去這個函數确實有些複雜,但實際上,這些代碼隻是在重複同一項任務————對于将要在Texture mapping中運用到的圖檔進行處理。

其中涉及到的一些不認識的函數,我建議大家去到網站上查一下,畢竟自己查來的東西可能記憶更加深刻一些吧~(*__*),不過還是給大家講講吧^_^~~

glTexEnvf():定義貼圖環境。

glGenTexture():規定所用貼圖的數量以及所存在的數組名。

glBindTexture():将編号與特定圖檔綁定

glTexImage2D():函數原型:

GL_APICALL void GL_APIENTRY glTexImage2D(GLenum target, GLint level, GLenum internalformat,

GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void* pixels);

參數說明:

target 指定目标紋理,這個值必須是GL_TEXTURE_2D。

level 執行細節級别。0是最基本的圖像級别,n表示第N級貼圖細化級别。

internalformat 指定紋理中的顔色元件。可選的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE, GL_LUMINANCE_ALPHA 等幾種。

width 指定紋理圖像的寬度,必須是2的n次方。紋理圖檔至少要支援64個材質元素的寬度

height 指定紋理圖像的高度,必須是2的m次方。紋理圖檔至少要支援64個材質元素的高度

border 指定邊框的寬度。必須為0。

format 像素資料的顔色格式, 不需要和internalformatt取值必須相同。可選的值參考internalformat。

type 指定像素資料的資料類型。可以使用的值有GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4,GL_UNSIGNED_SHORT_5_5_5_1。

pixels 指定記憶體中指向圖像資料的指針(網上找的,說的已經很詳細啦~~,連結在這裡http://baike.baidu.com/link?url=7t1EAJCxmwi1qSI3f9lsWAn0QPUSCw378mfvgfSVHzXKc3TuaUPYSGzITqTbmJI7V1w0vF40OO4K3leOSDuSWK)

glBuild2DMipmaps():gluBuild2DMipmaps(GL_TEXTURE_2D,//此紋理是一個2D紋理

                                          3,                                   //顔色成分

                                          w,               //紋理的寬度

                                          h,               //紋理的高度

                                           GL_RGB,                                      //告訴OpenGL圖像資料由紅、綠、藍三色資料組成

                                           GL_UNSIGNED_BYTE,                     //組成圖像的資料是無符号位元組類型

                                          pImage);             //告訴OpenGL紋理資料的來源,此例中指向存放在pImage記錄中的資料)(這也是摘抄的,連結在這裡~http://www.cnblogs.com/qingsunny/archive/2013/03/25/2980727.html)

到此為止,我們對texture mapping所要用到的圖檔的最基本處理也就完成了,這麼繁雜的代碼其實隻做了兩件事——告訴程式,我要開始貼圖了哦~,還有我要用這些圖檔貼哦~

然後就可以對台階管理器中的台階進行渲染了。

對于台階的渲染的總體方法就是,在渲染之前先用if語句判斷它是屬于哪一個種類的台階,然後再找對應圖檔的編号進行貼圖。

既然說到了渲染,那我們就先把遊戲中需要制作的邊框,人物等的渲染代碼全都貼上來吧,都很簡單,全部是在固定位置畫上固定圖案的代碼:

其中Compute_Player()事先計算好遊戲人物的位置,将坐标存在player類型的結構體變量player中,之後再對他進行渲染。

這是對于邊界的渲染,都是“體力活兒”,打的時候會辛苦一點

别忘了把他們都要加進設定的渲染函數中去,我的渲染函數叫Render(),剛才貼過了,現在再貼一次:

怎麼樣,是不是和剛才比起來,不了解的地方要少了?

下面我們再處理另一項,鍵盤控制的回調函數:

要注意我們的人物隻能在邊框範圍内左右移動

此處需要分割線

現在我們就要開始這個遊戲最關鍵的部分———碰撞檢測了,當遊戲人物降落到台階上時,人物必須停下來,就像我們踩在地面上一樣,這就是我們要進行的所謂碰撞檢測和碰撞效果程式的編寫。

也許大家會問,我們為什麼要進行碰撞檢測程式的編寫,難道神通廣大的OpenGL難道就沒有一個函數可以幫助我們進行碰撞檢測嗎?當然是沒有的,這裡大家要明白一點,OpenGL是圖形接口通俗一點講,OpenGL所涉及到的函數基本都隻與畫圖有關,再通俗一點講它就是用來畫畫的,雖然這麼說有些不精确,但是,類似于重力,摩擦力,以及各種實體上的作用力與反作用力在OpenGL裡是沒有的,大家也能體會到,剛才我們僅僅是在正方形上貼了幾個貼畫兒而已,代碼的數量已經開始呈指數上升了,說明OpenGL隻是一個比較底層的API它不是像unity,unreal一樣支援各種可視化操作的實體引擎(事實上,實體引擎也基本都是用這種底層圖形API編寫的

首先我們要判定什麼時候碰撞,也就是當人物踩到台階上時,人物會停在上面,換句話說,當人物進入到台階的坐标範圍時,或說當人物坐标與台階坐标有某種意義上的重疊時,人物會改變原先的移動方式,會與台階一樣同向同速運動,這樣,人們的感官就會感覺到,人物停在了台階上

我們先來看人物與台階各自最開始的運動方式:

可以看到如果沒有碰撞(預先定義碰撞感覺布爾變量Collide_d)人物會以十的速度勻速下降,并且到達視窗最低端時遊戲結束。

先給大家看一幅圖:

OpenGL2D小遊戲——是男人就下100層

這幅圖告訴我們,當且僅當人物的1點在台階的3點的右邊并且當人物的0點在台階2點的左邊,并且0點的縱坐标與3點縱坐标相等時,碰撞發生

用程式來翻譯這句話就是:

也許大家注意到,在最後我并沒有判斷y坐标相等,而也是用了一個區間,這與時間函數有關,時間函數的調用頻率與OpenGL幀重新整理頻率不符,導緻一些碰撞會檢測不到,是以,這裡y坐标的檢測也會是一個範圍

這會是一個基本的碰撞,但是我們這個遊戲的樂趣在于,我們有五種不同的台階,人物跟這五種台階分别碰撞會分别産生五種不同效果,這樣,我們的碰撞代碼就變成了這樣:

看了這個代碼,也許大家會産生很多疑惑,不用着急,我會為大家一一解釋:

首先,大家可以看到許多像Collide_d,id,Collide_Right之類的布爾類型的變量,這些變量是用來控制台階碰撞後發生的變化的,在任務與台階發生碰撞後,人物與台階的狀态或者運動方式都會發生改變,盡管你完全可以将這些描述變化的代碼寫進你的碰撞函數裡去,完全沒有任何錯誤,但是,本着讓代碼簡潔清楚明了的原則,我們不想讓這個函數過于繁雜,是以,我們隻在碰撞函數裡安放碰撞開關,也就是簡單的碰撞觸發器(trigger),也就是大家看到的bool類型的Collide_d和Collide_left等變量,之後再在台階與人物變化函數Update_player()和Update_Step()中對人物與台階的狀态進行變化。下面是經過改變後的Update函數:

最後是血量控制,也就是當人物踩在帶有尖刺的台階上時,血量減少三分之一,再次碰到,繼續減少三分之一,直到血量為零之後遊戲結束。

要注意一點,當人物與尖刺台階碰撞時,一定要記錄碰撞的台階編号(index),當人物再次碰到其他,隻有當現在的index與剛才記錄的index不同的時,才繼續減少血量,如果不進行記錄的話,會導緻隻要人物接觸到尖刺台階,血槽就會立刻變空的情況。

以下是血量控制與血槽渲染的代碼:

到此為止,我們的2D遊戲的主要代碼已經全部講解完畢了,我們做的隻是一個利用OpenGL做的最簡單的遊戲了,大家可以以這個代碼為參照,加入背景音樂,遊戲開始選項,暫停選項,以及更加好看的背景等,這如介紹中說的那樣,這隻是抛磚引玉,希望能對大家的學習帶來幫助,如果需要完整代碼或是有問題的朋友,加QQ852170906,謝謝大家。

繼續閱讀