一、命名法

Pascal命名法:每个单词首字母大写。
Camel命名法:第一个单词首字母小写,其余单词首字母大写。
C++标准库命名法:全小写,单词用下划线分割。

1.1 CSharp

函数和类采用Pascal命名法,变量采用Camel命名法。
代码目录和文件采用Pascal命名法。

1.2 Lua

类采用Pascal命名法,其余采用C++标准库命名法。
代码目录和文件采用C++标准库命名法。

1.3 其它

其它目录和文件采用Pascal命名法。

二、C#代码规范

2.1 命名的基本约定

函数用动词命名,其它的用名词或者形容词命名。

避免使用拼音

原则上避免使用拼音命名代码。

尽量避免缩写

尽量不要缩写名字,名字长没关系,尽可能描述清楚。

类型前缀

类和变量前一般不要加前缀。模板类型加前缀T,接口加前缀I,枚举加前缀E。

类型后缀

特殊类型可选加后缀。
List:可选加List后缀。
Dictionary:可选加Dict后缀。
delegate:加上后缀Event。

命名空间

使用Pascal命名法。
命名空间采用GY开头,比如GYEngine、GYGame。

使用Pascal命名法。
类名要用名词。模板类开头用T。

接口

使用Pascal命名法。
接口开头用I。接口名要用名词或者形容词。

枚举

枚举类型采用Pascal命名法,需要加上前缀E,比如EMessageType。
枚举常量不需要加前缀,采用Pascal命名法,特殊情况下可以拆成两部分用下划线区分,比如Message_Start。

1
2
3
4
5
6
7
8
9
10
11
12
public Enum EWeaponType
{
Knife,
Pistol,
MachineGun,
}

public Enum EMessageType
{
Message_Start,
Message_End,
}

函数

使用Pascal命名法。
函数名最好用动词开头。

委托和事件

使用Pascal命名法。
使用动词短语命名,delegate类型的命名需要加上后缀Event。
event类型的实例需要加上On前缀,Event后缀。

1
2
3
public delegate void KillMonsterEvent();

public event KillMonsterEvent OnKillMonsterEvent = null;

属性

使用Pascal命名法。
属性是对Get和Set的语法封装,一般是public或者protected采有意义。

特性(Attribute)

使用Pascal命名法。
用名词或名词短语+Attribute方式命名特性。
比如,

1
2
3
public class ObsoleteAttribute
{
}

局部变量

采用Camel命名法。

函数参数

采用Camel命名法。

成员变量

类非公有非静态成员变量用m开头。比如mActorId。
类的公有成员变量大写开头,不需要加m前缀,尽量用属性代替公有变量。

静态变量

类的静态成员变量用s开头。
函数内的静态变量用s开头。
比如,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public Actor
{
private int mActorId = 0;

private static int sActorNumInClass = 0;

protected int mActorClassId = 0;

public string ActorName = "name";

public int ActorId
{
get
{
return mActorId;
}

set
{
mActorId = value;
}
}

public delegate void KillMonsterEvent();

public event KillMonsterEvent OnKillMonsterEvent = null;

public Actor()
{
mActorId = 0;
}

public int GetActorNum(bool isFirstTime)
{
static int sActorNumInFun;
int addNum = 1;
return sActorNumInFun = (isFirstTime ? 0 : sActorNumInFun + addNum);
}
}

常量

所有单词大写,多个单词之间用下划线隔开,比如public const int MAX_NUM = 10。

注释

原则上,尽量写可读性良好、自解释的代码,避免写冗余的注释。

文件注释

文件开头必须要有注释,如果是单个类的文件,可以将用类注释替代。

类注释

单个类的文件,必须有类注释。
类注释说明该类是做什么的,可选包含怎么实现以及为什么这么实现的原因。

函数注释

简单函数不需要注释,难以使用的函数需要加注释,想想为什么难以使用,这个时候往往需要重构或者拆分函数代码了。

语句注释

关键难以理解的代码语句,需要加上注释说明。

变量注释

关键变量加上注释,普通的不需要加注释。

2.2 代码风格

类成员排列顺序

  1. 属性:公有属性 、受保护属性
  2. 字段:受保护字段、私有字段(公有字段当作属性对待)
  3. 事件:公有事件、受保护事件、私有事件
  4. 构造函数:参数数量最少的构造函数,参数数量中等的构造函数,参数数量最多的构造函数
  5. 方法:重载方法的排列顺序与构造函数相同,从参数数量最少往下至参数最多。方法按照功能分块,尽可能按照公有、保护、私有的访问级别来分布。

变量

  1. 一行只能声明一个变量,尽量避免用var定义变量类型,除非类型写起来很冗余。
  2. 尽量在声明的同时初始化。
  3. 变量定义在开头,比如类开头或者函数开头。除非是根据条件定义的块变量。

比如,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class People
{
private string mName = "PeopleName";
private int mAge = 0;

public void ChangeAge(int newAge, bool needAddAge)
{
mAge = newAge;
if (needAddAge)
{
int tempAddAge = 1;
mAge += tempAddAge;
}
}
}

语句

  1. 一行只能有一条语句。
  2. 单行复合语句必须加大括号。原则上,即使只有一行语句,也需要加大括号包起来,防止后续修改代码破坏忘记语句范围。比如,

  3. else if等必须新起一行。比如,

    1
    2
    3
    4
    5
    6
    7
    8
    if (isWorkday)
    {
    Work();
    }
    else if (isHoliday)
    {
    Rest();
    }

缩进

代码缩进使用Tab键实现,最好不要使用空格,为保证在不同机器上使代码缩进保持一致,设置Tab键宽度为4个字符。

大括号

  1. 大括号需要占一行对齐,而不是将左大括号放在行尾。
  2. Lambda函数可以将左大括号放在同一行,不需要另起一行。

空格

  1. if、while、for、return等关键词后应有一个空格[eg. “if (a == b)”]。
  2. 运算符前后应各有一个空格[eg. “a = b + c;”]。
  3. 函数调用后不需要加空格。
  4. 左括号后面和右括号前面不需要加额外的空格。

空行

  1. 函数之间必须加空行。
  2. 较长函数的代码块直接用空行分割。
  3. 变量定义可以分块加空行分割。

行长度

每一行代码的行长度,建议不要超过110个字符或者说不超过屏幕宽度。如果超过这个长度,可以按照以下规则换行:

  1. 在逗号后换行。
  2. 在操作符前换行。
  3. 第一条优先于第二条。

函数长度

建议单个函数长度不要超过80行。越简短越好。
超过80行,可以考虑拆分函数重用代码。

类长度

单个类文件原则上不超过1000行。接近或者超过,考虑拆分类或者多个文件实现类。

2.3 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
namespace YMGame
{
public Enum EWeaponType
{
Knife,
Pistol,
MachineGun,
}

public Actor
{
private int mActorId = 0;

private static int sActorNumInClass;

protected int mActorClassId;

public string ActorName;

public int ActorId
{
get
{
return mActorId;
}

set
{
mActorId = value;
}
}

public int GetActorNum(bool isFirstTime)
{
static int sActorNumInFun;
int addNum = 1;
return sActorNumInFun = (isFirstTime ? 0 : sActorNumInFun + addNum);
}

public void SetActorId(int classId, int actorId)
{
static int sNonClassIdActorNum = 0;

mActorClassId = classId;
mActorId = actorId;

if (mActorClassId <= 0 )
{
sNonClassIdActorNum++;
bool isNonClassIdActor = true;
actorId = 0;
}
}
}
}

三、Lua代码规范

除了以下特殊提及到的,Lua的代码规范参照C#的代码规范。

3.1 命名规则

文件(类)名

采用Pascal命名法。

函数

采用Pascal命名法。

文件的local变量

下划线开头,采用Camel命名法。比如_classType。

函数的local变量

采用Camel命名法。

函数参数

采用Camel命名法。

C#代码导出到Lua

必须增加Cs前缀以做区分,比如CsFileManager = CS.GYEngine.FileManager.Instance。

双下划线

双下划线用于一些特殊函数的前缀,比如类的初始化和销毁函数。

日志打印

使用项目规定的log函数。比如使用log.l,可以通过个人logid来过滤其他人日志;警告使用log.w;错误使用log.e,避免使用默认的error。

四、编程技巧

避免使用魔数

代码里面不要出现魔法数字,用常量来替代。

1
2
3
4
5
6
7
8
9
10
11
public double CalculateCircularArea(double radius) 
{
return (3.1415) * radius * radius;
}

// 常量替代魔法数字
public static final Double PI = 3.1415;
public double CalculateCircularArea(double radius)
{
return PI * radius * radius;
}

解释型变量

如下所示,用bool变量代替复杂的条件判断,bool变量的命名可以解释条件判断的意思。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (date.after(SUMMER_START) && date.before(SUMMER_END))
{
// ...
}
else
{
// ...
}

// 引入解释性变量后逻辑更加清晰
bool isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);
if (isSummer)
{
// ...
}
else
{
// ...
}

避免函数参数过多

参数过多时候,可以将参数组合成一个结构体传入,方便后续对参数的修改。

避免函数参数控制函数内部逻辑

可以考虑拆分成多个函数,保证函数职责单一。

避免嵌套过深

可以考虑使用continue、break、return关键字,提前退出嵌套。

分割代码和单一职责

如果函数或者类的代码过长,考虑拆分成多个函数或者类,保证职责单一。

预计算和缓存

比如Component或者UI控件的获得等,可以在初始化的时候获取然后缓存引用,避免重复查询。

避免频繁创建字符串

由于C#中的string是独一无二的,无法修改,所以字符串操作会创建新的字符串,不像C++可以就地初始化或者重复利用对象,因此避免大量使用string的操作符构建字符串,改成使用StringBuilder。

五、安全性编程

5.1 安全性编程原则

判空

C#中的对象都是引用,使用前需要判空,空引用会造成异常。这个是良好的编程习惯。可以用空值传播操作符等,简略代码。

参数检查

对传入的参数要进行安全性检查,比如空引用,索引范围等,非法情况提前返回,然后再进行正常的逻辑处理。

尽可能使用错误处理而不是异常处理

异常有额外的性能消耗,加上异常会破坏调用链,应该尽可能用错误判断得方式处理各种可以预测的问题,而不是抛出异常。游戏引擎内一般不使用异常,比如UE4的源码内就禁用异常。

