用OpenGL(C++)绘制扫雷游戏的动态笑脸

本文介绍了使用OpenGL基础语法实现Windows XP经典扫雷游戏中笑脸图形的过程,包括笑脸的四种形态、点线多边形绘制函数封装以及关键细节处理。通过自定义函数展示如何绘制笑脸、眼睛、嘴巴和表情变化。
摘要由CSDN通过智能技术生成

tags: OpenGL Game

写在前面

最近看看OpenGL的东西, 学了一些基础的语法, 所以用来做点东西, 用基本的点线多边形等来绘制一个Windows XP中经典的游戏扫雷里面的标志性笑脸, 如下图:

minesweeper-1

思路

这个笑脸有四种形态,分别是初始状态的笑脸, 点击时候的o型嘴, 赢的时候的墨镜脸和输了时候的伤心脸, 下面通过简单的直线和圆来绘制, 都是简单图形, 本质上其实都是点的绘制, 这里我封装了一下函数, 调用会方便一些, 分别是通过vector<GLint>表示的点, 这样用initializer_list时候会方便很多.

其次就是一些小细节, 例如圆的绘制中的角度问题(这里就是嘴的绘制), 还有键鼠事件的交互, 我觉得熟悉了这个流程之后, 主要的思路就都放在点的坐标上了, 这里我建议可以通过Geogebra这款软件进行草图的绘制, 找到待绘制的点的坐标之后会方便很多.

代码(C++)

下面是代码, 这里分成几个部分分别说:

首先是导言区, 导入相应的模块和常量.

#include <GL/glut.h>
#include <vector>
//#include <iostream>
#include <cmath>

using namespace std;
const int w = 400, h = 400;

const GLfloat pi = 3.1415926536f;

void init() {
    glClearColor(1, 0, 1, 0);//黑色背景
    glMatrixMode(GL_PROJECTION);//正投影
    glLoadIdentity();
    gluOrtho2D(-w, w, -h, h);
}

然后是一些自定义的函数, 分别是画圆画直线和画多边形(这里是四边形)的, 用起来比直接读取点要方便一些.

void drawCircle(GLint x, GLint y, GLint r,
                vector<GLfloat> rgb = {0, 0, 0},
                GLint cir = 2, int mode = GL_POLYGON,
                GLfloat lw = 1,
                const int n = 10000) {
    glColor3f(rgb[0], rgb[1], rgb[2]);
    glLineWidth(lw);
    glBegin(mode);
    for (int i = 0; i < n; i++) {
        glVertex2i(x + r * cos(cir * pi / n * i),
                   y + r * sin(cir * pi / n * i));
    }
    glEnd();
}

void drawLine(vector<GLint> p1, vector<GLint> p2, GLfloat lw = 1,
              vector<GLfloat> rgb = {0, 0, 0},//default:black
              int mode = GL_LINE_STRIP) {
    glColor3f(rgb[0], rgb[1], rgb[2]);
    glLineWidth(lw);
    glBegin(mode);
    glVertex2i(p1[0], p1[1]);
    glVertex2i(p2[0], p2[1]);
    glEnd();
}

void drawPolygon(vector<GLint> p1, vector<GLint> p2,
                 vector<GLint> p3, vector<GLint> p4,
                 vector<GLfloat> rgb = {0, 0, 0},//default:black
                 int mode = GL_POLYGON,
                 GLfloat lw = 1) {
    glColor3f(rgb[0], rgb[1], rgb[2]);
    glLineWidth(lw);
    glBegin(mode);
    glVertex2i(p1[0], p1[1]);
    glVertex2i(p2[0], p2[1]);
    glVertex2i(p3[0], p3[1]);
    glVertex2i(p4[0], p4[1]);
    glEnd();
}

最后就是主要的绘制了,

void display() {// 绘制基本的笑脸
    drawCircle(0, 0, 50, {1, 1, 0});//face
    drawCircle(-20, 15, 6);//left eye
    drawCircle(20, 15, 6);//right eye
    drawCircle(0, -10, 20, {0, 0, 0}, -1, GL_LINE_STRIP, 3);//mouth
    glFlush();
}

