游戏编程之常用设计模式

游戏编程之常用设计模式

作者:老九—技术大黍

社交:知乎

公众号:老九学堂(新人有惊喜)

特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者授权

前言

一说到设计模式,可能大家会想到Java这个编程语言,其实,玩设计模式C++才是老祖宗啊。不掌握设计模式,那是不可能使用C++写出非常复杂的大型应用来的。

下面我们来介绍游戏编程中常用的设计模式。

Singleton(单例模式)

单例模式是一个全局对象,该对象在整个应用中只有一个实例。文本编程器、遥杆甚至玩家在游戏中都是单一的。这时我们需要它总是可见的,并且我们在内存中只保存一个这样的对象。

通常有两种方式来实现,第一种是把对象作为参数传送给呼叫它的方法,以便该方法可以访问它。该方法不是很有效,因为每次被呼叫时,需要反这个额外的参数被压栈到内存,这会导致编码和阅读都比较困难;第二种是在源文件中定义这些对象,在外部引用这些对象。但是,不管哪种实现方式都不是优雅的实现方式;第二种方式虽然有点技术含量,因为这种做法让编程接收这些标识,然后分另绑定真实对象到各种源文件中去。

但是它的缺陷是,随着代码的增长,源文件会有大量属性的定义,因此会降低编程的阅读性。另外从功能观点读,它是非常危险的,因为任何熟悉OOP编程的人员都知道,组件代码需要最大化的内聚性(cohesion)和最小化的绑定其它组件。

内聚意味着一个应该封装所有的功能,而不需要管特定的问题或者数据结构—这就意味一个类应该有很少或者没有依赖其它的类—这样才变成独立的和自省的可重新组件。(Cohesion means a class should encapsulate all functionality regarding a specific problem or data structure. Binding implies that a class should have little or no dependencies on other classes, so it becomes an independent and self-sufficient reusable component.)

但是这种情况几乎是不可能的,大多数需要使用其它的类,因为这种额外的绑定,这会导致我们的图类逐渐变成了一个蜘蛛网,因此,这样也会导致代码不能多其它的代码区分开。

因此,单例模式不同以上的解决方案。它开始需要声明一个类,然后让这个类只有一个公有方法可以从外部访问—让外部通过个方法来请求单例模式对象的实例。所有的实例实际上都指向相同的结构,因此第一请求呼叫必须创建一个单例模式对象,然后剩下的请求时,只需要返回这个对象的指针即可。因此,单例模式最主要的特点是它的构造方法不是公有的,它可以的保护类型、私人类型和默认类型的构造方法,这样防止外部类直接实例化这个单例对象。

类图

image-20210423101107821.png

Singleton.h头文件

#include <iostream>
#include <memory>
#include <new>
#include <ostream>
using namespace std;

/**
	功能:书写一个单例模式类,用来演示单例模式的C++语法
	作者:技术大黍
  */

class Egg{
private:
	static Egg egg; //声明一个Egg的实例
	int i; //声明它的成员变量
	Egg(int ii): i(ii){}  //重载构造方法给成员变量赋初始值
	Egg(const Egg&); //阻止拷贝构造方法

public:
	static Egg* instance(){
		return &egg;
	}
	
	int val() const{
		return i;
	}
};

//呼叫私有的构造方法
Egg Egg::egg(47);


/**
	功能:使用操作符重载的方式来实现单例模式的书写
	作者:技术大黍
  */
class Singleton{
private:
	Singleton(){}
	Singleton(int i){
		a = i;
	}
	Singleton(const Singleton&); //防止拷贝构造方法
	//定义两个成员变量
	static Singleton* instance;
	static size_t count;
	int a;
public:
	//重载操作符new方法
	void* operator new(size_t size)throw (bad_alloc){
		cout << "Singleton::new\n";
		if(instance == 0){
			instance = ::new Singleton;
		}
		++count;
		return instance;
	}
	
	void operator delete(void* p){
		cout << "Singleton:: delete\n";
		if(--count == 0){
			instance = 0;
		}
	}
	
	static Singleton* make(){
		return new Singleton();
	}
	
