当前位置: 首页 > news >正文

游戏编程模式-享元模式(Flyweight)

享元模式(Flyweight)

迷雾散去,显露雄伟的古老森林;阳光洒落,尽显各式的繁枝绿叶。

这是我们游戏开发者梦想的超凡场景,这样的场景通常由一个模式支撑着,它的名字低调至极:享元模式。

森林

我可以用几句话描述一片森林,但是在游戏中实现却是另外一回事。当你看到一整片树林填满屏幕时,图像编程人员所知道的是他们必须每1/60秒就要将数百万个面(多边形)发送给GPU。

我们所说的成千上万的树,一颗树就要数千个面(多边形)去描述。即使有足够多的内存存储这片森林,为了渲染它,数据也必须从CPU传送到GPU,这会造成CPU到GPU的带宽总线紧张(受限)。

那么描述一棵树大概需要哪些元素呢?

  • 描述树干、树枝和绿叶形状的多边形网格
  • 树皮和树叶的纹理。
  • 在森林中的位置和朝向
  • 使得每棵树有所不同的可调参数:大小/色彩等

如果在代码中描述一棵树,那么大概会是如下所示:

class Tree
{
private:Mesh mesh_;Texture bark_;Texture leaves_;Vector position_;double height_;double thickness_;Color barkTint_;Color leafTint_;
};

从上代码可知,一个Tree对象含有很多数据,其中的网格和纹理数据量尤其大。在游戏更新的一帧时间内是不可能把一片森林的树的对象全部发送给GPU的。幸运的是享元模式可以解决这个问题。

关键就是即使森林中有成千的树,这些树是相似的。树使用了相同的网格和纹理。即不同实例化后的Tree对象中mesh_/bark_/leaves_是一样的。因此,我们可以把这些共享的元素剥离出来,形成下述的分离的两个类:

// 共享的类
class TreeModel
{
private:Mesh mesh_;Texture bark_;Texture leaves_;
};// 细分的类
class Tree
{
private:TreeModel* model_; // 指向共享的对象Vector position_;double height_;double thickness_;Color barkTint_;Color leafTint_;
};

地形

上面提到了森林中的树木,那么游戏中必然涉及到了树木所位于的地形。为了加深对这个模式的理解,我们继续讨论游戏中的地形问题。

游戏中的地形有各式各样:草地、沙地、丘陵、河流等。不同地形在游戏中可能有以下主要的属性:

  • 人物角色在该地的移速
  • 人物是否可以行走的标识
  • 用于渲染的纹理

那么我们自然可以想到如何优雅地表示地形:

class Terrain
{
public:Terrain(int movementCost,bool isAccess,Texture texture): movementCost_(movementCost),isAccess_(isAccess),texture_(texture){}int getMovementCost() const { return movementCost_; }bool isAccess() const { return isAccess_; }const Texture& getTexture() const { return texture_; }private:int movementCost_;bool isAccess_;Texture texture_;
};

自然地,那么整个游戏世界上的地形该如何表达呢?最直接简单的如下所示:

class World
{void setTerrain(int x, int y, Terrain dst) {tiles_[x][y] = dst;}
private:Terrain tiles_[WIDTH][HEIGHT];// Other stuff...
};

那么上述的地形如何使用享元模式呢?请看下述的例子:

World wd;
wd.setTerrain(0, 0, Terrain(1, true, GRASS_TEXTURE)); // 草地
wd.setTerrain(0, 1, Terrain(1, true, GRASS_TEXTURE)); // 草地wd.setTerrain(1, 0, Terrain(3, true, HILL_TEXTURE)); // 丘陵
wd.setTerrain(1, 1, Terrain(3, true, HILL_TEXTURE)); // 丘陵wd.setTerrain(2, 0, Terrain(1, false, RIVER_TEXTURE)); // 河流
wd.setTerrain(2, 1, Terrain(1, false, RIVER_TEXTURE)); // 河流

这个例子中,[0,0]和[0,1]位置上的都是使用了相同的草地地形,Terrain实例元素完全一致,跟外部的位置因素没有任何关系,因此这个Terrain元素可以分离出来的元素。World实现变为如下所示:

class World
{void setTerrain(int x, int y, Terrain* dst) {tiles_[x][y] = dst;}
private:Terrain* tiles_[WIDTH][HEIGHT];// Other stuff...
};