5.2 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public IEnumerator SpawnObjectAsync(string assetPath, Vector3 position, Quaternion rotation, Vector3 scale, Transform parent = null,
string name = "", Action<GameObject> onSpawnObjectDone = null)
{
if (string.IsNullOrEmpty(assetPath))
{
GYLog.LogError("GameObjectPool SpawnObjectAsync assetPath is IsNullOrEmpty");
yield break;
}

GameObjectPooledItemList pool = null;
if (mAssetPathLookup.TryGetValue(assetPath, out pool) == false)
{
yield return WarmPoolAsync(assetPath, 1, (tempPool) => pool = tempPool);
}

if (pool == null)
{
GYLog.LogError("GameObjectPool SpawnObjectAsync Get GameObjectCollection return null");
yield break;
}

GameObject clone = pool.GetItem();

if (clone == null)
{
GYLog.LogError("GameObjectPool SpawnObject Get GameObject from GameObjectCollection return null");
yield break;
}

clone.SetActiveEx(true);

if (parent != null)
{
clone.transform.parent = parent;
}
clone.transform.position = position;
clone.transform.rotation = rotation;
clone.transform.localScale = scale;

if (name != "")
{
clone.name = name;
}

mInstanceLookup.Add(clone.GetInstanceID(), pool);
mIsDirty = true;

onSpawnObjectDone?.Invoke(clone);
}

比如示例代码,首先做了输入参数检查,然后在执行过程中做了条件检查,检查失败直接主动报错,马上返回。

六、改动权限

项目中可以通过SVN或者Git的权限限制,避免过多人改动底层或者关键代码。下面举例说明,

C#的Engine代码

原则上,Engine代码不做改动,主程或者指定的人有权限改动,其它人需要改动需要事先跟主程沟通后才能改动。

C#的Game代码

在游戏发布之前,Game代码允许改动;在游戏发布之后,改动Game层的C#代码需要热更新二进制包或者打补丁更新,有改动需求需要事先跟主程沟通。

Lua的框架代码

框架代码改动之前需要考虑清楚,客户端程序都有改动权限,改动大的部分最好同步主程或者执行主程等,并且负责跟踪和修复改动后引入的问题

Lua的业务代码

客户端程序一直有改动权限,需要遵守代码规范。

一、内存对齐

C++的对象都会进行内存对齐,所谓内存对齐,指的是对象的地址和大小都会对齐到n的倍数上。比如按照4对齐,那么对象的地址会是4的倍数,对象的大小也是4的倍数。究其原因是,机器在内存对齐的地址上访问数据更快,可以一起取出数据;如果数据存在在不对齐的地址上,需要换成2次对齐地址上的取数据,再组合出原始数据;而且,部分机器根本没有取非对齐的数据。

1.1 默认对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class OrdinaryClassWithMemoryPack
{
public:
int intA;

short shortB;

float floatC;
};

std::cout << "sizeof(int):" << sizeof(int) << std::endl;
std::cout << "sizeof(short):" << sizeof(short) << std::endl;
std::cout << "sizeof(float):" << sizeof(float) << std::endl;
std::cout << "sizeof(OrdinaryClassWithMemoryPack):" << sizeof(OrdinaryClassWithMemoryPack) << std::endl;
std::cout << "address of omp:" << &omp << std::endl << std::endl;

vs2019 x86的结果

vs2019 x64的结果

可以看到,默认都是按照4字节对齐,int和float都是4个字节,short是2个字节,不过强制按照4字节对齐了。对象的地址都是4的倍数,不过64位程序的地址是64位了。

1.2 Pack(n)

假如我们用pack指令强制按照2字节对齐,那么输出结果如何了?

1
2
3
4
5
6
7
8
9
10
11
12
#pragma pack(push)
#pragma pack(2)
class OrdinaryClassWithMemoryPack
{
public:
int intA;

float floatB;

short shortC;
};
#pragma pack(pop)

vs2019 x86的结果

vs2019 x64的结果


从输出结果可以看出,对象还是位于4对齐的地址上,只是对象本身的大小变成10了。short只占2个字节,那么接下来的float并没有强制在4字节的地址对齐,而是根据pack指令对齐在2字节的地址上了。

1.3 实验环境

未避免文章过于啰嗦,接下来的例子只说明vs2019 x86的输出结果。

二、普通类的对象

2.1 基类的对象

接下来的讨论为避免内存对齐的干扰,忽略内存对齐。因此,类的成员变量只有一个int。定义基类如下,

1
2
3
4
5
class OrdinaryClassA
{
public:
int intA;
};

2.2 单继承子类的对象

定义子类如下,

1
2
3
4
5
class OrdinaryClassAFirstSon : public OrdinaryClassA
{
public:
int intAFirstSon;
};

2.3 多继承子类的对象

定义多继承的子类如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class OrdinaryClassASecondSon : public OrdinaryClassA
{
public:
int intASecondSon;
};

class OrdinaryMultipleInheritClassE : public OrdinaryClassAFirstSon, public OrdinaryClassASecondSon
{
public:
int intE;
};
std::cout << "sizeof(OrdinaryClassA):" << sizeof(OrdinaryClassA) << std::endl;
std::cout << "sizeof(OrdinaryClassAFirstSon):" << sizeof(OrdinaryClassAFirstSon) << std::endl;
std::cout << "sizeof(OrdinaryMultipleInheritClassE):" << sizeof(OrdinaryMultipleInheritClassE) << std::endl;

输出结果:

根据输出结果,可以看出:基类是4个字节;子类拥有基类的对象,加上自己的成员,一起是8个字节;多重继承的子类,拥有2个基类对象,加上自己的成员,总共是8+8+4=20个字节。
OrdinaryMultipleInheritClassE的两个基类都继承同一个类OrdinaryClassA,因此E的对象中会有2份A的实例。一般的编程范式中,都要求避免多继承,改用多接口继承。C++在针对这种情况,也有一种虚拟继承的方式来避免数据冗余。

三、带虚函数的类对象

3.1 带虚函数的基类的对象

1
2
3
4
5
6
7
8
9
10
11
12
class VirtualFunClassA
{
public:
int intA;

public:
virtual int VirtualFunA()
{
return 0;
}
};

3.1 带虚函数的单继承子类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class VirtualFunClassAFirstSon : public VirtualFunClassA
{
public:
int intAFirstSon;

public:
virtual int VirtualFunA() override
{
return 0;
}

virtual int VirtualFunAFirstSon()
{
return 0;
}
};

VirtualFunClassA va;
VirtualFunClassAFirstSon vason;

std::cout << "sizeof(VirtualFunClassA):" << sizeof(VirtualFunClassA) << std::endl;
std::cout << "sizeof(VirtualFunClassAFirstSon):" << sizeof(VirtualFunClassAFirstSon) << std::endl << std::endl;

用vs2019调试,自动窗口中显示的va和vason的内存布局如下:

输出结果:

可以看到,类对象内多了一个vfptr(虚函数指针),其中子类的虚函数指针是放在父对象内的。

3.2 带虚函数的多继承子类的对象

现在来考虑多继承的情况,假如多个基类都有虚函数,那么内存布局如何了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class VirtualFunClassB
{
public:
int intB;

public:
virtual int VirtualFunB()
{
return 0;
}
};

class VirtualFunMultipleInheritClassC : public VirtualFunClassA, public VirtualFunClassB
{
public:
int intC;

public:
virtual int VirtualFunA() override
{
return 0;
}

virtual int VirtualFunB() override
{
return 0;
}

virtual int VirtualFunC()
{
return 0;
}
};

VirtualFunMultipleInheritClassC vmc;

std::cout << "sizeof(VirtualFunClassA):" << sizeof(VirtualFunClassA) << std::endl;
std::cout << "sizeof(VirtualFunClassB):" << sizeof(VirtualFunClassB) << std::endl;
std::cout << "sizeof(VirtualFunMultipleInheritClassC):" << sizeof(VirtualFunMultipleInheritClassC) << std::endl << std::endl;

用vs2019调试,自动窗口中显示的vmc的内存布局如下:

输出结果:

可以得出结论:vmc中有2个基类的对象,大小分别是8,自身有一个大小为4的int,因此总共是20的大小;多继承的对象内会有多个虚函数指针,一个指针对应一个带虚函数的基类;子类如果也带非继承而来的虚函数,那么这个虚函数也会放在某个基类的虚函数表内。
因此,多重继承的子类对象,会有多个虚函数指针,对应多个虚函数表,自身虚函数会被合并到某个基类的虚函数表中,不会再多一个虚函数指针和虚函数表。对于多重继承子类的多个虚函数表,可能是分开存储,也可能是连续存储为一个表,只是虚函数指针有一定的偏移。

四、虚拟继承的类对象

下面来讨厌最变态的部分,虚拟继承的对象。

4.1 虚多继承子类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class OrdinaryClassAVirtualFirstSon : virtual public OrdinaryClassA
{
public:
int intAFirstSon;
};

class OrdinaryClassAVirtualSecondSon : virtual public OrdinaryClassA
{
public:
int intASecondSon;
};

class OrdinayVirtualMultipleInheritClassF : public OrdinaryClassAVirtualFirstSon, public OrdinaryClassAVirtualSecondSon
{
public:
int intF;
};

OrdinaryClassAVirtualFirstSon oavson;
OrdinayVirtualMultipleInheritClassF ovmf;

std::cout << "sizeof(OrdinaryClassAVirtualFirstSon):" << sizeof(OrdinaryClassAVirtualFirstSon) << std::endl;
std::cout << "sizeof(OrdinaryClassAVirtualSecondSon):" << sizeof(OrdinaryClassAVirtualSecondSon) << std::endl;
std::cout << "sizeof(OrdinayVirtualMultipleInheritClassF):" << sizeof(OrdinayVirtualMultipleInheritClassF) << std::endl << std::endl;

用vs2019调试,自动窗口中显示的ovmf的内存布局如下:

输出结果:

可以看到2个基类的大小都是12,子类的大小是24。如果是普通继承的话,基类的大小是8,子类的大小是20,这个可以参考2.3。那么,虚继承的对象内肯定多了什么?具体是什么了。

启用类内存布局分析

由于自动窗口无法显示虚拟继承的内存布局了,那么我们只能用其它方式来查看。
如下图,我们通过Project的属性窗口,找到C++ ->命令行,添加新的选项 /d1 reportAllClassLayout。