	static Singleton* make(int i){
		cout << "a的值是:" << i <<endl;
		return new Singleton(i);
	}
	
	int getA(){
		return a;
	}
};

//声明变量
Singleton* Singleton::instance;
size_t Singleton::count;

/**
	功能:按照Thinking Pattern In C++一书来实现单例模型
	作者:技术大黍
	*/
class SingletonPattern{
	static SingletonPattern s;
	int i;
	SingletonPattern(int x): i(x){}
	SingletonPattern& operator = (SingletonPattern&); //不允许拷贝
	SingletonPattern(const SingletonPattern&);//不允许拷贝
	
public:
	static SingletonPattern& instance(){
		return s;
	}
	
	int getValue(){
		return i;
	}
	
	void setValue(int x){
		i = x;
	}
};

SingletonPattern SingletonPattern::s(47);

Singleton.cpp文件

#include "Singleton.h"

int main(){
	cout << Egg::instance()->val() <<endl;
	
	auto_ptr<Singleton> one(Singleton::make());
	auto_ptr<Singleton> two(Singleton::make());
	cout << Singleton::make(250)->getA() << endl;
	
	SingletonPattern& s = SingletonPattern::instance();
	cout << s.getValue() << endl;
	SingletonPattern& s2 = SingletonPattern::instance();
	s2.setValue(9);
	cout << s.getValue() << endl;

	return 0;
}

运行结果

image-20210423101324978.png

Strategy(策略模式)

有时候我们需要根据情况来动态创建对象,比如战士,它的功能会根据他它的AI来及时更新。比如战士的AI有四种类型:战斗、逃跑、防御以及进入攻击范围。

Void recalculateAI(){
    switch(state){
        case FIGHT: recalcFight();break;
        case ESCAPE: recalEscape();break;
        case IDEL: recalIdle();break;
        case PATROL: recalPatrol();break;
    }
}

使用这种解决方案不是非常优雅,如果有非常很的策略需要实现时的时候。另外一个解决方案是使用子类,这样可以让子类实现一种算法。这种方式简单,但是实现比较复杂,解决它的缺陷是使用策略模式。它的目标是把一个类根据算法定义定义几个类,这些算法可以运行交换。

结果就是,我们的战士只有一个全局的算法(recalculateAI()方法被呼叫),从而动态的、优雅的封装了各种实现的算法。实现该模式需要两个类。第一个是策略类,它提供策略算法,这是一纯的抽象类,它会指定物定的策略让不同的子类去实现;第二个是上下文类,它定义该策略在哪里被使用,以及使用时候执行选中的策略和封装策略。

图类

image-20210423101858007.png

Strategy.h头文件

#include <iostream>
using namespace std;

/**
	功能:书写几个类,用来说明策略模式的使用
	作者:技术大黍
	*/
class Strategy{
public:
	virtual void greet() = 0;
};

class SayHi: public Strategy{
public:
	void greet(){
		cout << "Hi,你好吗?" << endl;
	}
};

class Ignore: public Strategy{
public:
	void greet(){
		cout << "(假装没有看到你)" << endl;
	}
};

class Admission: public Strategy{
public:
	void greet(){
		cout << "I'm sorry, 我忘记你的名称。" << endl;
	}
};


class Soldier{
	Strategy& strategy;
public:
	//在构造方法中判断策略
	Soldier(Strategy& s):strategy(s){}
	
	void greet(){
		strategy.greet();
	}
};

Strategy.cpp文件

#include "Strategy.h"

int main(){
	//声明三种策略
	SayHi sayhi;
	Ignore ignore;
	Admission admission;
	
	//扔给策略使用者,然后策略使用会多态使用greet()方法
	Soldier s1(sayhi),s2(ignore),s3(admission);
	s1.greet();
	s2.greet();
	s3.greet();
	
	return 0;
}

运行效果

image-20210423101954336.png

Factory(工厂模式)

现代应用都需要创建对象,然后删除这个对象。不这个对象是Word中文本块还是游戏中的敌人,就我们程序而言要确保需要时创建对象,在它们的生成周期结束时销毁这些对象。大多数程序员随着程序代码的不断增长,他们也写了相同的代码来探操作这些对象,这样导致创建/销毁的代码都处都是,这样结果会导致因为协议不一致而产生问题。工厂模式就是保证集中创建和销毁对象,从而提供了一种统一的方式来操作需要对象。工厂模式有两个分类:

  • 抽象工厂
  • 普通工厂