最终的地形赋值如下所示:

World wd;
Terrain grassTerrain = Terrain(1, true, GRASS_TEXTURE);
Terrain hillTerrain = Terrain(3, true, HILL_TEXTURE);
Terrain riverTerrain = Terrain(1, false, RIVER_TEXTURE);
wd.setTerrain(0, 0, &grassTerrain); // 草地
wd.setTerrain(0, 1, &grassTerrain); // 草地wd.setTerrain(1, 0, &hillTerrain); // 丘陵
wd.setTerrain(1, 1, &hillTerrain); // 丘陵wd.setTerrain(2, 0, &riverTerrain); // 河流
wd.setTerrain(2, 1, &riverTerrain); // 河流

享元模式(关键概念)

享元模式(Flyweight Pattern)是一种结构型设计模式,用于通过共享尽可能多的对象,以减少内存占用和提高性能。
它将对象的状态分为内部状态外部状态,从而实现对可共享部分的复用。

概念说明示例
Flyweight(享元对象)表示可以共享的对象,封装内部状态共享的网格、纹理、地形数据
Intrinsic State(内部状态)对象中可被多个实例共享的部分,不随环境变化。同上
Extrinsic State(外部状态)每次使用享元时由外部传入的部分,不可共享。树的位置、颜色、朝向、地形位置
Flyweight Factory(享元工厂)创建并管理享元对象的共享池,确保重复使用已有实例。MeshManager、MaterialCache
Client(客户端)使用享元对象的代码部分,负责维护外部状态。场景渲染器、Entity系统

享元模型在渲染中的作用(OpenGL实践)

上述的两个例子用了享元模式共享了很多数据,目前我们只看到了能够减少内存的作用,那么再来看下该模式是如何提升渲染过程的性能的?

我们以OpenGL为例,OpenGL天然支持享元模式。很多时候渲染的时候可以用共享的顶点数据,在屏幕上进行不同位置的渲染。如下所示


#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <cmath>
#include <cstdlib>// 顶点着色器
const char* vertexShaderSrc = R"(
#version 330 core
layout(location = 0) in vec2 aPos;     // 顶点坐标(共享)
layout(location = 1) in vec2 iOffset;  // 实例位置
layout(location = 2) in float iScale;  // 实例缩放
layout(location = 3) in vec3 iColor;   // 实例颜色out vec3 vColor;void main()
{vec2 pos = aPos * iScale + iOffset;gl_Position = vec4(pos, 0.0, 1.0);vColor = iColor;
}
)";// 片段着色器
const char* fragmentShaderSrc = R"(
#version 330 core
in vec3 vColor;
out vec4 FragColor;void main()
{FragColor = vec4(vColor, 1.0);
}
)";// Shader编译工具函数
GLuint createShader(GLenum type, const char* src)
{GLuint shader = glCreateShader(type);glShaderSource(shader, 1, &src, nullptr);glCompileShader(shader);GLint success;glGetShaderiv(shader, GL_COMPILE_STATUS, &success);if (!success){char log[512];glGetShaderInfoLog(shader, 512, nullptr, log);std::cerr << "Shader compile error:\n" << log << std::endl;}return shader;
}int main()
{// 初始化GLFWif (!glfwInit()) return -1;GLFWwindow* window = glfwCreateWindow(800, 600, "Two-leaf Grass (Flyweight)", nullptr, nullptr);if (!window) { glfwTerminate(); return -1; }glfwMakeContextCurrent(window);// 初始化GLEWglewExperimental = GL_TRUE;if (glewInit() != GLEW_OK){std::cerr << "Failed to init GLEW\n";return -1;}// -------------------------------// 共享几何(每棵草由两片叶子组成)// -------------------------------float vertices[] = {// 第一片叶子(往左)-0.02f, 0.0f,0.00f, 0.1f,0.02f, 0.0f,// 第二片叶子(往右)0.00f, 0.0f,0.02f, 0.1f,0.04f, 0.0f};GLuint vao, vbo;glGenVertexArrays(1, &vao);glGenBuffers(1, &vbo);glBindVertexArray(vao);glBindBuffer(GL_ARRAY_BUFFER, vbo);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// -------------------------------// 实例数据(位置 + 缩放 + 颜色)// -------------------------------const int NUM = 150;std::vector<float> instanceData;for (int i = 0; i < NUM; ++i){float x = ((rand() % 200) / 100.0f - 1.0f); // [-1, 1]float y = ((rand() % 100) / 100.0f - 1.0f); // [-1, 0]float scale = 0.4f * ((rand() % 100) / 100.0f + 0.5f);float r = 0.1f + (rand() % 30) / 255.0f;float g = 0.6f + (rand() % 80) / 255.0f;float b = 0.1f + (rand() % 30) / 255.0f;instanceData.insert(instanceData.end(), { x, y, scale, r, g, b });}GLuint instanceVBO;glGenBuffers(1, &instanceVBO);glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);glBufferData(GL_ARRAY_BUFFER, instanceData.size() * sizeof(float), instanceData.data(), GL_STATIC_DRAW);// 实例属性glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(1);glVertexAttribDivisor(1, 1);glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(2 * sizeof(float)));glEnableVertexAttribArray(2);glVertexAttribDivisor(2, 1);glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(3);glVertexAttribDivisor(3, 1);// 着色器程序GLuint vs = createShader(GL_VERTEX_SHADER, vertexShaderSrc);GLuint fs = createShader(GL_FRAGMENT_SHADER, fragmentShaderSrc);GLuint shader = glCreateProgram();glAttachShader(shader, vs);glAttachShader(shader, fs);glLinkProgram(shader);glUseProgram(shader);glClearColor(0.3f, 0.6f, 0.9f, 1.0f);// -------------------------------// 渲染循环// -------------------------------while (!glfwWindowShouldClose(window)){glClear(GL_COLOR_BUFFER_BIT);glUseProgram(shader);glBindVertexArray(vao);glDrawArraysInstanced(GL_TRIANGLES, 0, 6, NUM);glfwSwapBuffers(window);glfwPollEvents();}// 清理glDeleteVertexArrays(1, &vao);glDeleteBuffers(1, &vbo);glDeleteBuffers(1, &instanceVBO);glfwTerminate();return 0;
}