虚继承的基类内存布局

然后清理工程重新生成,在输出窗口会输出所有类的局部情况,然后搜索OrdinaryClassAVirtualFirstSon,如下图所示,

可以看到,对象内有三个成员,按照顺序分别是vbptr(虚表指针)、数据成员intAFirstSon、基类的数据成员intA。相比普通的继承,多了虚表指针。大小总和是4+4+4=12。

虚继承的多重继承子类内存布局


可以看到,对象的成员按照顺序分别是基类1对象、基类2对象、数据成员intF、虚继承的基类数据成员intA。
大小总和是8+8+4+4=24。基类1和基类2里面都是带1个虚表指针和1个数据成员。
相比普通的继承,多了2个虚表指针,但是减少了重复基类数据,总的大小变化是20+8-4=24。如果,重复的基类OrdinaryClassA有更多的数据成员,那么虚拟继承这种机制就更划算了。

4.2 带虚函数的虚多继承子类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class VirtualFunClassASecondSon : virtual public VirtualFunClassA
{
public:
int intASecondSon;

public:
virtual int VirtualFunA() override
{
return 0;
}

virtual int VirtualFunASecondSon()
{
return 0;
}
};

class VirtualFunClassAThirdSon : virtual public VirtualFunClassA
{
public:
int AThirdSon;

public:
virtual int VirtualFunA() override
{
return 0;
}

virtual int VirtualFunAThirdSon()
{
return 0;
}
};

class VirtualFunVirtualInheritClassG : public VirtualFunClassASecondSon, public VirtualFunClassAThirdSon
{
public:
int intG;

public:
virtual int VirtualFunA() override
{
return 0;
}

virtual int VirtualFunASecondSon() override
{
return 0;
}

virtual int VirtualFunAThirdSon() override
{
return 0;
}

virtual int VirtualFunE()
{
return 0;
}
};

std::cout << "sizeof(VirtualFunClassASecondSon):" << sizeof(VirtualFunClassASecondSon) << std::endl;
std::cout << "sizeof(VirtualFunClassAThirdSon):" << sizeof(VirtualFunClassAThirdSon) << std::endl;
std::cout << "sizeof(VirtualFunVirtualInheritClassG):" << sizeof(VirtualFunVirtualInheritClassG) << std::endl << std::endl;

输出结果:

发现基类的大小变成了20,多了8个字节。子类的从24变成了36,多了12个字节。猜测是多了虚函数指针。

带虚函数的虚继承的基类内存布局


可以看到,内存布局是虚函数指针、虚表指针、数据成员、基类对象(基类的虚函数指针、基类数据成员)。相比不带虚函数的虚拟继承,是多了2个虚函数指针。相比,普通的继承,是多了1个虚表指针和1个虚函数指针。所以,最奇怪的地方是没有像普通继承那样将2个虚函数指针合并成一个。

如果注释掉当前类的虚函数VirtualFunASecondSon,得到的内存布局如下:

区别是少了当前类的虚函数指针,基类对象内的虚函数指针保留。

带虚函数的虚继承的多重继承子类内存布局

这应该是已知的最复杂的类对象布局情况了。按照顺序是基类1、基类2、数据成员、虚拟基类。基类1和基类2内部都是虚函数指针、虚表指针、数据成员,大小都是12,那么总共是24。数据成员大小是4。虚拟基类的内部是虚函数指针、数据成员,大小是8。因此,总共的大小是12+12+4+8=36。
相比不带虚函数的虚拟继承,多了3个虚函数指针,总计12个字节。相比普通的继承,多了2个虚表指针和1个虚函数指针,但是减少了虚拟基类数据的重复,那么总大小是28+12-4=36。

虚拟继承的最终结论

1、虚拟继承的对象内会多一个虚表指针。
2、带虚函数的虚继承,子类和基类的虚函数表不会合并,因此会多一个虚函数指针。
3、多重继承的基类,如果虚继承了共同的基类,那么其共同基类对象只会存在一份,包括数据成员和虚函数指针。

疑问:带虚函数的虚继承为何不合并子类和基类的虚函数指针?

猜测可能跟vs2019对应的vc++编译器实现有关。

4.3 虚表指针的用途

我们知道,虚函数指针指向的是虚函数表,虚函数表内存储的是虚函数的地址。对于采用指针或者引用来动态调用虚函数的情况,会在运行时才能确定真正的虚函数地址,这个就叫做延迟绑定。为了灵活性,失去了部分性能。
那么,虚表指针是用来做什么的?可以肯定的是用于找到共同的基类对象的。猜测虚表指针指向一张table,该table内部存储共同的基类数据在类对象内的偏移。

4.4 虚拟继承实现的编译器差异

根据深入探索C++对象模型的说明,虚拟继承在不同的编译器下有不同的实现,而且C++标准并未规定如何实现。因此,g++的内存布局跟vc++的内存布局可能会有显著差别。

NGUI介绍

NGUI是Unity中最流行的UI插件,在UGUI出现前几乎是Unity唯一的UI解决方案。
NGUI是一个提供高效事件通知框架的强大UI系统。NGUI遵循Kiss准则,其中类代码简洁,多数在200行以内。程序员可以方便的扩展其组件类代码以获得定制的功能。
NGUI官方网址
NGUI官方文档地址

NGUI下载

我们可以从unity商店购买NGUI,或者下载其免费版本。
NGUI的Unity商店
当然也可以下载网上其它人提供的版本学习研究。
NGUI 3.10.2

NGUI导入

下载NGUI后,我们得到的是一个.unitypackage文件,比如NGUI Next-Gen UI v3.6.8.unitypackage。
Unity编辑器中,打开菜单Assets->ImportPackage->CustomPackage,然后选择下载的.unitypackage文件导入编辑器。导入NGUI后,在工程的Assets目录下会出现一个NGUI文件夹,并且Unity编辑器中会多了一个NGUI主菜单。

NGUI例子

打开NGUI->Options->Reset Prefab ToolBar,会出现如下工具条:

NGUI例子
这里面有基本的NGUI控件例子,是我们学习参照的好材料。

NGUI类图

下面是我整理的NGUI类图:
NGUI类图

该类图中列出了NGUI中绝大部分的类。
类图中有两个最重要的分支,UIWidgetContainer分支和UIWidget分支。
NGUI中的大部分控件都继承自UIWidgetContainer,这说明在NGUI中,其实是把控件当作Sprite的容器而已。UIWidget的子类就是Sprite和Texture,表示NGUI中的控件都是图片化的,控件的表现都依赖图片。

NGUI常用组件

UILabel 文本

UIInput 输入框

UITextList 多文本显示框,类似聊天窗

UISprite 图片精灵

UIBotton 按钮

UIToggle 单选框/复选框

UIScrollBar 滚动条

UISlider 滑动条/进度条

UIProgressBar 进度条

UIPopupList 下拉框

UIGrid 将子控件按照单元格布局

UITable UIGrid加强版,类似Html的table

UIPanel 控件渲染器,管理和绘制其下所有的组件

UIScrollView 滚动视窗

UIKeyBinding 给控件的点击或者选中事情绑定按键

UIRoot NGUI的UI根物体

参考资料:

NGUI官网

本文主要介绍编写一个原生的WebGL程序需要哪些步骤。

WebGL程序的软件结构

默认情况下,一个动态网页程序只包括HTML和JavaScript两种语言。
而在WebGL程序中,还包括了第三种语言:GLSL ES。

enter description here

WebGL编程模型

enter description here
上图表示一个WebGL程序运行的主要流程。主要分为3个阶段,应用程序阶段、着色器阶段、片元后处理阶段。
本文接下来按照一定的规律介绍编写一个原生WebGL程序主要的步骤。

获得WebGL渲染环境

在Html中定义canvas标签

1
<canvas id="webgl" width="400" height="400"> </canvas>

在JS代码中获得canvas对象

1
var canvas = document.getElementById('webgl');

通过canvas对象获得WebGL渲染环境

1
var gl = getWebGLContext(canvas);

编写着色器

编写顶点着色器

顶点着色器是用来描述顶点属性(比如位置、颜色、纹理坐标等的程序)
enter description here

编写片元着色器

片元着色器处理光栅后的数据,可以片元将其理解为像素。
片元着色器的输出构成了最终的像素值(开启多重采样的话只构成了某个像素的一部分值)
enter description here

初始化着色器

初始化着色器基本上是一个固定的流程,主要分为以下几个步骤。

创建shader

加载shader源码

编译shader

创建程序

附加编译好的shader

链接程序

使用程序

获得顶点属性

顶点上有各种属性,比如空间坐标、纹理坐标、材质等,一个顶点就是一个属性集合。
如下图所示的立方体,顶点上有2个属性,坐标和颜色。
enter description here
顶点属性可以通过读取模型文件,比如obj文件等获得,或者简单写在代码定义中,比如上图的立方体。

创建顶点缓冲区

缓冲区存在于显存中,能够被显卡直接用来进行渲染,不需要进行数据传输。
在WebGL中,通过以下调用获得一个缓冲区对象。

1
var vertexColorBuffer = gl.createBuffer();

写入顶点数据到顶点缓冲区对象

这个步骤分为两个操作。

首先,绑定创建的缓冲区

1
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);

然后,传输系统内存中上的顶点数据到缓冲区(显存中)

1
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

传输数据的标志

gl.bufferData的第三个参数表示数据的使用标志,表示三种不同的应用场景。
1. gl.STATIC_DRAW :表示数据不会经常改变,通常用于静态物体,比如地形、墙体等。
2. gl.STREAM_DRAW:表示数据使用一次后就会被丢弃。
3. gl.DYNAMIC_DRAW:表示数据会被多次修改,也会被使用多次。

系统会根据usage标示符为缓冲区对象分配最佳的存储位置。
STATIC_DRAW和STREAM_DRAW分配在显存上,DYNAMIC_DRAW可能分配在AGP中。

将顶点数据传输到顶点着色器