void openMouth() {//点击时候的张嘴
    drawCircle(-20, 15, 8);//left eye
    drawCircle(20, 15, 8);//right eye
    drawCircle(0, -10, 20, {1, 1, 0}, -1, GL_LINE_STRIP, 3);//cover old mouth
    drawCircle(0, -18, 10, {0, 0, 0}, 2, GL_LINE_STRIP, 3);//mouth
    glFlush();
}

void coolFace() {//赢时候的墨镜表情
    drawPolygon({-40, 25}, {-10, 25}, {-10, 8}, {-40, 8});//left eye
    drawPolygon({40, 25}, {10, 25}, {10, 8}, {40, 8});//right eye
    drawLine({-10, 23}, {10, 23}, 3);//glasses middle
    drawLine({-45, 20}, {-40, 25}, 3);//glasses left
    drawLine({45, 20}, {40, 25}, 3);//glasses right
    glFlush();
}

void dieFace() {//输了时候的伤心脸
    drawCircle(0, 0, 50, {1, 1, 0});//face
    drawLine({-12, 7}, {-28, 23}, 3);
    drawLine({-12, 23}, {-28, 7}, 3);//left eye
    drawLine({12, 7}, {28, 23}, 3);
    drawLine({12, 23}, {28, 7}, 3);//right eye
    drawCircle(0, -10, 20, {0, 0, 0}, 1, GL_LINE_STRIP, 3);//mouth
    glFlush();
}

// 这里是键盘交互, 通过按下q或者esc键实现退出, 按d实现伤心脸, 按w实现墨镜脸
void Key(unsigned char key, int x, int y) {
    switch (key) {
        case 27:
        case 'q':
            exit(0);
        case 'd':
            dieFace();
            break;
        case 'w':
            coolFace();
            break;
    }
}

// 鼠标事件, 点击笑脸变形为O型嘴
void Mouse(int btn, int state, int x, int y) {
    // 如果在圆内, 点击生效
    if (x * x + y * y - w * x - w * y + (w / 2) * w < 50 * 50) {
        if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
//            cout << x << " " << y << endl;
            openMouth();
        } else {
            display();
        }
    }
}


int main(int argc, char **argv) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
    glutInitWindowPosition(200, 200);
    glutInitWindowSize(w, h);
    glutCreateWindow("MineSweeper Smiling Face");
    init();
    glutDisplayFunc(display);
    glutKeyboardFunc(Key);
    glutMouseFunc(Mouse);
    glutMainLoop();
    return 0;
}

最后实现结果:

aa

顺便附带一下cmake文件:(我的测试环境是Mac, 在Windows中需要配置一下路径)

cmake_minimum_required(VERSION 3.21)
project(minesweeper-smile)

set(CMAKE_CXX_STANDARD 17)
set(program_SOURCES main.cpp)
link_directories("/opt/homebrew/lib")
include_directories("/opt/homebrew/include")

find_library(Cocoa_Library Cocoa)
find_library(OpenGl_Library OpenGL)
find_library(GLUT_Library glut)
add_executable(${PROJECT_NAME} ${program_SOURCES})
target_link_libraries(${PROJECT_NAME}
        ${Cocoa_Library}
        ${OpenGl_Library}
        ${GLUT_Library}
        )