抽象工厂可适用于抽象类的地方,这样可以继承特定的产品功能实现。这非常有用,因为我们只有一个可以呼叫的抽象方法,通过创建各种延伸产品,我们的可类可以适应各种情况。而普通工厂模式,需要每种类型都有一个方法对应,因为它们没有继承概念。抽象工厂模式特别适合创建核心对象和游戏引擎—比如一个对象总控文本地创建、陷井的创建等。

图类

image-20210423102142606.png

AbstractFactory.h头文件

#include <iostream>
#include <stdexcept>
#include <cstddef>
#include <string>
#include <vector>
#include "purge.h"
using namespace std;

/**
	功能:书写几个类,用来演示抽象工厂的使用
	作者:技术大黍
	*/
class Product{
	
protected:
	Product(){
		cout << "父类Product()构造方法" << endl;
	}
};

class Texture: public Product{
public:
	Texture(){
		cout << "Texture类的构造方法" << endl;
	}
};

class Mesh: public Product{
public:
	Mesh(){
		cout << "Mesh类的构造方法" << endl;
	}
};

class Item: public Product{
public:
	Item(){
		cout << "Item类的构造方法" << endl;
	}
};

typedef int ProductId;

#define TEXTURE 0
#define MESH 1
#define ITEM 2

//下面是抽象工厂工具类
class AbstractFactory{
public:
	Product* create(ProductId);
};

Product* AbstractFactory::create(ProductId id){
	switch(id){
		case TEXTURE :
			return new Texture;
			break;
		case MESH:
			return new Mesh;
			break;
		case ITEM:
			return new Item;
			break;
		default:
			return new Texture;
	}
};

//普通工厂模式
class Factory{
public:
	Texture* createTexture();
	Mesh* createMesh();
	Item* createItem();
};

Texture* Factory::createTexture(){
	return new Texture;
}

Mesh* Factory::createMesh(){
	return new Mesh;
}

Item* Factory::createItem(){
	return new Item;
}

//使用STL实现的抽象工厂模式
class Shape {
public:
	virtual void draw() = 0; //需要子类重写draw方法
	virtual void erase() = 0; //需要子类重写erase方法
	virtual ~Shape() {} //需要子类重写晰构函数
	
	//使用内部类来实现错误日志
	class BadShapeCreation : public logic_error {
	public:
		BadShapeCreation(string type)
			: logic_error("不能创建类型:" + type) {}
	};
	//定义一个工厂方法,用来创建Shape类型
	static Shape* factory(const string& type)throw(BadShapeCreation);
};
 
class Circle : public Shape {
	Circle() {} //私有构造方法
	friend class Shape;
public:
	void draw() { cout << "Circle::draw" << endl; }
	void erase() { cout << "Circle::erase" << endl; }
	~Circle() { cout << "Circle::~Circle" << endl; }
};
 
class Square : public Shape {
	Square() {}
	friend class Shape;
public:
	void draw() { cout << "Square::draw" << endl; }
	void erase() { cout << "Square::erase" << endl; }
	~Square() { cout << "Square::~Square" << endl; }
};
 
Shape* Shape::factory(const string& type)throw(Shape::BadShapeCreation) {
	if(type == "园") return new Circle;
	if(type == "矩形") return new Square;
	throw BadShapeCreation(type);
}
 
char* sl[] = { "园", "矩形", "矩形", "园", "园", "园", "矩形" };

复制代码

AbstractFactory.cpp文件

#include "AbstractFactory.h"
#include "purge.h"