目前,我们已经准会了WebGL渲染环境,并且数据已经从系统内存传输到显存中的缓冲区对象中。现在,我们要将缓存区对象中的数据指定给顶点着色器中对应的变量。
顶点着色器中的attribute变量对象顶点的属性。我们的顶点着色器中定义了2个变量,a_Position,a_Color。下面我们分为三步为这其指定数据。

  1. 获得着色器中attribute变量位置
    1
    var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  2. 根据变量位置传入缓冲区中的顶点属性数组

    1
    gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
  3. 启用该attribute变量的属性数组

    1
    gl.enableVertexAttribArray(a_Position);

对于a_Color,我们在系统内存中定义在坐标的后面,因此在第2步中需要进行偏移,gl.vertexAttribPointer的最后一个参数可以指定数据的偏移位置,因此第2步修改为:

1
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);

FSIZE表示float的大小。

传入uniform变量到着色器

着色器中还存在一种uniform变量,这种变量对于所有顶点来说都是一样的。
比如,mvp矩阵就应该定义为uniform变量。一般情况,我们在js代码中计算好mvp矩阵,然后传输到着色器中的uniform变量中。主要步骤如下:
1. 获取uniform变量的在着色中的位置

1
var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');

  1. 计算uniform变量(比如mvp矩阵)的值
1
2
3
var mvpMatrix = new Matrix4();
mvpMatrix.setPerspective(30, 1, 1, 100);
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
  1. 传入uniform变量
1
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

目前,顶点着色器已经有了每个顶点的属性,以及用uniform变量表示的mvp矩阵,因此可以变换顶点属性后传入片元着色器中进一步处理。

定义面片索引

上面我们处理的数据都是顶点属性,但是我们实际要绘制的图元是面片,比如三角面片。
通常情况下,我们会用三个顶点索引表示一个三角面片。
如下所示:

1
2
3
4
5
6
7
8
9
10
// Indices of the vertices
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
0, 3, 4, 0, 4, 5, // right
0, 5, 6, 0, 6, 1, // up
1, 6, 7, 1, 7, 2, // left
7, 4, 3, 7, 3, 2, // down
4, 7, 6, 4, 6, 5 // back
]);

indices表示一个立方体的面片索引。

创建索引缓冲区,写入索引

接下来,我们要创建索引缓冲区,并将内存中的索引数据传入缓存区。
1. 创建索引缓冲区

1
var indexBuffer = gl.createBuffer();
  1. 绑定索引缓冲区
1
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  1. 将面片索引写入缓冲区对象
1
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

根据索引绘制图元

最后一步只需要根据面片索引绘制图元即可。
根据面片的顶点索引绘制图元节省内存,不需要存储重复的顶点数据。
我们只需要调用gl.drawElements即可。

1
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);

其中,第二个参数n表示要绘制的图元(三角形面片)个数。最后一个参数0表示使用已经绑定好的索引缓冲区对象。

完整代码

下面给出绘制一个彩色立方体的完整代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// Vertex shader program
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'uniform mat4 u_MvpMatrix;\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
' v_Color = a_Color;\n' +
'}\n';

// Fragment shader program
var FSHADER_SOURCE =
'#ifdef GL_ES\n' +
'precision mediump float;\n' +
'#endif\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_FragColor = v_Color;\n' +
'}\n';

function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');

// Get the rendering context for WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}

// Initialize shaders
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}

// Set the vertex coordinates and color
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the vertex information');
return;
}

// Set clear color and enable hidden surface removal
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);

// Get the storage location of u_MvpMatrix
var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
if (!u_MvpMatrix) {
console.log('Failed to get the storage location of u_MvpMatrix');
return;
}

// Set the eye point and the viewing volume
var mvpMatrix = new Matrix4();
mvpMatrix.setPerspective(30, 1, 1, 100);
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);

// Pass the model view projection matrix to u_MvpMatrix
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

// Clear color and depth buffer
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

// Draw the cube
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

function initVertexBuffers(gl) {
// Create a cube
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
var verticesColors = new Float32Array([
// Vertex coordinates and color
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, // v0 White
-1.0, 1.0, 1.0, 1.0, 0.0, 1.0, // v1 Magenta
-1.0, -1.0, 1.0, 1.0, 0.0, 0.0, // v2 Red
1.0, -1.0, 1.0, 1.0, 1.0, 0.0, // v3 Yellow
1.0, -1.0, -1.0, 0.0, 1.0, 0.0, // v4 Green
1.0, 1.0, -1.0, 0.0, 1.0, 1.0, // v5 Cyan
-1.0, 1.0, -1.0, 0.0, 0.0, 1.0, // v6 Blue
-1.0, -1.0, -1.0, 0.0, 0.0, 0.0 // v7 Black
]);

// Indices of the vertices
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
0, 3, 4, 0, 4, 5, // right
0, 5, 6, 0, 6, 1, // up
1, 6, 7, 1, 7, 2, // left
7, 4, 3, 7, 3, 2, // down
4, 7, 6, 4, 6, 5 // back
]);

// Create a buffer object
var vertexColorBuffer = gl.createBuffer();
var indexBuffer = gl.createBuffer();
if (!vertexColorBuffer || !indexBuffer) {
return -1;
}

// Write the vertex coordinates and color to the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

var FSIZE = verticesColors.BYTES_PER_ELEMENT;
// Assign the buffer object to a_Position and enable the assignment
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if(a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return -1;
}
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);
// Assign the buffer object to a_Color and enable the assignment
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if(a_Color < 0) {
console.log('Failed to get the storage location of a_Color');
return -1;
}
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
gl.enableVertexAttribArray(a_Color);

// Write the indices to the buffer object
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

return indices.length;
}

上面代码中使用到的创建WebGL渲染环境、初始化着色器、创建矩阵的操作,读者可以自行找相应的代码库替代。
或者在下面的链接中下载:
WebGL Lib, 密码:tncd。

PPT文档如下:

命令行参数和控制台命令

游戏命令包括两种,一种是运行游戏时候指定的命令行参数,另外一种则指的是进入游戏后输入的控制命令。

控制台命令

对于虚幻三来说,控制台命令分为两种,一种是引擎中已经支持的可执行命令,这些命令都是在C++类的Exec函数中进行处理的。另一种是带exec前缀的脚本函数,称为可执行函数。
下面根据用途统一总结,不进行区分。

常用命令

exit(quit)

退出游戏

pause

暂停游戏,按pause break键也可以。

open [url]

打开地图,额外的参数同命令行参数的url部分。比如,在大厅中开始游戏就是用的该命令打开服务器下发的ip地址。

restartLevel

重启当前关卡

reconnect/disconnect/cancel

重新连接服务器,断开服务器连接,取消进行的服务器连接操作。

Kill系列

  1. KillAll [class] - 销毁或破坏关卡中特定类的所有实例。
  2. KillPawns - 销毁关卡中的所有 pawn。
  3. KillBadGuys - 销毁所有不在同一个团队作为玩家的 pawns。
  4. Suicide -玩家自杀

god/fly/walk

幽灵模式(可以飞、穿墙)/飞行模式/正常模式

AllAmmo

将弹药数目设置为所有武器的最大值

SetRes

setres [width] [x|X] [height] [w|f]
改变分辨率(width为宽,height为高)
模式(w = 窗口; f = 全屏),
比如 800x600w表示分辨率为800乘以600的窗口模式,
1024x768f表示分辨率为1024乘以768的全屏模式。

FreeCamera

将玩家的相机设置为自由轨道相机模式(第三人称视角),FreeCamera false恢复。

Addbots

  1. AddBots [number] - 为了进行测试,会向关卡中添加指定机器人数.
  2. AddBlueBots [number] - 在团队游戏中为蓝队添加指定的机器人数。
  3. AddRedBots [number] - 在团队游戏中为红队添加指定的机器人数。
  4. AddNamedBot [name] [bUseTeamNum] [teamnum] - 添加一个使用指定名称的机器人。如果 bUseTeamNum 为真而且指定了团队数,那么将该机器人添加到指定的团队。

Shot

  1. shot/screenshot 以当前的屏幕分辨率截取屏幕截图。
  2. tiledshot [factor] 以当前分辨率乘以指定因数为分辨率来获取屏幕截图。比如,tiledshot 2会得到2乘以2的shot截图,分辨率也是shot的2倍。
  3. SHOTNOHUD,不截屏hud。

渲染命令

ViewMode

viewmode命令设置渲染模式
1. detaillight 默认模式,使用受到法线贴图的光照影响的中性色彩材质渲染场景
2. unlit 无光照
3. lightingonly 只使用光照
4. wireframe 线框模式
5. brushwireframe 线框模式,但是显示画刷边缘
6. lightcomplexity 显示光照复杂度
7. lightmapdensity 显示光照贴图密度
8. litlightmapdensity 显示光照贴图像素密度 6和7的结合
9. texturedensity 显示每个表面上漫反射通道上的贴图像素密度
10. shadercomplexity 显示每个表面上所应用的材质的复杂度

Show

切换各种项目的显示(仅用于客户端)
1. bounds 切换actor边界的显示(包围盒和包围球)
2. volumes 切换体积的显示(体积盒)
3. collision 切换碰撞体的显示状态
4. bsp 切换bsp几何体的显示(用bsp画刷制作的物体,比如墙)
5. fog 切换雾actors的显示
6. particles 切换粒子几何体的显示(比如烟雾弹,特效做的门)
7. paths 切换路径或导航网格物体的显示
8. navnodes 切换和寻路相关的actors的显示
9. foliage 切换植被的显示
10. terrain 切换地形几何体的显示
11. terrainpatches 切换地形块的显示。在每个块的周围描画一个轮廓。
12. staticmeshes 切换静态网格物体几何体的显示
13. decal 切换decal actors的显示
14. decalinfo 切换decals(贴花)的调试开发信息的显示(平头截体、切线轴等)。
15. staticmeshes 切换静态网格物体几何体的显示。
16. postprocess 切换后期处理特效的显示
17. skelmeshes/skeletalmeshes 切换骨架网格物体几何体的显示
18. MISSINGCOLLISION 切换高亮显示启用了碰撞但是没有碰撞网格物体的静态网格物体

显示命令

display系列

displayall class prop

在屏幕上实时地显示类class所有实例的属性prop的值

display obj prop

在屏幕上实时地显示对象obj的属性prop的值。

displayallstate class

在屏幕上实时地显示类class所有实例的当前处于的状态,比如行走,空闲,攻击等

displayclear

清楚display系列命令所有的输出

set

set class/obj prop value
1. 设置给定类class(包括其子类)的所有对象的属性prop的值为value
2. 设置给定对象obj的属性prop的值为value