解析代码:

  • vao: 使用了一份共同的vertices,代表叶子几何形状(两个三角形)
  • instanceVBO: 表示每个实例的数据(位置 + 缩放 + 颜色)
  • glDrawArraysInstanced:执行对实例的绘制

在这里插入图片描述

http://www.dtcms.com/a/470221.html

相关文章:

  • 新郑做网站优化桂林网站优化公司
  • B站排名优化:知识、娱乐、生活类内容的差异化实操策略
  • 闵行网站制作设计公司昆明哪些做网站建设的公司
  • Spring Boot 3.x核心特性与性能优化实战
  • 域名解析后多久打开网站建个人网站
  • 基于MATLAB的PIV(粒子图像测速) 实现方案
  • 北京市网站建设企业怎么自己开发一个app软件
  • 基于springboot的技术交流和分享平台的设计与实现
  • Spring Boot 处理JSON的方法
  • 在Gin项目中使用API接口文档Swagger
  • asp.net 4.0网站开发高级视频教程订阅号怎么做免费的视频网站吗
  • 重庆响应式网站制作没有后台的网站怎么做排名
  • ENSP Pro Lab笔记:配置STP/RSTP/MSTP(1)
  • ajax 效果网站中国室内装饰设计网
  • 5-流程控制语句
  • Dify实战:调试技巧深度解析
  • Linux下Mysql初始化如,密码如何查找
  • 2025知识管理平台深度测评:从工具进化为智能决策引擎
  • 网站后台开发教程jsp网站缓存在哪
  • 网站页面怎么做的好看百度在西安的公司叫什么
  • Python 打印1-100的素数
  • 创建子进程时的一些细节
  • STM32 EC11旋转编码器扫描读取
  • 如何对抗GPS欺骗式干扰之二:多天线阵列测向的识别原则和应用场景
  • Linux 内核网络调优:单连接大带宽吞吐配置
  • STM32 外设驱动模块【含代码】:XY摇杆模块
  • 商会网站模板河南核酸检测vip
  • 外骨骼手套带来了一种仅用手就能与XR进行交互的更自然的方式
  • 学习随笔-Math
  • Android 权限模型(前台、后台、特殊权限)