int main(){
	//声明一个抽象工厂对象
	AbstractFactory abFactory;
	
	//使用该工厂对象统一实例化Product对象
	Product* text = abFactory.create(TEXTURE);
	Product* item = abFactory.create(ITEM);
	Product* mesh = abFactory.create(MESH);

	cout << "下面是普通工厂模式//" << endl;
	
	//使用普通工厂模式
	Factory factory;
	text = factory.createTexture();
	item = factory.createItem();
	mesh = factory.createMesh();
	
	cout << "下面是STL实现的抽象工厂模式//" << endl;
	//使用STL实现的抽象工厂模式
	vector<Shape*> shapes;
	try {
		for(size_t i = 0; i < sizeof sl / sizeof sl[0]; i++)
		  shapes.push_back(Shape::factory(sl[i]));
	} catch(Shape::BadShapeCreation e) {
		cout << e.what() << endl;
		purge(shapes);
		return EXIT_FAILURE;
	}
	for(size_t i = 0; i < shapes.size(); i++) {
		shapes[i]->draw();
		shapes[i]->erase();
	}
	purge(shapes);
	
	return 0;
}
复制代码

运行效果

image-20210423102649379.png

组合模式(Composition)

相对于大多数应用类型而言,游戏是特殊的应用类型,它需要保存和使用不同数据类型的集合。它有游戏级别(子级别)、房间、敌人(可以组装,比如一匹马和骑士),以及对象等。所有数据结构可以被描述成整体与部分的结构,以及每个元素是否是原子性或者一个组合性的。从理论上讲,为解决这个问题,组合模式是最好的解决方案。游戏元素类应该定义成纯虚类的,这样我们不能直接创建这个类的对象,而需要通过继承类来实现。这些继承需要继承所有的属性和抽象方法,但是可以使用额外属性来封装位置类型的信息。

图类

image-20210423102852043.png

Composite.h头文件

#include <iostream>
#include <stdexcept>
#include <cstddef>
#include <string>
#include <vector>
#include "purge.h"
using namespace std;
/**
	功能:书写几个用来演示组合模式
	作者:技术大黍
	*/
//2.创建一个接口
class Component{
public:
	virtual void traverse() = 0;
};

//1.接口的实现类,它“是”Component
class Leaf: public Component{
	int value;
public:
	Leaf(int val){
		value = val;
	}
	void traverse(){
		cout << value << ' ';
	}
};

//集合类
class Composite: public Component{
	//1.创建一个集合
	vector<Component*> children; //耦合到接口
public:
	void add(Component * ele){
		children.push_back(ele);
	}
	
	void traverse(){
		for(int i = 0; i < children.size(); i++){
			children[i]->traverse();
		}
	}
};
/实战编程示例//
//定义一个行为接口
class LevelItem{
public:
	virtual float lifePoints() = 0;
};

//玩家的位置实现接口
class Potion: public LevelItem{
public:
	float lifePoints(){
		cout << "在Potion的lifePoints方法中" << endl;
		return 0.0f;
	}
};

//玩家的体力实现接口
class Bread: public LevelItem{
public:
	float lifePoints(){
		cout << "在Bread的lifePoints方法中" << endl;
		return 0.0f;
	}
};

//使用一个组合器,把各种东西组合起来,然后使用一个方法迭代出来。
class CompositeItem: public LevelItem{
	vector <LevelItem*> list;
public:
	void add(LevelItem* item){
		list.push_back(item);
	}
	float lifePoints(){
		for(int i = 0 ; i < list.size(); i++){
			list[i]->lifePoints();
		}
		return 0.0f;
	}
	
};
复制代码

Composite.cpp文件

#include "Composite.h"

int main(){
	Composite containers[4];
	int i = -1;
	for(i = 0; i < 4; i++){
		for(int j = 0; j < 3; j++){
			containers[i].add(new Leaf(i * 3 + j));
		}
	}
	
	for(i = 1; i < 4; i++){
		containers[0].add(&(containers[i]));
	}
	
	for(i = 0; i < 4; i++){
		containers[i].traverse();
		cout << endl;
	}
	
	//游戏中的组合模式
	CompositeItem composites[4];
	for(int k = 0; k < 4; k++){
		if(k % 2 == 0){
			composites[k].add(new Potion());
		}else{
			composites[k].add(new Bread());
		}
	}
	//组合的特点就是使用单一方式来遍历所有的行为
	for(int m = 0; m < 4; m++){
		composites[m].lifePoints();
	}
	
	return 0;
}
复制代码