可以用displayall实时显示出来这个属性,再用set设置后观察属性变化。

统计命令

stat命令负责在游戏运行时在屏幕上启用显示统计数据功能。

none

关闭所有统计数据的显示

fps

切换帧频率统计数据的显示

anim

切换动画系统统计数据的显示状态

net

切换网络统计数据显示的 打开/关闭 状态

game

切换游戏统计数据的显示。(更新时间等)

ui

切换UIScene统计数据的显示

collision

切换碰撞统计数据的显示状态

octree

切换八叉树相关统计数据的显示

physics

切换一般物理统计数据的显示
1. physicscloth 切换关于布料仿真统计数据的显示。
2. physicsfields 切换关于物理域的统计数据的显示状态。
3. physicsfluids 切换关于PhysX流体仿真统计数据的显示。

memory

切换一般内存统计数据的显示

memorychurn

切换处理内存分配的统计数据的显示

scenerendering

切换场景渲染统计数据的显示

startfile/stopfile

  1. startfile开始捕获统计数据文件以便和StatsViewer结合使用。
  2. stopfile完成捕获统计数据文件。
  3. 文件存储位置:UDKGame\Profiling\UE3Stats\xxx文件..ustats
  4. 打开工具:Binaries\StatsViewer.exe

GameProfile/ProfileGame

该命令在虚幻三和UDK中用于统计脚本函数的运行时间。
1. start 开始Profile
2. stop 结束Profile
3. 文件:UDKGame\Profiling\T-2016.11.29-19.26.55.gprof
4. 打开工具:Binaries\GameplayProfiler.exe
5. UObject::CallFunction中统计了每个函数的调用时间。

调试命令

调试命令的结果是控制台形式的输出,并不是在游戏窗口中显示。在逆战中,需要按f8显示控制台窗口,再输入调试命令。其余类型的命令可以使用f7也可以使用f8。

obj

gc/garbage

强制进行垃圾回收清理。

list

显示包中的一个类别的所有物体的列表。
1. obj list显示包中所有的物体列表。
2. obj list class=pawn 只显示指定的类的所有物体的对象列表,比如pawn。

dump

dump objname
在控制台中输出某个对象的所有属性,可以先用displayall找到这个对象名。

物理命令

nxvis collision…

碰撞相关命令

nxvis joint…

关节相关命令

nxvis cloth…

布料相关命令

nxvis fluid…

流体相关命令

nxvis softbody…

软体相关命令

内存命令

mem

显示内存分配信息
1. mem
2. mem detailed
3. mem stat

configmem

显示配置文件内存分配信息

particlememory

粒子内存信息

memfragcheck

内存碎片检测

memleakcheck

内存泄漏检测

UI/GFX命令

ShowHUD

显示(隐藏)所有的HUD

ShowScores

显示(隐藏)积分面板

Toggleui

切换UI的更新和显示

gfxinvoke

调用GfxMovie对应的flash文件的as函数

dumpsftextures

输出GFx Texture Usage到log文件中。

虚幻三控制台命令调用流程

  • APlayerController::ConsoleCommand。
    • ULocalPlayer::Exec,处理一部分命令。
      • UGameViewportClient::Exec,引擎自带的大部分命令在此函数中实现。
        • UGFxInteraction::Exec,执行gfx相关的命令。
        • UUIInteraction::Exec,处理一部分命令。
          • UUIInteraction::ScriptConsoleExec。
          • UGameUISceneClient::Exec。
            • UUISceneClient::Exec.
              • UUISceneClient::ScriptConsoleExec.
        • UGameViewportClient::ScriptConsoleExec。
        • UEngine::Exec,处理一部分命令。
      • UPlayer::Exec,处理一部分命令。
        • UWorld::Exec。
        • APlayerInput::ScriptConsoleExec
        • APlayerController::ScriptConsoleExec
        • APawn::ScriptConsoleExec
        • AInvManager::ScriptConsoleExec
        • AWeapon::ScriptConsoleExec
        • AHUD::ScriptConsoleExec
        • AGameInfo::ScriptConsoleExec
        • ACheatManager::ScriptConsoleExec
        • AInteraction::ScriptConsoleExec
  • 返回APlayerController::ConsoleCommand,命令未处理。

注意:UObkect::ScriptConsoleExec,处理的是当前类中自定义脚本命令(执行带exec前缀的脚本函数)。
因此,流程中带有ScriptConsoleExec函数执行的类(以及子类)都可以定exec脚本函数来执行控制台命令。只有在处理流程中的命令才有效,处理流程外的命令无法被处理。

自定义命令

Native命令

在控制台命令调用流程中涉及到的C++类的Exec函数中添加对新命令的处理逻辑。

脚本命令

在处理流程中的有ScriptConsoleExec调用的类(Interaction、UISceneClient、GameViewportClient、PlayerInput、PlayerController、Pawn、InvManager、Weapon、HUD、GameInfo、CheatManager)
中添加exec前缀的脚本函数。

更多的游戏命令可以参考文档:Console Commands

虚幻引擎游戏命令

命令行参数是指通过命令行或者可执行文件快捷方式启动游戏进程的时候,附加在后面的一系列参数。

命令行参数

命令行参数分为两种,一种是编译游戏代码时候需要用到的命令行参数。另一种则是在启动游戏进程时候指定的参数。

编译命令行参数

编译代码期间会用到的参数如下:

  1. 版本 -debug -release</p>
  2. 全量 -full (默认增量)

  3. 自动更新C++头文件,不弹框确认 -auto
    注意:虚幻三编译脚本.uc文件时候,会更新.h头文件

  4. 移除.u文件中的源代码信息 -stripsource注意:.u文件是虚幻三脚本文件编译后的字节码文件,虚幻四中不存在

    因此,编译代码时候最多可能指定以下参数组合:
    make -debug -full -auto -stripsource
    make -release -full -auto -stripsource

注意:通过在vs中指定make命令可以调试编译代码的过程。
如下图所示:
enter description here

运行命令行参数

启动游戏进程指定的命令行参数分为两个个部分,第一个部分用于指定进程的运行模式(客户端、服务器、编辑器),第二个部分用于指定地图的URL以及附加选项。

运行模式

游戏进程可以用三种不同的模式进行启动,分别是客户端、服务器、编辑器模式。因此,虚幻引擎生成的游戏可执行文件同时可以作为游戏服务器、游戏客户端、游戏编辑器运行。这是一个很神奇的地方。

  1. 客户端模式
    默认情况下,启动的游戏进程就是客户端模式,不需要指定额外的命令行参数。
    在UE4中,也可以指定-game参数。

  2. 服务器模式
    通过指定server参数,可以启动一个游戏服务器。
    比如,udk.exe server,则是使用udk启动一个游戏服务器。
    对于UE4,则是UE4Editor.exe -server。
    实际上,可以修改游戏引擎设置,输出自定义的游戏执行文件。在启动这个游戏可执行文件时候,只需要附近sever参数就可以启动一个游戏服务器。

  3. 编辑器模式
    通过指定editor参数,可以启动一个游戏服务器。
    比如,udk.exe editor,则是使用udk启动游戏编辑器。
    对于UE4,则是UE4Editor.exe -editor。

模式的URL参数

URL分为两个部分:地图名称或者服务器地址,可选的附加参数。
地图部分用于强制游戏启动时候加载特定的地图,附加参数用于设置额外的启动方式,比如设置分辨率,是否打开log窗口等。这些参数和server或editor模式结合起来就可以启动特定地图的服务器或者用编辑器打开特定地图。
如果没有url参数,那么游戏进程会打开默认的地图。URL参数必须在可执行命令名称的后面或者在模式参数后面。

  1. 地图
    如果运行本地游戏,则指定Maps目录下的地图名称,比如MyMap. umap。
    如果运行网络游戏,则指定游戏服务器的IP地址(server模式启动的游戏进程就是游戏服务器)。

  2. 附加参数
    附加参数与地图之间用”?”分隔。
    附加参数分为两种类型,一种是用”=”指定的选项,一种是用”-“指定的开关。

    常用的选项参数:
    dedicated:指定服务器作为专用服务器。
    listen: 指定服务器作为监听服务器 。
    spectatoronly:以观看模式启动游戏
    class: 告诉引擎要使用的玩家类(覆盖默认值)。
    game:: 指定使用的GameInfo类。
    name: 要使用的玩家名称。
    team: 指定玩家所在的团队。
    resx/resy: 设置游戏窗口的分辨率。
    consolex/consoley:设置控制台窗口(log窗口)分辨率。

    常用的开关参数:
    log: 打开日志窗口。
    windowed:窗口模式运行。
    nomoviestartup: 略过启动动画。
    nosplash: 略过启动splash窗口。

    更多的附加参数请参考文档:虚幻四引擎命令行参数

  3. 一些示例:
    UDK.exe server MyMap.udk
    UDK.exe 127.0.0.1
    UDK.exe MyMap.udk?-resX=640 -resY=480 -log log=log.txt
    UDKLift.exe DM-发电站?Game=UTGame.UTTeamGame?listen=true?TeamIndex=0?Name=FS01 -log -windowed -resX=640 -resY=360 -nomoviestartup -nosplash windowPosX=0 windowPosY=0 -consolePosX=0 -consolePosY=365
    MyGame.exe editor MyMap.umap -NoLoadStartupPackages -NoGADWarning

DebugView简介

DebugView是一个监视本地系统或者通过tcp/ip连接的网络系统的OutputDebugString输出的应用程序。DebugView不仅能够监视Win32应用的debug输出,还可以监视内核模型的debug输出。因此,如果使用OutputDebugString来打印调试信息的话,就可以在程序运行时候通过DebugView来实时显示程序的调试信息。
这种方式在某种意义上,比将Log打印到文件中,关闭程序后再查看Log输出的方式更加方便。而且可以将这两种调试程序的方式结合起来,既使用DebugView来实时显示调试信息,又将调试信息输出到Log文件中,方便以后分析。

安装DebugView

下载地址:DebugView
下载后面后解压压缩包,发现里面有三个文件:Dbgview.exe、dbgview.chm、Eula.txt。
Dbgview.exe就是我们要使用的实时显示Log工具。dbgview.chm是自带的文档,有不懂的地方可以查阅该文档。
现在可以将DebugView.exe放到任何你喜欢的目录,比如桌面。