作者对游戏的说明: 首先,您应当以一种批判的眼光来看待本程序。这个游戏是我制作 的第一部RPG游戏,无任何经验可谈,完全按照自己对游戏的理解进 行设计的。当我参照了《圣剑英雄2》的源码之后,才体会到专业游 戏引擎的博大精深。 该程序的内核大约有2000余行,能够处理人物的行走、对话、战斗, 等等。由于该程序的结构并不适于这种规模的程序,故不推荐您详 细研究该程序。所附地图编辑器的源程序我已经添加了详细的注释, 其程序结构也比较合理,可以作为初学VC的例子。 该程序在VC的程序向导所生成的SDI框架的基础上修改而成。它没有 使用任何关于VC底层的东西。程序的绝大部分都是在CgameView类中 制作的,只有修改窗口特征的一段代码在CMainFrm类中。其他的类 统统没有用到。另外添加的一个类是CEnemy类。 整个游戏的故事情节分成8段,分别由Para1.h ~ Para8.h八个文件 实现。由于程序仅仅能够被动的处理各种各样的消息,所以情节的 实现也只能根据系统的一些参数来判断当前应当做什么。在程序中 使用了冗长的if……else if……结构来实现这种判断。 当然,在我的记录本上,详细的记录了每个事件的判断条件。这种 笨拙的设计当然是不可取的。成都金所作《圣剑英雄II》采用了 剧本解读的方式,这才是正统的做法。但这也需要更多的编程经验 和熟练的code功夫。 下面列举的是程序编制过程中总结出来的经验和教训。 第一,对话方式应该采用《圣剑英雄II》的剧本方式。 现在的方式把一个段落中所有的对话都混在一个文件中,然后给每 句话一个号码相对应。这样做虽然降低了引擎的难度,却导致剧情的 编写极其繁琐。 第二,运动和显示应当完全分开。 现在的程序中,运动和显示是完全同步的。即:在定时器中调用所有 敌人的运动函数,然后将主角的动画向前推一帧,接着绘制地图,调 用所有敌人的显示函数、重绘主角。这样的好处是不会掉帧,但带来 的问题是,如果要提高敌人的运动速度,那么帧数也跟着上去了。所 以当DEMO版反馈说速度太慢的时候,我修改起来非常困难。而这个问 题到最后也仅仅是将4步一格该成了2步一格。 第三,VC中数组存在上限。如果用“int aaa[1000000000]”定义一个 数组,编译器肯定不会给分配那么大的内存空间。而在这个程序中, 地图矩阵、NPC矩阵都超过了VC中数组的上限。但这一知道的太晚了。 在1.0版本中已经发现地图最右端缺少了几行,但不知道是什么原因 造成的。(地图编辑器中未出现此问题,因为地图编辑器是用“序列 化”的方式存盘读盘的。)解决这个问题的方法是用“new”来分配 内存空间。 第四,由于不知道应该如何使用“new”和“delete”,几乎所有的DC 都使用了全局变量。这是完全没有必要的。程序运行期大约会耗用20 多M的内存空间,相当于一个大型游戏所使用的内存空间了。 另外,在游戏的剧情、美工方面也有许多问题,总之一个词“业余”。 我就不总结了。下一部作品,我将争取在程序上有一个质的飞跃。
绘制斯坦福兔子模型,首先需要加载模型文件。可以使用Assimp库来加载模型文件。接下来,需要使用OpenGL来渲染模型。 下面是一个简单的OpenGL C代码片段,可以绘制斯坦福兔子模型: ```c #include <GL/gl.h> #include <GL/glut.h> #include <stdio.h> #include <stdlib.h> #include <math.h> #include <string.h> #include <assimp/cimport.h> #include <assimp/scene.h> #include <assimp/postprocess.h> void display() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 绘制斯坦福兔子模型 glPushMatrix(); glColor3f(1.0, 1.0, 1.0); glScalef(0.02, 0.02, 0.02); glTranslatef(-20.0, -10.0, 0.0); glRotatef(-90.0, 1.0, 0.0, 0.0); aiVector3D zero(0.0f, 0.0f, 0.0f); const aiScene* scene = aiImportFile("bunny.obj", aiProcessPreset_TargetRealtime_MaxQuality); aiMesh* mesh = scene->mMeshes[0]; glBegin(GL_TRIANGLES); for (GLuint i = 0; i < mesh->mNumFaces; i++) { const aiFace& face = mesh->mFaces[i]; for (GLuint j = 0; j < 3; j++) { aiVector3D pos = mesh->mVertices[face.mIndices[j]]; glVertex3f(pos.x, pos.y, pos.z); } } glEnd(); aiReleaseImport(scene); glPopMatrix(); glutSwapBuffers(); } void init() { glClearColor(0.0, 0.0, 0.0, 0.0); glEnable(GL_DEPTH_TEST); } int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH); glutInitWindowSize(640, 480); glutCreateWindow("Stanford Bunny"); init(); glutDisplayFunc(display); glutMainLoop(); return 0; } ``` 其中,bunny.obj是斯坦福兔子模型的模型文件,需要放在代码所在目录或指定路径下。在display函数中,通过Assimp库加载模型文件,并使用OpenGL绘制模型。需要注意的是,需要设置适当的缩放、平移和旋转来调整模型的位置和大小。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zorchp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值