运行效果

image-20210423103152973.png

代码示例中需要实现级别度数据结构:它可以hold sublevel级别,而每个sublevel级别拥有位置和对象。类Level表示整个级别,类LevelItem描述了内部的敌人单元,以及该敌人的级别:位置、玩家可获取的对象等。以上代码的创建是基于Lord of the Rings游戏的数据结构。我们可以创建两个子级别(Moria和Shire)以及创建者的房屋和每个地区的位置,代码说明了怎样使用组合模式来处理不同类型数据的使用问题。

image-20210423103302896.png

轻量级模式(Flyweight)

最后一个模式是轻量级模式,该模式用来处理大量基本相同对象的集合时非常有用。这种情况下,我们不想因为有很多相似的对象而来消耗内存,而我们希望使用系统资源来使用一个统一接口来有效的访问这些对象。

比如实现策略游戏,所有步兵都都希望两个参数:位置和生命值!但是AI循环、图形处理代码、文本和几何图形数据,以及大多数其它的参数,比如移到速度和武器都是相同的。而轻量级模式把对象分成两个类。

第一,我们需要创建实际轻量级,该轻量级是核心对象,并且它所有的实例共享。轻量级是通过FlyweightFactory来管理,该工厂在一个内存池创建并保存这些对象。轻量级包含所有对象内置元素—所有该对象的上下文的独立信息,并且共享。

第二,我们需要使用轻量级的外部对象,该外部对象是作为一个参数被使用的。这些对象包含了状态信息,比如位置和我们策略游戏战士的位置与生命值。

图类

image-20210423103414791.png

Flyweight.h头文件

#include <iostream>
#include <stdexcept>
#include <cstddef>
#include <string>
#include <vector>
using namespace std;
/**
	功能:书写几个类用来演示轻量级模式
	作者:技术大黍
	*/


//定义一个抽象的轻量级类型
class AbstractFlyweight{
public:
	virtual void paint(string extrInfo);
	virtual void recalculateAI(string extrInfo);
};

void AbstractFlyweight::paint(string extrInfo){}

void AbstractFlyweight::recalculateAI(string extrInfo){}


//1.首先创建轻量级工厂
class FlyweightFactory{
	vector<AbstractFlyweight*> flyweights;
public:
	AbstractFlyweight* getFlyweight(int); //获取轻量级对象
	
	//添加轻量级对象
	void add(AbstractFlyweight* flyweight){
		flyweights.push_back(flyweight);
	}
	
	//需要在构造方法加添加元素
	FlyweightFactory();
};


//实际的轻量对象--步兵
class InfantrySoldier: public AbstractFlyweight{
	float speed;
	float turnSpeed;
public:
	void paint(string extrInfo);
	void recalculateAI(string extrInfo);
};

void InfantrySoldier::paint(string extrInfo){
	cout << "在InfantrySoldier的paint方法中" << extrInfo << endl;
}

void InfantrySoldier::recalculateAI(string extrInfo){
	cout << "在InfantrySoldier的recalculateAI方法中"  << endl;
}

//实际的轻量对象--机枪兵
class Strafer: public AbstractFlyweight{
	float speed;
	float turnSpeed;
public:
	void paint(string extrInfo);
	void recalculateAI(string extrInfo);
};

void Strafer::paint(string extrInfo){
	cout << "在Strafer的paint方法中" << extrInfo << endl;
}

void Strafer::recalculateAI(string extrInfo){
	cout << "在Strafer的recalculateAI方法中" << extrInfo << endl;
}

FlyweightFactory::FlyweightFactory(){
	
	InfantrySoldier* infantrySoldier = new InfantrySoldier;
	add(infantrySoldier);

	Strafer* strafer = new Strafer;
	add(strafer);
}


//通过抽象工厂模式返回需要的轻量级对象
AbstractFlyweight* FlyweightFactory::getFlyweight(int num){
	if (flyweights.size() == 0){
		return 0;
	}else{
		int i = -1;
		for(i = 0; i < flyweights.size(); i++){
			if(i == num){
				break;
			}
		}
		return flyweights[i];
	}
}