配置DebugView

配置Capture

如下图所示,要记得勾选Capture Win32和Capture Global Win32。Capture Global Win32用于网络模式下捕获网络主机的Debug输出的时候。如果需要捕获内核模式的调试输出,记得勾选Capture Kernel Win32。
enter description here
如果点击Capture Global Win32菜单出现提示:
enter description here
重新以管理员的身份启动DebugView。
enter description here

配置Filter

如下图所示,打开Filter对话框,
enter description hereenter description here
然后在Include中输入要包含的字符串,比如”hankpcxiao”,多个字符串用;分隔,比如”hankpcxiao;xpc”。
这样就只会捕获包括过滤字符串hankpcxiao或者xpc的OutputDebugString输出。
如果我们在每个OutputDebugString输出前自动加上过滤字符串,那么DebugView就只会输出我们的Log信息了。

开启捕获

最后确保开启了捕获,如下图所示:
enter description here

如何在程序中输出Log信息?

默认情况下,DebugView会捕获函数OutputDebugString的输出,但是这个函数的参数是个字符串指针,不太方便。下面我们通过一些列步骤来创建一个方便使用的Log类。

格式化输出

我们习惯使用printf这样的函数来格式化输出信息,因此这次我们也把OutputDebugString包装成可变参数形式的格式化输出函数。

1
2
3
4
5
6
7
8
9
10
11
void DebugViewOutput(const char* fm, ...)
{
static char szMsg[MAX_PATH];

va_list argList;
va_start(argList, fm);
vsprintf_s(szMsg, fm, argList);
va_end(argList);

OutputDebugString(szMsg);
}

让DebugView在VS调试程序时候也能够捕获Log

网上有不少介绍DebugView使用的文章,但是都忽略了一个事实,那就是默认情况下,使用VS运行程序时候,OutputDebugString的输出是到VS的输出窗口中,DebugView中并没有任何信息。只有单独运行程序的时候,DebugView才能够捕捉到信息。
但是这样就不能结合打断点调试和DebugView两个强大的调试方法了。不过,还是有解决办法的。通过一个叫做DBWin通信机制可以实现调试程序时候,把OutputDebugString的输出信息显示到DebugView窗口中。这套机制的本质是通过内存映射文件来跨进程交换数据。
具体参考以下的类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
    class XpcDebugView
{
public:
XpcDebugView() {}
~XpcDebugView() {}
void XpcDebugViewOutput(const char* fm, ...);

private:
struct DBWinBuffer
{
DWORD ProcessId;
char Data[4096 - sizeof(DWORD)];
};
bool Initialize();
void UnInitialize();

HANDLE m_mutex;
HANDLE m_fileMapping;
HANDLE m_bufferReadyEvent;
HANDLE m_dataReadyEvent;
DBWinBuffer* m_buffer;
};

bool XpcDebugView::Initialize()
{
m_mutex = OpenMutex(SYNCHRONIZE, FALSE, TEXT("DBWinMutex"));

//打开DBWIN_BUFFER
m_fileMapping = OpenFileMapping(FILE_MAP_WRITE, FALSE, TEXT("DBWIN_BUFFER"));

if (m_fileMapping == NULL) return false;

//打开DBWIN_BUFFER_READY
m_bufferReadyEvent = OpenEvent(SYNCHRONIZE, FALSE, TEXT("DBWIN_BUFFER_READY"));

//打开DBWIN_DATA_READY
m_dataReadyEvent = OpenEvent(EVENT_MODIFY_STATE, FALSE, TEXT("DBWIN_DATA_READY"));

//等待DBWIN_BUFFER就绪
WaitForSingleObject(m_bufferReadyEvent, INFINITE);

//把DBWIN_BUFFER映射到某个地址
m_buffer = (DBWinBuffer*)MapViewOfFile(m_fileMapping, FILE_MAP_WRITE, 0, 0, 0);
m_buffer->ProcessId = GetCurrentProcessId();
}

void XpcDebugView::UnInitialize()
{
//释放和关闭DBWIN_BUFFER
FlushViewOfFile(m_buffer, 0);
UnmapViewOfFile(m_buffer);

//触发DBWIN_DATA_READY
SetEvent(m_dataReadyEvent);

CloseHandle(m_fileMapping);

//清理
CloseHandle(m_dataReadyEvent);
CloseHandle(m_bufferReadyEvent);
ReleaseMutex(m_mutex);
CloseHandle(m_mutex);
}

void XpcDebugView::XpcDebugViewOutput(const char* fm, ...)
{
if (Initialize() == false) return;

static char szMsg[MAX_PATH];

va_list argList;
va_start(argList, fm);
vsprintf_s(szMsg, fm, argList);
va_end(argList);

#pragma warning( push )
#pragma warning( disable: 4996 )
//向DBWIN_BUFFER写入数据
strcpy(m_buffer->Data, szMsg);
printf(szMsg);
#pragma warning( pop )

UnInitialize();
}

使用的时候直接调用类成员函数XpcDebugViewOutput即可。
关于这部分的内容更具体的可以参考文章,如何让OutputDebugString绕过调试器

输出到DebugView的同时输出到Log文件

我将上面的类改造成下面的样子,在初始化时候创建一个Log文件,在反初始化时候关闭Log文件,每次调用XpcDebugViewOutput使用调用fprintf将格式化字符串输出到文件中。这样就能达到输出Log信息到DebugView中的同时,又能够将Log信息持久化保存了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
    class XpcDebugView
{
public:
XpcDebugView() { m_pszLogName = "DefaultLog.txt"; Initialize(); }
XpcDebugView(const char* pszLogName) { m_pszLogName = pszLogName; Initialize();}
~XpcDebugView() { UnInitialize(); }
void XpcDebugViewOutput(const char* fm, ...);

private:
struct DBWinBuffer
{
DWORD ProcessId;
char Data[4096 - sizeof(DWORD)];
};

bool Initialize();
void UnInitialize();

bool InitializeDBWin();
void UnInitializeDBWin();

HANDLE m_mutex;
HANDLE m_fileMapping;
HANDLE m_bufferReadyEvent;
HANDLE m_dataReadyEvent;
DBWinBuffer* m_buffer;
const char* m_pszLogName;
FILE* m_pFileLog;
};

#pragma warning( push )
#pragma warning( disable: 4996 )
bool XpcDebugView::Initialize()
{
m_pFileLog = fopen(m_pszLogName, "w");
return m_pFileLog != NULL;
}

void XpcDebugView::UnInitialize()
{
if (m_pFileLog)
{
fclose(m_pFileLog);
}
}

bool XpcDebugView::InitializeDBWin()
{
m_mutex = OpenMutex(SYNCHRONIZE, FALSE, TEXT("DBWinMutex"));

//打开DBWIN_BUFFER
m_fileMapping = OpenFileMapping(FILE_MAP_WRITE, FALSE, TEXT("DBWIN_BUFFER"));

if (m_fileMapping == NULL) return false;

//打开DBWIN_BUFFER_READY
m_bufferReadyEvent = OpenEvent(SYNCHRONIZE, FALSE, TEXT("DBWIN_BUFFER_READY"));

//打开DBWIN_DATA_READY
m_dataReadyEvent = OpenEvent(EVENT_MODIFY_STATE, FALSE, TEXT("DBWIN_DATA_READY"));

//等待DBWIN_BUFFER就绪
WaitForSingleObject(m_bufferReadyEvent, INFINITE);

//把DBWIN_BUFFER映射到某个地址
m_buffer = (DBWinBuffer*)MapViewOfFile(m_fileMapping, FILE_MAP_WRITE, 0, 0, 0);
m_buffer->ProcessId = GetCurrentProcessId();
}

void XpcDebugView::UnInitializeDBWin()
{
//释放和关闭DBWIN_BUFFER
FlushViewOfFile(m_buffer, 0);
UnmapViewOfFile(m_buffer);

//触发DBWIN_DATA_READY
SetEvent(m_dataReadyEvent);

CloseHandle(m_fileMapping);

//清理
CloseHandle(m_dataReadyEvent);
CloseHandle(m_bufferReadyEvent);
ReleaseMutex(m_mutex);
CloseHandle(m_mutex);
}

void XpcDebugView::XpcDebugViewOutput(const char* fm, ...)
{
if (InitializeDBWin() == false) return;

static char szMsg[MAX_PATH];

va_list argList;
va_start(argList, fm);
vsprintf_s(szMsg, fm, argList);
va_end(argList);

//向DBWIN_BUFFER写入数据
strcpy(m_buffer->Data, szMsg);
if (m_pFileLog)
{
fprintf(m_pFileLog, szMsg);
}

UnInitializeDBWin();
}
#pragma warning( pop )

如何使用XpcDebugView类

最简单的方式是定义一个XpcDebugView的全局变量,比如:
XpcDebugView myDebugview(“myLog.txt”);
输出Log信息的时候调用函数myDebugview.XpcDebugViewOutput(“%d %d %s\n”, 1, 2, “log”):
为了方便使用,可以在XpcDebugViewOutput的输出后面添加换行符,这样每次调用后就会自动换行了。
并且加上过滤字符串前缀,这样DebugView就只会捕获我们的输出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void XpcDebugView::XpcDebugViewOutput(const char* fm, ...)
{
static char szMsg[MAX_PATH];
static char szOutput[MAX_PATH];

va_list argList;
va_start(argList, fm);
vsprintf_s(szMsg, fm, argList);
va_end(argList);

strcpy(szOutput, "[hankpcxiao] ");
strcat(szOutput, szMsg);
strcat(szOutput, "\n");
if (m_pFileLog)
{
fprintf(m_pFileLog, szOutput);
}

//向DBWIN_BUFFER写入数据
if (InitializeDBWin())
{
strcpy(m_buffer->Data, szOutput);
UnInitializeDBWin();
}
}

总结

首先需要下载好DebugView程序,然后配置capture选项,另外是Filter字符串。最后为了保证在VS中调试程序时候,能够将调试信息输出到DebugView,需要使用DBWin通信进制。为此,我封装了一个Log类,在将Log输出到DebugView的同时也将Log输出到日志文件中。

参考资料:

[1] Frequently asked questions on the Microsoft application DebugView.exe
[2] 如何让OutputDebugString绕过调试器

WebGL概述

