利用OpenGL設計貪喫蛇遊戲
文章目錄
任務介紹
- 貪喫蛇遊戲:玩家控制貪喫蛇在遊戲區域裏馳騁,避免碰到自己或障礙物,儘可能地喫更多的食物以生長!
遊戲玩法
- WASD控制蛇的移動
- 遊戲開始,會在地圖空閒位置刷新一個食物,蛇觸碰到食物後食物消失,食物會重新刷新,分數增加,蛇會增加一個單位的長度
- 當蛇觸碰到自己,則遊戲失敗
- 當蛇接觸到地圖邊界,蛇會在地圖另一端重新進入地圖
開發環境
- OpenGL3
- GLFW
- IMGUI
遊戲實現
貪喫蛇遊戲的框架搭建
主程序
對GLFW進行初始化,創建遊戲對象,創建GUI,對用戶的輸入進行傳遞。主要是在一個 while 循環中,進行對遊戲對象的更新與渲染。
//渲染
while (!glfwWindowShouldClose(window))
{
// 開啓深度測試
glEnable(GL_DEPTH_TEST);
// 清空深度緩存
glClear(GL_DEPTH_BUFFER_BIT);
//處理用戶輸入
processInput(window);
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// 計算每一次渲染相差的時間
GLfloat currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
std::string score = "Score : " + std::to_string(Snake.score);
const char *scorechar = score.c_str();
// 創建gui
ImGui::Begin("Gluttonous Snake");
// 顯示分數或者遊戲結束
if (Snake.State == GAME_WIN)
{
ImGui::Text("Game Over!!!!!!");
ImGui::Text(scorechar);
}
else
{
ImGui::Text(scorechar);
}
ImGui::End();
// Rendering
ImGui::Render();
// 遊戲處理用戶輸入
Snake.ProcessInput(deltaTime);
// 更新遊戲的狀態
Snake.Update(deltaTime);
//設置清空屏幕所用的顏色
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
//清除顏色緩衝
glClear(GL_COLOR_BUFFER_BIT);
// 渲染遊戲
Snake.Render((float)glfwGetTime());
//渲染gui
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
//交換顏色緩衝
glfwSwapBuffers(window);
//檢查IO事件
glfwPollEvents();
}
遊戲類
維護遊戲狀態,提供更新狀態的接口供主程序調用,進行碰撞檢測等
// 遊戲的狀態
enum GameState {
GAME_ACTIVE,
GAME_MENU,
GAME_WIN
};
class Game
{
public:
// 遊戲狀態
GameState State;
GLuint Width, Height;
// 遊戲分數
int score;
Game(GLuint width, GLuint height);
~Game();
// 初始化遊戲加載shader等
void Init();
void ProcessInput(GLfloat dt);
void Update(GLfloat dt);
void Render(float rotateRad);
void ProcessMouseMovement(float xoffset, float yoffset, bool constrainPitch);
// 鍵盤狀態
bool pressW;
bool pressS;
bool pressA;
bool pressD;
void ResetPress();
// 移動蛇
void MoveTheSnack();
// 碰撞檢測
void DoCollisions();
};
遊戲對象類
維護遊戲對象的狀態,在本次遊戲中就是蛇頭與蛇身、以及生成的食物
class GameObject
{
public:
// 狀態
glm::vec3 Position, Size;
GLfloat Rotation;
GameObject();
// 設置位置
void setPosition(glm::vec3 pos);
private:
GLuint gameobjectVAO;
};
工具類
管理着色器、紋理的加載,本次遊戲中沒有用到紋理於是只有着色器的加載
class ResourceManager
{
public:
// Resource storage
static std::map<std::string, Shader*> Shaders;
// Loads (and generates) a shader program from file loading vertex, fragment (and geometry) shader's source code. If gShaderFile is not nullptr, it also loads a geometry shader
static Shader * LoadShader(const GLchar *vShaderFile, const GLchar *fShaderFile, const GLchar *gShaderFile, std::string name);
// Retrieves a stored sader
static Shader * GetShader(std::string name);
// Properly de-allocates all loaded resources
static void Clear();
private:
// Private constructor, that is we do not want any actual resource manager objects. Its members and functions should be publicly available (static).
ResourceManager() { }
// Loads and generates a shader from file
static Shader * loadShaderFromFile(const GLchar *vShaderFile, const GLchar *fShaderFile, const GLchar *gShaderFile = nullptr);
};
着色器類
用於對單個的着色器進行初始化,並修改裏面的值
class Shader
{
public:
// State
GLuint ID;
// Constructor
Shader() { }
// Sets the current shader as active
Shader &Use();
// Compiles the shader from given source code
void Compile(const GLchar *vertexSource, const GLchar *fragmentSource, const GLchar *geometrySource = nullptr); // Note: geometry source code is optional
// Utility functions
void SetFloat(const GLchar *name, GLfloat value, GLboolean useShader = false);
void SetInteger(const GLchar *name, GLint value, GLboolean useShader = false);
void SetVector2f(const GLchar *name, GLfloat x, GLfloat y, GLboolean useShader = false);
void SetVector2f(const GLchar *name, const glm::vec2 &value, GLboolean useShader = false);
void SetVector3f(const GLchar *name, GLfloat x, GLfloat y, GLfloat z, GLboolean useShader = false);
void SetVector3f(const GLchar *name, const glm::vec3 &value, GLboolean useShader = false);
void SetVector4f(const GLchar *name, GLfloat x, GLfloat y, GLfloat z, GLfloat w, GLboolean useShader = false);
void SetVector4f(const GLchar *name, const glm::vec4 &value, GLboolean useShader = false);
void SetMatrix4(const GLchar *name, const glm::mat4 &matrix, GLboolean useShader = false);
private:
// Checks if compilation or linking failed and if so, print the error logs
void checkCompileErrors(GLuint object, std::string type);
};
攝像機類
在一個遊戲中會擁有一個攝像機,拍攝整個場景,可以對視角進行移動
class Camera
{
public:
// 鍵盤移動
void moveForward(float deltaTime);
void moveBack(float deltaTime);
void moveRight(float deltaTime);
void moveLeft(float deltaTime);
// 鼠標移動
void ProcessMouseMovement(float xoffset, float yoffset, bool constrainPitch);
// 鼠標滾動
void ProcessMouseScroll(float yoffset);
// 獲取view矩陣
glm::mat4 GetViewMatrix();
Camera(glm::vec3 position, glm::vec3 up, float yaw, float pitch);
Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch);
// 獲取視角值
float getZoom();
glm::vec3 Position;
private:
// 攝像機的屬性
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
// 歐拉角
float Yaw; // 偏航角
float Pitch; // 俯仰角
float MovementSpeed; // 相機移動速度
float MouseSensitivity; // 鼠標靈敏度
float Zoom; // 縮放視野
void updateCameraVectors();
};
精靈渲染類
用於對場景中物體進行頂點的初始化以及使用着色器去渲染
class SpriteRenderer
{
public:
SpriteRenderer(Shader *shader, Camera* camera);
// 渲染場景中的平面
void DrawPlaneSprite();
// 渲染場景中的牆
void DrawWallSprite();
// 渲染食物
void DrawFoodSprite(glm::vec3 position, glm::vec3 size, GLfloat rotate);
// 渲染蛇頭
void DrawHeadSprite(glm::vec3 position, glm::vec3 size = glm::vec3(1.0f, 1.0f, 1.0f), GLfloat rotate = 0.0f);
// 渲染蛇身
void DrawBodySprite(glm::vec3 position, glm::vec3 size = glm::vec3(1.0f, 1.0f, 1.0f), GLfloat rotate = 0.0f);
private:
Shader* shader;
Camera* camera;
// 需要渲染的VAO
GLuint planeVAO;
GLuint wallVAO;
GLuint headVAO;
GLuint bodyVAO;
GLuint foodVAO;
// 初始化渲染對象的VAO,VBO
void initPlaneRenderData();
void initRenderWall();
void initRenderHead();
void initRenderBody();
void initRenderFood();
};
場景、蛇、食物的渲染
場景
場景需要一個平面與四周的圍牆,圍牆使用一個長方體表示,然後將長方體進行旋轉位置的平移就可以得到四面牆。這部分直接在精靈渲染類中初始化以及渲染。初始化的時候定義頂點的位置和顏色,然後綁定相應的
VBO
以及VAO
在渲染平面背景的時候設置透視矩陣與觀察矩陣
void SpriteRenderer::DrawPlaneSprite()
{
this->shader->Use();
glm::mat4 view = camera->GetViewMatrix();
this->shader->SetMatrix4("view", view);
// 設置透視矩陣
glm::mat4 projection = glm::perspective(glm::radians(camera->getZoom()), (float)1200 / (float)900, 0.1f, 100.0f);
this->shader->SetMatrix4("projection", projection);
// 渲染平面
glm::mat4 model = glm::mat4(1.0f);
this->shader->SetMatrix4("model", model);
glBindVertexArray(planeVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
渲染圍牆時候定義不同的世界座標位置以及旋轉
void SpriteRenderer::DrawWallSprite()
{
// 每個正方體的世界座標
glm::vec3 cubePositions[] = {
glm::vec3(0.0f, 16.0f, 0.0f),
glm::vec3(0.0f, -16.0f, 0.0f),
glm::vec3(16.0f, 0.0f, 0.0f),
glm::vec3(-16.0f, 0.0f, 0.0f)
};
this->shader->Use();
// 設置不同的物體原點
for (int i = 0; i < 4; i++)
{
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, cubePositions[i]);
if (i == 2 || i == 3)
{
model = glm::rotate(model, glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
}
this->shader->SetMatrix4("model", model);
glBindVertexArray(wallVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
}
蛇、食物
蛇分爲蛇頭與蛇身,蛇頭與蛇身的顏色不同但是都是正方體,所以都是
GameObject
,在遊戲類中進行初始化,然後在通過精靈渲染類進行渲染。在遊戲類中使用一個vector
進行存儲。食物其實也是一個正方體,它的顏色與蛇區分開
// 保存蛇身和蛇頭
std::vector<GameObject *> allGameObject;
// 食物
GameObject *food;
void Game::Init()
{
// ...............
// 初始化頭和身體
GameObject * head = new GameObject();
allGameObject.push_back(head);
// 每個身體正方體的世界座標
glm::vec3 bodyPositions[] = {
glm::vec3(0.0f, -2.0f, 0.0f),
};
for (unsigned int i = 0; i < 1; i++)
{
GameObject * body = new GameObject();
body->setPosition(bodyPositions[i]);
allGameObject.push_back(body);
}
// 初始化食物位置
food = new GameObject();
food->Size = glm::vec3(0.7f, 0.7f, 0.7f);
int x, y;
while (true)
{
x = rand() % (14 + 14 + 1) - 14;
y = rand() % (14 + 14 + 1) - 14;
if (abs(x) % 2 == 0 && abs(y) % 2 == 0)
{
break;
}
}
food->setPosition(glm::vec3(x, y, 0.0f));
// ...............
}
void SpriteRenderer::DrawHeadSprite(glm::vec3 position, glm::vec3 size, GLfloat rotate)
{
this->shader->Use();
// 位置 縮放 旋轉
// 創建變換矩陣
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, position);
model = glm::rotate(model, rotate, glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::scale(model, size);
// 重新設置值
this->shader->SetMatrix4("model", model);
// 渲染正方體
glBindVertexArray(headVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
void SpriteRenderer::DrawFoodSprite(glm::vec3 position, glm::vec3 size, GLfloat rotate)
{
this->shader->Use();
// 位置 縮放 旋轉
// 創建變換矩陣
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, position);
model = glm::rotate(model, rotate, glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::scale(model, size);
// 重新設置值
this->shader->SetMatrix4("model", model);
// 渲染正方體
glBindVertexArray(foodVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
蛇、食物的控制邏輯
蛇的移動
蛇的移動使用了一個定時器在0.5s的時候進行蛇身的移動,進行對x軸或y軸增量的判定。從蛇尾開始,蛇尾的位置設置爲前一個蛇身的位置以此類推,蛇頭進行移動的邏輯要對方向進行判定,並且在超出範圍後要從另一邊進入,設置爲另一個邊界的座標
// 在主程序中
HWND m_hWnd = NULL;
SetTimer(m_hWnd, 1, 500, TimerProc); // 設置0.5s移動一次蛇
void CALLBACK TimerProc(HWND hWnd, UINT nMsg, UINT nTimerid, DWORD dwTime)
{
Snake.MoveTheSnack(); // 蛇移動
}
void Game::MoveTheSnack()
{
if (this->State == GAME_ACTIVE)
{
for (int i = allGameObject.size() - 1; i >= 0; i--)
{
// 對於蛇頭進行移動
if (i == 0)
{
glm::vec3 tmp = allGameObject[i]->Position;
if (playerDirection == UP)
{
tmp.y += 2.0f;
if (tmp.y > 14.0f)
{
tmp.y = -14.0f;
}
}
else if (playerDirection == DOWN)
{
tmp.y -= 2.0f;
if (tmp.y < -14.0f)
{
tmp.y = 14.0f;
}
}
else if (playerDirection == LEFT)
{
tmp.x -= 2.0f;
if (tmp.x < -14.0f)
{
tmp.x = 14.0f;
}
}
else if (playerDirection == RIGHT)
{
tmp.x += 2.0f;
if (tmp.x > 14.0f)
{
tmp.x = -14.0f;
}
}
allGameObject[i]->setPosition(tmp);
}
else
{
// 對蛇身進行移動
glm::vec3 tmp = allGameObject[i - 1]->Position;
allGameObject[i]->setPosition(tmp);
}
}
}
}
void Game::ProcessInput(GLfloat dt)
{
if (this->State == GAME_ACTIVE)
{
// 得到鍵盤的輸入
if (this->pressA)
{
if (playerDirection == RIGHT || playerDirection == LEFT)
{
return;
}
else
{
playerDirection = LEFT;
}
}
if (this->pressD)
{
if (playerDirection == RIGHT || playerDirection == LEFT)
{
return;
}
else
{
playerDirection = RIGHT;
}
}
if (this->pressS)
{
if (playerDirection == DOWN || playerDirection == UP)
{
return;
}
else
{
playerDirection = DOWN;
}
}
if (this->pressW)
{
if (playerDirection == DOWN || playerDirection == UP)
{
return;
}
else
{
playerDirection = UP;
}
}
}
}
食物的隨機擺放和旋轉
食物的位置隨機放置但是不能超出邊界,在每次喫到之後進行對位置的重置
int x;
int y;
while (true)
{
x = rand() % (14 + 14 + 1) - 14;
y = rand() % (14 + 14 + 1) - 14;
if (abs(x) % 2 == 0 && abs(y) % 2 == 0)
{
break;
}
}
food->setPosition(glm::vec3(x, y, 0.0f));
食物在每幀的時候會隨着時間不停旋轉
// 在主程序中將旋轉的角度傳入
Snake.Render((float)glfwGetTime());
void Game::Render(float rotateRad)
{
if (this->State == GAME_ACTIVE)
{
// 對食物角度的設置
food->Rotation = rotateRad;
// 渲染背景的平面牆
Renderer->DrawPlaneSprite();
Renderer->DrawWallSprite();
// 渲染食物
Renderer->DrawFoodSprite(food->Position,food->Size,food->Rotation);
// 渲染蛇身和蛇頭
for (unsigned int i = 0; i < allGameObject.size(); i++)
{
if (i == 0)
{
Renderer->DrawHeadSprite(allGameObject[i]->Position, allGameObject[i]->Size, allGameObject[i]->Rotation);
}
else
{
Renderer->DrawBodySprite(allGameObject[i]->Position, allGameObject[i]->Size, allGameObject[i]->Rotation);
}
}
}
}
碰撞檢測與響應
當蛇頭的位置和食物位置相同的時候,則表示喫到了食物,這時候食物需要重置位置,蛇身需要創建一個遊戲對象,然後加入
vector
中,當蛇頭的位置與任何一個蛇身的位置相同時候,則表示蛇頭與身體相撞,遊戲需要結束。
GLboolean CheckCollision(GameObject *one, GameObject *two) // AABB - AABB collision
{
// x軸方向碰撞
bool collisionX = (one->Position.x == two->Position.x);
// y軸方向碰撞
bool collisionY = (one->Position.y == two->Position.y);
// 只有兩個軸向都有碰撞時才碰撞
return collisionX && collisionY;
}
void Game::DoCollisions()
{
// 檢測蛇與食物的碰撞
for (int i = 0; i < allGameObject.size(); i++)
{
if (CheckCollision(allGameObject[i], food))
{
int x;
int y;
while (true)
{
x = rand() % (14 + 14 + 1) - 14;
y = rand() % (14 + 14 + 1) - 14;
if (abs(x) % 2 == 0 && abs(y) % 2 == 0)
{
break;
}
}
food->setPosition(glm::vec3(x, y, 0.0f));
GameObject * body = new GameObject();
body->setPosition(allGameObject[allGameObject.size() - 1]->Position);
allGameObject.push_back(body);
score++;
break;
}
}
// 檢測蛇頭與蛇身的碰撞
for (int i = 1; i < allGameObject.size(); i++)
{
if (CheckCollision(allGameObject[i], allGameObject[0]))
{
this->State = GAME_WIN;
break;
}
}
}
實現效果
總結
本次實驗其實對整個遊戲框架的構成有了更深入的瞭解,知道各個類之間應該怎樣配合,但是還是因爲經驗不足有一些耦合性,對材質掌握不熟練所以只是使用了單一的幾何體去表示,在蛇移動的刷新沒有很連貫,使用的是網格的方法,如果使用每幀刷新的話因爲每一個蛇身體之間都有一個偏移量要去處理這個偏移量還是需要一些技巧。