//下面是一个客户端程序,用来使用轻量级对象
class Client{
public:
	void paint();
	void recalculateAI();
};

void Client::paint(){
	FlyweightFactory* ff = new FlyweightFactory;
	AbstractFlyweight* flyweight = ff->getFlyweight(0); //0.表示步兵 1.表示机枪兵
	flyweight->paint("我是步兵");
}

void Client::recalculateAI(){
	FlyweightFactory* ff = new FlyweightFactory;
	AbstractFlyweight* flyweight = ff->getFlyweight(1); //0.表示步兵 1.表示机枪兵
	flyweight->recalculateAI("俺是机枪兵!!!");
}

复制代码

Flyweight.cpp文件

#include "Flyweight.h"

int main(){
	//演示轻量级模式
	Client client1, client2;
	client1.paint();
	client2.recalculateAI();
	return 0;
}
复制代码

运行效果

image-20210423103641232.png

补充: Spatial Index(空间序列)

随着游戏不断的复杂,3D效果需求不断增长,使用几个三角表示一个简单对象的时代已经过去。现代游戏中会每秒使用几百万个三角来表示一个对象,甚至最简单的3D效果都出现系统瓶颈,如果使用了复杂的数据集。因此,空间序列(Spatial Index)用来定义一个DP以允许应用程序员可以查询大型的3D环境,比如游戏级别、效果等。使用空间序列处理的情况有:

  • 有特别靠近x单元情况
  • 有多少特别靠近y单元情况

几年前,作者已经完成使用零代价算法完成以上问题的解决方案,这就是空间序列,它只有一个固定的性能代价,也就是说,它只需要一定的计算就可以达到效果,而这种计算是一种独立输入数据集的计算。这也是空间序列的主要特点:它序列空间,这样我们可以不需要知道哪个数据集而执行查询动作。空间序列可以使用不同的数据结构来实现,从最简单到非常复杂的方式都有。一些解决方案非常快,这种方式一般都采取多占内存空间来达到这种效果。

我们可以根据具体来选择相应的数据结构,但是单从API角度讲,一个空间序列可以被看成是一个黑盒,该黑盒可以加快几何图形效果的显示速度,而不管它的内部是怎样实现的。最简单实现方式是把空间序列看成一个链表。不管查询哪个元素,我们都必须每次扫描整个链表,但是因为链表中元素查找需要循环循环实现,所以它可以适用于小数据集(小于20)的处理。比如精典的例子是把敌人保存在一个游戏级别中(不是很多敌人),这种情况下我们使用链表处理,会获取小的代价和高的性能。

image-20210423103935964.png

第二种实现是规则表格来实现,它把空间切割成相同尺寸的桶。每个桶有一个链表保存元素。桶的坏心由装载时间来确定,以便取得良好的性能平衡(桶越小越好,因为桶小内存利用率高)。

image-20210423104046835.png

桶结构的空间序列可以用在所有的几何图形算法之中:我们只需要找出玩家的位置,然后我们把它转换3D位置为cell的坐标,然后再扫描相应的链表(可能在相邻桶的中)。

总之,不管哪种情况,桶结构的效率都比纯链表的效率高。比如有一个空间序列是1000个单位(比如一颗树),平均空间影射1x1公里。我们可使用100类尺寸的表格中保存,也就是创建一个10x10的表格。这样做的结果就是可以使用这种单位来实现碰撞侦测,我们可以简单的扫描cell列表,以及相邻九个cell列表是否有碰撞的情况。因为有1000单元和100桶,我们可假设每个桶有平均装载10个单元,那么扫描10个桶(一个是游戏角色占有的位置,以及九个相邻的桶),包括100最坏的情况。解决方式是使用Quadtree/Octree (四叉树/八叉树)数据结构来解决,其中八叉树是处理实时3D数据的。这样,我们把当前的结点拆成四叉树,四叉树不是非常适合动态几何图形的运算。

image-20210423104229354.png

总结

我们认为:想法即算法,当我们能够掌握这些常用的算法后,那么就可以自己书写游戏引擎了。

最后

记得给大黍❤️关注+点赞+收藏+评论+转发❤️

作者:老九学堂—技术大黍

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值