什么是WebGL?WebGL简单的说就是在Web中渲染OpenGL的技术,也可以理解为把OpenGL的接口移植到浏览器中使用。具体的可以参考WebGL的维基百科
使用WebGL可以通过编写网页代码在浏览器中渲染三维图像,而且不需要任何的插件,比如Adobe Flash Player等。
WebGL在最新的浏览器中得到了广泛支持。

WebGL与HTML5的关系

HTML5是最新的HTML(超文本标记语言)的最新修订版本。
HTML5中新增了<canvas>标签用于绘图。在HTML5之前,只能使用<img>标签在网页中显示静态图片,如果要显示动画得借助于Adobe Flash Player等第三方插件。在HTML5中,可以在<canvas>标签上绘制二维图像,也可以使用WebGL绘制三维图像。
WebGL相对于HTML5的关系就好比是OpenGL库和三维应用程序的关系。WebGL只是提供了底层的渲染和计算的函数。

WebGL与JavaScript的关系

JavaScript是一种浏览器中运行的动态脚本语言。WebGL也需要依靠JavaScript来操作浏览器中的对象。JavaScript与WebGL的关系类似于C或者C++和OpenGL的关系。

WebGL与OpenGL的关系

WebGL基于OpenGL ES 2.0,WebGL实现了OpenGL ES 2.0的一个子集。WebGL使用Javascript进行内存管理,使用GLSL ES作为着色器语言。具体的关系可以参考下图:
WebGL与OpenGL

WebGL程序的结构

默认情况下,网页程序包括HTML和Javascript脚本语言两部分。但是WebGL程序,还有特殊的GLSL ES着色器语言部分。
具体结构如下图所示:
WebGL程序的结构

总结

本文介绍WebGL的基本概念,以及WebGL和HTML、JavaScript、OpenGL之间的关系等。接下来的文章会介绍具体的WebGL编程知识。

参考

[1] WebGL编程指南
[2] 维基百科

上一篇文章定制IE浏览器弹窗中的外部窗口就是一个不规则窗口,这篇文章介绍下其是如何实现的。思路是根据这张图片创建一个不规则区域,然后将窗口的区域设置为该不规则区域。

第一步,在资源文件rc中设置对话框的属性

Border:None

Style:Popup

第二步,导入背景图片到程序资源中

最好是导入位图,虽然也可以导入其它格式的图片。假设导入位图ID为IDB_BITMAP_BACK。

第三步,在OnInitialDlg函数中,创建区域,并将其设置为窗口区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//OnInitDialog()中
CRgn wndRgn;

m_bitmapBack.LoadBitmap(IDB_BITMAP_BACK);
CreateRgn(m_bitmapBack, RGB(255, 255, 255), wndRgn);
SetWindowRgn(wndRgn, TRUE);

//根据图片创建区域的函数
void CClientBrowserDlg::CreateRgn(CBitmap cBitmap, COLORREF dwColorKey, CRgn wndRgn)
{
CDC *pDC = this->GetDC();
CDC memDC;
//创建与传入DC兼容的临时DC
memDC.CreateCompatibleDC(pDC);

CBitmap *pOldMemBmp=NULL;
//将位图选入临时DC
pOldMemBmp = memDC.SelectObject(cBitmap);

//创建总的窗体区域,初始region为0
wndRgn.CreateRectRgn(0,0,0,0);

BITMAP bit;
cBitmap.GetBitmap (bit);//取得位图参数,这里要用到位图的长和宽

int y;
for(y=0; y <= bit.bmHeight; y++)
{
CRgn rgnTemp;
int iX = 0;
do
{
//跳过透明色找到下一个非透明色的点.
while (iX <= bit.bmWidth memDC.GetPixel(iX, y) == dwColorKey)
iX++;
//记住这个起始点
int iLeftX = iX;
//寻找下个透明色的点
while (iX <= bit.bmWidth memDC.GetPixel(iX, y) != dwColorKey)
++iX;
//创建一个包含起点与重点间高为1像素的临时“region”
rgnTemp.CreateRectRgn(iLeftX, y, iX, y+1);
//合并到主"region".
wndRgn.CombineRgn(wndRgn, rgnTemp, RGN_OR);
//删除临时"region",否则下次创建时和出错
rgnTemp.DeleteObject();
} while(iX < bit.bmWidth );
iX = 0;
}

if(pOldMemBmp)
memDC.SelectObject(pOldMemBmp);
}

第四步,在OnPaint()绘制窗口背景图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void CClientBrowserDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // device context for painting

SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;

// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
{
//选入DC
CClientDC cdc(this);
CDC comdc;
comdc.CreateCompatibleDC(cdc);
comdc.SelectObject(m_bitmapBack);

//生成BITMAP
BITMAP bit;
m_bitmapBack.GetBitmap(bit);

//客户区域
CRect rect;
GetClientRect(rect);

//用客户区的DC绘制所生成的BITMAP,并适应为窗口大小
cdc.StretchBlt(0,0,rect.Width(),rect.Height(),comdc,0,0,bit.bmWidth,bit.bmHeight,SRCCOPY);

CDialog::OnPaint();
}
}

第五步,点击客户区移动窗口

这一点还是有意义的,比如上一篇定制IE浏览器窗口的文章,其外部窗口就是使用这里介绍的不规则窗体。不规则窗体由于是无边框的,因此无法点击边框移动窗口了。因此,设置点击客户端移动是有意义的。而且窗口的内部区域已经被浏览器控件占据了,只有外部的边界区域可以点击到,因此这样刚好模拟出了点击正常窗口边框的效果。

设置客户区可以点击的代码如下:

1
2
3
4
5
6
7
8
9
10
11
LRESULT CClientBrowserDlg::OnNcHitTest(CPoint point)
{
// TODO: Add your message handler code here and/or call default
// 取得鼠标所在的窗口区域
UINT nHitTest = CDialog::OnNcHitTest(point);

// 如果鼠标在窗口客户区,则返回标题条代号给Windows
// 使Windows按鼠标在标题条上类进行处理,即可单击移动窗口
return (nHitTest == HTCLIENT) ? HTCAPTION : nHitTest;
//return CDialog::OnNcHitTest(point);
}

在客户端程序中嵌入浏览器,有两种方式,一种是使用微软的IE控件,一种是使用CEF。这里介绍的是使用CWebBrowser2类(在MFC程序中插入IE的Active控件生成),定制内嵌浏览器窗口的一些经验。

本文的经验积累于实现逆战退出游戏时候的广告弹窗的过程中,下面Show一下这个自带萌妹子的弹窗吧。

这是一个无边框的Windows对话框程序,并且是一个基于背景图片的不规则弹窗窗口;内部嵌入了一个浏览器控件窗口,这个漂亮的妹子就是浏览器控件打开的网页显示出来的。对这个妹子有兴趣的,可以去玩一把逆战,退出客户端的时候就会出来这个弹窗了。

下面介绍一些关于实现该弹窗浏览器的Tips。

一、如何获得CWebBrowser2

方法1:网络搜索下载,比如我以前的一篇博文里面有下载链接:vc内嵌浏览器。

方法2:在MFC程序中插入IE对应的Activex控件,工程中就会生成这个类。

为了定制浏览器窗口,我继承了该类,自定义了浏览器窗口类CYXBrwser。

二、让浏览器窗口适应对话框窗口大小

在对话框类的OnInitDialog()函数中,添加如下代码:

1
2
3
4
5
6
7
8
m_pBrowser = new CYXBrowser(); 
RECT rect;
GetClientRect(rect);
rect.left += 8;
rect.right -= 8;
rect.top += 8;
rect.bottom -= 1;
m_pBrowser->Create(TEXT("NZBrowser"), WS_CHILD | WS_VISIBLE, rect, this, MY_IEBROWSER_ID);

注意,rect的大小需要调节来获得需要的效果。

三、屏蔽右键

有种比较的方法是在PreTranslateMessage中过滤WM_RBUTTONDOWN消息。

1
2
3
4
5
6
7
8
9
10
11
//屏蔽右键
BOOL CYXBrowser::PreTranslateMessage(MSG* pMsg)
{
// TODO: Add your specialized code here and/or call the base class
if(WM_RBUTTONDOWN == pMsg->message)
{
//AfxMessageBox(_T("Right Menu!"));
return TRUE;
}
return CWnd::PreTranslateMessage(pMsg);
}

四、隐藏网页的滚动条

这是最难处理的一个地方。不仅仅需要修改程序,而且需要web端的配合。

第一步:添加DocumentComplete事件响应。在C*Dlg的cpp中添加如下宏:

1
2
3
BEGIN_EVENTSINK_MAP(CClientBrowserDlg, CDialog)
ON_EVENT(CClientBrowserDlg, MY_IEBROWSER_ID, DISPID_DOCUMENTCOMPLETE, DocumentComplete, VTS_DISPATCH VTS_PVARIANT)
END_EVENTSINK_MAP()

注意,CClientBrowserDlg是响应函数所在的类,MY_IEBROWSER_ID是二中指定的浏览器窗口ID。DocumentComplete是CClientBrowserDlg中的响应该事件的成员函数。

第二步:实现该函数,直接贴代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void CClientBrowserDlg::DocumentComplete(LPDISPATCH pDisp, VARIANT* URL)
{
UNUSED_ALWAYS(pDisp);
ASSERT(V_VT(URL) == VT_BSTR);

CString str(V_BSTR(URL));
m_pBrowser->OnDocumentComplete(str);
}

void CYXBrowser::OnDocumentComplete(LPCTSTR lpszURL)
{
m_bDocumentComplete = true;
HideScrollBar();
}

void CYXBrowser::HideScrollBar()
{
HRESULT hr;
IDispatch *pDisp = GetDocument();
IHTMLDocument2 *pDocument = NULL;
IHTMLElement* pEl;
IHTMLBodyElement *pBodyEl;

if (pDisp)
{
hr = pDisp->QueryInterface(IID_IHTMLDocument2, (void**)pDocument);
if (!SUCCEEDED(hr))
{
return;
}
}

if(pDocument SUCCEEDED(pDocument->get_body(pEl)))
{
if(pEl SUCCEEDED(pEl->QueryInterface(IID_IHTMLBodyElement, (void**)pBodyEl)))
{
pBodyEl->put_scroll(L"no");//去滚动条
}
IHTMLStyle *phtmlStyle;
pEl->get_style(phtmlStyle);

if(phtmlStyle != NULL)
{
phtmlStyle->put_overflow(L"hidden");
//需要设置网页源码DOCTYPE为<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
//去除边框才有效
phtmlStyle->put_border(L"none");// 去除边框

phtmlStyle->Release();
pEl->Release();
}
}
}

关键函数是HideScrollBar()。

第三步:在浏览器内嵌网页的最前面添加,

1
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

注意,第三步是不可缺少的。

五、屏蔽多次点击浏览器窗口的提示:”服务器正在运行中”要选择”切换到…”或”重试”的对话框**

在CClientBrowserDlg::OnInitDialog()中添加如下代码,

1
2
3
4
5
/*屏蔽掉"服务器正在运行中"要选择"切换到..."或"重试"的对话框*/
AfxOleGetMessageFilter()->EnableBusyDialog(FALSE);
AfxOleGetMessageFilter()->SetBusyReply(SERVERCALL_RETRYLATER);
AfxOleGetMessageFilter()->EnableNotRespondingDialog(TRUE);
AfxOleGetMessageFilter()->SetMessagePendingDelay(-1);

六、点击网页打开系统默认浏览器

第一步:绑定NEWWINDOW2事件。

1
ON_EVENT(CClientBrowserDlg, MY_IEBROWSER_ID, DISPID_NEWWINDOW2, OnNewWindow2, VTS_PDISPATCH VTS_PBOOL)

第二步:设置该OnNewWindow2的*bCancel为true,并且调用ShellExecute打开网页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void CClientBrowserDlg::OnNewWindow2(LPDISPATCH* ppDisp, BOOL* bCancel)
{
m_pBrowser->OnNewWindow2(ppDisp, bCancel);
}

void CYXBrowser::OnNewWindow2(LPDISPATCH* ppDisp, BOOL* bCancel)
{
*bCancel = TRUE;//禁止弹出新窗口(因为会使用IE弹窗)

HRESULT hr;
IDispatch *pDisp = GetDocument();
IHTMLDocument2 *pHTMLDocument2 = NULL;

if (pDisp)
{
hr = pDisp->QueryInterface(IID_IHTMLDocument2, (void**)pHTMLDocument2);
if (!SUCCEEDED(hr))
{
return;
}
}

if (pHTMLDocument2 != NULL)
{
CComPtr<IHTMLElement> pIHTMLElement;
pHTMLDocument2->get_activeElement(pIHTMLElement);

if (pIHTMLElement != NULL)
{
variant_t url;
hr = pIHTMLElement->getAttribute(L"href", 0, url);
if (SUCCEEDED(hr))
{
CString strURL(V_BSTR(url));
//打开默认浏览器
ShellExecute(m_hWndOwner, NULL, strURL, NULL, NULL, SW_NORMAL);
}
}
}
}

处理了六,五也就不需要了,因为点击网页不会再弹出IE浏览器了。

七、为网页元素的添加事件处理:比如web按钮的点击等

第一步:继承CCmdTarget新建类CHtmlEventHandle,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#pragma once

#import <mshtml.tlb>

// CHtmlEventHandle command target
class CYXBrowser;

class CHtmlEventHandle : public CCmdTarget
{
DECLARE_DYNAMIC(CHtmlEventHandle)

public:
CHtmlEventHandle();
virtual ~CHtmlEventHandle();

public:
void SetWnd(CWnd* pWnd) { m_pWnd = pWnd;}
void SetWebBrowser(CYXBrowser* pWebBroswer) { m_pWebBrowser = pWebBroswer; }
// 消息处理函数
void OnClick(MSHTML::IHTMLEventObjPtr pEvtObj);

private:
CWnd* m_pWnd;
CYXBrowser* m_pWebBrowser;

protected:
DECLARE_MESSAGE_MAP()
DECLARE_DISPATCH_MAP()
DECLARE_INTERFACE_MAP()
};

// HtmlEventHandle.cpp : implementation file
//

#include "stdafx.h"
#include "ClientBrowser.h"
#include "HtmlEventHandle.h"
#include "mshtmdid.h"
#include "MsHTML.h"
#include "YXBrowser.h"

// CHtmlEventHandle

IMPLEMENT_DYNAMIC(CHtmlEventHandle, CCmdTarget)

CHtmlEventHandle::CHtmlEventHandle()
{
EnableAutomation(); // 重要:激活 IDispatch
}

CHtmlEventHandle::~CHtmlEventHandle()
{
}

BEGIN_MESSAGE_MAP(CHtmlEventHandle, CCmdTarget)
END_MESSAGE_MAP()

BEGIN_DISPATCH_MAP(CHtmlEventHandle, CCmdTarget)
DISP_FUNCTION_ID(CHtmlEventHandle, "HTMLELEMENTEVENTS2_ONCLICK",
DISPID_HTMLELEMENTEVENTS2_ONCLICK, OnClick,
VT_EMPTY, VTS_DISPATCH)
END_DISPATCH_MAP()

BEGIN_INTERFACE_MAP(CHtmlEventHandle, CCmdTarget)
INTERFACE_PART(CHtmlEventHandle,
DIID_HTMLButtonElementEvents2, Dispatch)
END_INTERFACE_MAP()

// CHtmlEventHandle message handlers

void CHtmlEventHandle::OnClick(MSHTML::IHTMLEventObjPtr pEvtObj)
{
MSHTML::IHTMLElementPtr pElement =
pEvtObj->GetsrcElement(); // 事件发生的对象元素
while(pElement) // 逐层向上检查
{
_bstr_t strId;
pElement->get_id(strId.GetBSTR());
if(_bstr_t(HTML_CLOSE_BUTTON) == strId)//响应关闭按钮点击
{
PostQuitMessage(0);
break;
}
else if (_bstr_t(HTML_SET_BUTTON) == strId)//30天不弹出设置
{

}

pElement = pElement->GetparentElement();
}
}
//注意那几个宏。宏的具体解释我没有去深究,仿照DISP_FUNCTION_ID可以为点击外的其它事件添加处理。

第二步:在CYXBrowser中注册这个web事件处理类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//添加成员
private:
void InstallEventHandler();
void UninstallEventHandler();

private:
CHtmlEventHandle *m_pEventHandler;
DWORD m_dwDocCookie; // 用于卸载事件响应函数
IDispatch *m_pDispDoc; // 用于卸载事件响应函数
bool m_bDocumentComplete;

//相应的函数实现

// 安装响应函数。省略了一些失败判断以突出主要步骤
void CYXBrowser::InstallEventHandler()
{
if(m_dwDocCookie) // 已安装,卸载先。最后一次安装的才有效
UninstallEventHandler();

m_pDispDoc = GetDocument();
IConnectionPointContainerPtr pCPC = m_pDispDoc;
IConnectionPointPtr pCP;
// 找到安装点
pCPC->FindConnectionPoint(DIID_HTMLDocumentEvents2, pCP);
IUnknown* pUnk = m_pEventHandler->GetInterface(IID_IUnknown);
//安装
HRESULT hr = pCP->Advise(pUnk, m_dwDocCookie);
if(!SUCCEEDED(hr)) // 安装失败
m_dwDocCookie = 0;
}

// 卸载响应函数。省略了一些失败判断以突出主要步骤
void CYXBrowser::UninstallEventHandler()
{
if(0 == m_dwDocCookie) return;

IConnectionPointContainerPtr pCPC = m_pDispDoc;
IConnectionPointPtr pCP;
pCPC->FindConnectionPoint(DIID_HTMLDocumentEvents2, pCP);
HRESULT hr = pCP->Unadvise(m_dwDocCookie);
}

//在OnDocumentComplete中安装事件处理
CYXBrowser::CYXBrowser()
{
m_pEventHandler = new CHtmlEventHandle;
m_pEventHandler->SetWnd(m_pParent);
m_pEventHandler->SetWebBrowser(this);
m_dwDocCookie = 0; // 用于卸载事件响应函数
m_pDispDoc = NULL; // 用于卸载事件响应函数
m_bDocumentComplete = false;
}

void CYXBrowser::OnDocumentComplete(LPCTSTR lpszURL)
{
m_bDocumentComplete = true;
HideScrollBar();
InstallEventHandler();
}

//在OnBeforeNavigate2和OnDestroy中卸载处理
// 在 BeforeNavigate2 和 Destroy 事件中卸载响应函数
void CYXBrowser::OnBeforeNavigate2(LPCTSTR lpszURL, DWORD nFlags,
LPCTSTR lpszTargetFrameName, CByteArray baPostedData,
LPCTSTR lpszHeaders, BOOL* pbCancel)
{
UninstallEventHandler();
// 其他代码...
}

void CYXBrowser::OnDestroy()
{
UninstallEventHandler();
CWebBrowser2::OnDestroy();
}

现在就可以在void CHtmlEventHandle::OnClick(MSHTML::IHTMLEventObjPtr pEvtObj)函数内捕获网页按钮之类的点击了。处理代码的思路是从当前元素开始,不断往上查找父元素,直到匹配的元素ID为止。

八、判断url是否有效,如果无效则打开资源url,防止Web页面为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//使用该函数判断url是否能打开
bool CYXBrowser::IsUrlAvailable(CString strUrl)
{
CInternetSession* session = new CInternetSession();
CInternetFile* file = NULL;
bool bAvailable = true;

try
{
file = (CInternetFile*)session->OpenURL(strUrl);
}
catch (CInternetException*)
{
bAvailable = false;
}

delete session;
if (!file)
{
bAvailable = false;
}

return bAvailable;
}

//在对话框初始化时候,判断外网url是否能打开,如果不能则加载资源内的url
if (m_pBrowser->IsUrlAvailable(theApp.m_strUrl))
{
m_pBrowser->Navigate2(theApp.m_strUrl);
}
else
{
TCHAR szModule[MAX_PATH];
GetModuleFileName(theApp.m_hInstance, szModule, MAX_PATH);
theApp.m_strUrl.Format(_T("res://%s/%s"), szModule, theApp.m_strLocalUrl);
m_pBrowser->Navigate2(theApp.m_strUrl);
}

注意资源url的格式是res://模块名/网页名,因此需要在该程序中导入自定义的资源,并且将其命名为theApp.m_strLocalUrl代表的字符串值,比如”NZ.HTML”。