一点心得


  • 首页

  • 归档

Unity GameObject深度剖析

发表于 2016-05-09

创建一个GameObject实例的过程是怎样的?

UnityEngine的Mono对象 全部是用C++实现的,我们在Mono层的使用的如GameObject、MonoBehaviour这些对象都是Mono层绑定的C++对象。Mono层对这些C++对象做了简单的封装。下图是Mono层GameObject类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//mono
public GameObject(string name)
{
    GameObject.Internal_CreateGameObject(this, name);
}

public GameObject()
{
    GameObject.Internal_CreateGameObject(this, null);
}

public GameObject(string name, params Type[] components)
{
    GameObject.Internal_CreateGameObject(this, name);
    for (int i = 0; i < components.Length; i++)
    {
        Type componentType = components[i];
        this.AddComponent(componentType);
    }
}

可以看到GameObject的所有构造函数都调用了Internal_CreateGameObject静态方法, 接着我们再看这个方法的走向

1
2
3
4
5
6
7
	//cpp
	CUSTOM private static void Internal_CreateGameObject ([Writable]GameObject mono, string name)
	{
		DISALLOW_IN_CONSTRUCTOR
		GameObject* go = MonoCreateGameObject (name.IsNull() ? NULL : name.AsUTF8().c_str() ); 
		ConnectScriptingWrapperToObject (mono.GetScriptingObject(), go); 
	}

通过这个方法Internal_CreateGameObject方法是CShape创建并绑定C++对象的方法,其中很重的是MonoCreateGameObject方法,此方法创建了一个C++的GameObject对象,然后Scripting::ConnectScriptingWrapperToObject把新创建的GameObject的C++对象绑定到了Mono层的GameObject对象,这样我们就可以通过Mono层的GameObject对象访问到C++层的GameObject对象了(Scripting作用空间下定义了很多全局的静态方法,用于Mono和C++交互)。接着我们再看看MonoCreateGameObject方法

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
//cpp
GameObject* MonoCreateGameObject (const char* name)
{
	string cname;
	if (!name)
	{
		cname = "New Game Object";
	}
	else
	{
		cname = name;
	}
	return &CreateGameObject (cname, "Transform", NULL);
}

//cpp
GameObject& CreateGameObject (const string& name, const char* componentName, ...)
{
	// Create game object with name!
	GameObject &go = *NEW_OBJECT (GameObject);

	ActivateGameObject (go, name);

	// Add components with class names!
	va_list ap;
	va_start (ap, componentName);
	AddComponentsFromVAList (go, componentName, ap);
	va_end (ap);

	return go;
}

最终我们看到通过在CreateGameObject方法中分配的内存创建的对象, 这是我们从Mono层创建一个GameObject对象的过程, 通过这个过程我们解释了问题一。

通过GameObject对象获取到自身任何Component的内部实现是怎样的?

Transform只是一个普通的Component和其他继承自Component的组建不同的是,在Mono层的GameObject对象包含了一个Transform的 property, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//mono
namespace UnityEngine
{
	public sealed class GameObject : Object
	{
		public extern Transform transform
		{
			[WrapperlessIcall]
			[MethodImpl(MethodImplOptions.InternalCall)]
			get;
		}
		...
	}
}

1
2
3
4
5
6

//cpp
CUSTOM_PROP Transform transform { FASTGO_QUERY_COMPONENT(Transform) }

 #define FASTGO_QUERY_COMPONENT(x) GameObject& go = *self; return Scripting::GetComponentObjectToScriptingObject (go.QueryComponent (x), go, ClassID (x));

可以看到Mono的GameObject transform属性的get函数内部是通过GameObject的QueryComponent函数来得到C++层的Transform组件,再通过Scripting::GetComponentObjectToScriptingObject返回C++组建对应的Mono层Transform组件。现在我们知道其实Mono层的GameObject并没有持有任何Transform应用了。但是在C++呢?我们在看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
//cpp  GameObject.h
class EXPORT_COREMODULE GameObject : public EditorExtension
{
	public:

	typedef std::pair<SInt32, ImmediatePtr<Component> > ComponentPair;
	typedef UNITY_VECTOR(kMemBaseObject, ComponentPair)	Container;
	...
	private:
	Container	m_Component;
	...
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//GameObject.cpp
...
void GameObject::AddComponentInternal (Component* com)
{
	AssertIf (com == NULL);
	{
		m_Component.push_back (std::make_pair (com->GetClassID (), ImmediatePtr<Component> (com)));
	}
	...
}
...
void GameObject::AddComponentInternal (GameObject& gameObject, Component& clone)
{
	SET_ALLOC_OWNER(&gameObject);
	Assert(clone.m_GameObject == NULL);
	gameObject.m_Component.push_back(make_pair(clone.GetClassID(), &clone));
	clone.m_GameObject = &gameObject;
}
..

C++层的GameObject有一个成员变量m_Component。m_Component的作用可以通过代码中的两个方法AddComponentInternal方法可知,m_Component是用来存储添加到GameObject中的Component的集合。另外我还发现Container包装的Component用ImmediatePtr修饰了,那么ImmediatePtr有什么作用呢?通过查看代码我总结如(感兴趣的同学可以查看BaseObject.h/cpp文件)): Unity Engine在c++层封装了两个对象引用的包装类ImmediatePtr和PPtr。用ImmediatePtr包装的对象引用属于弱引用,用PPtr包装的对象引用属于强引用。变量m_Component为什么要定义成弱类型的呢?稍后再讲。现在GameObject的m_Component保存所有的添加的Component,那就解释了问题二。

通过Component(Component的派生类,如transform)获取到对应的Gameobject对象的内部实现是怎样的?

我们看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
//Mono
public class Component : Object
{
		public extern GameObject gameObject
		{
			[WrapperlessIcall]
			[MethodImpl(MethodImplOptions.InternalCall)]
			get;
		}
		...
}

1
2
3
4
5
6
7
8
9
//cpp

class EXPORT_COREMODULE Component : public EditorExtension
{
	private:

	ImmediatePtr<GameObject>	m_GameObject;
	...
}

通过上面代码可以看出,Component持有了GameObject的引用,这样就解释了问题三了。此外我们还发现了m_GameObject用ImmediatePtr修饰了,现在我们应该明白了为什么要用ImmediatePtr来修饰GameObject的成员m_Component了,因为GameObject和Component两个类互相持有引用,造成循环引用了。所以Unity 自己封装了保持弱引用功能的ImmediatePtr类解决了这个问题。

阅读全文 »

C#全局变量的初始化过程

发表于 2016-04-27

前言

C#类型的成员变量如果没有在定义的时候赋初值都会被默认初始化,初始化的具体指可以参考Default values(c#),那么当我一定一个成员变量的时候直接赋值给值这个变量的话,编译器是怎样处理这个初值的呢?下面我们通过一个简单的例子来说明。

代码

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
using System;

public class TestMemberVariable
{
    public Int32 Id;
    public String Name;
    public Int32 Id2 = 1001;
    public String Name2 = "Microsoft";

    //ctor_1
    public TestMemberVariable()
    {
    }

    //ctor_2
    public TestMemberVariable(Int32 Id, String Name)
    {
        this.Id = Id;
        this.Name = Name;
    }
    //ctor_3
    public TestMemberVariable(Int32 Id, String Name, Int32 Id2, String Name2) : this(Id, Name)
    {
        this.Id2 = Id2;
        this.Name2 = Name2;
    }

    //ctor_4
    public TestMemberVariable(Int32 Id, String Name, Int32 Id2, String Name2)
    {
        this.Id2 = Id2;
        this.Name2 = Name2;
    }

    public override string ToString()
    {
        return String.Format("Id: {0}, Name: {1} - Id2: {2}, Name2: {3}", Id, Name, Id2, Name2);
    }

    public static void Main()
    {
        Console.WriteLine(new TestMemberVariable().ToString());

        Console.WriteLine(new TestMemberVariable(1, "Apple").ToString());
    }
}

上面的代码运行起来输出的结果为: Id: 0, Name: - Id2: 1001, Name2: Microsoft Id: 1, Name: Apple - Id2: 1001, Name2: Microsoft

这里可以看到不管我是调用ctor_1还是调用ctor_2创建的对象,成员变量Id2和Name2都会有定义的初值。那么初值是在什么时候赋给变量的呢?下面我们用ildasm来反编译一下TestMemberVariable.exe,看能不能通过IL,ctor_1和ctor_2的IL代码如下:

ctor_1构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.method public hidebysig specialname rtspecialname 
    instance void  .ctor() cil managed
{
	// Code size       31 (0x1f)
	.maxstack  8
	IL_0000:  ldarg.0
	IL_0001:  ldc.i4     0x3e9
	IL_0006:  stfld      int32 TestMemberVariable::Id2 //这里把初值1001赋值给了Id2
	IL_000b:  ldarg.0
	IL_000c:  ldstr      "Microsoft"
	IL_0011:  stfld      string TestMemberVariable::Name2 //这里把初值"Microsoft"赋值给了Name
	IL_0016:  ldarg.0
	IL_0017:  call       instance void [mscorlib]System.Object::.ctor() //这里是ctor_1构造
	IL_001c:  nop
	IL_001d:  nop
	IL_001e:  ret
} // end of method TestMemberVariable::.ctor

ctor_2构造

.method public hidebysig specialname rtspecialname 
        instance void  .ctor(int32 Id,
                             string Name) cil managed
{
  // Code size       45 (0x2d)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4     0x3e9
  IL_0006:  stfld      int32 TestMemberVariable::Id2//这里把初值1001赋值给了Id2
  IL_000b:  ldarg.0
  IL_000c:  ldstr      "Microsoft"
  IL_0011:  stfld      string TestMemberVariable::Name2//这里把初值"Microsoft"赋值给了Name
  IL_0016:  ldarg.0
  IL_0017:  call       instance void [mscorlib]System.Object::.ctor()//这里是ctor_2构造
  IL_001c:  nop
  IL_001d:  nop
  IL_001e:  ldarg.0
  IL_001f:  ldarg.1
  IL_0020:  stfld      int32 TestMemberVariable::Id
  IL_0025:  ldarg.0
  IL_0026:  ldarg.2
  IL_0027:  stfld      string TestMemberVariable::Name
  IL_002c:  ret
} // end of method TestMemberVariable::.ctor

上面的ctor_1和ctor_2的IL代码我们可以看到,在CLR把c#编译成IL代码之后会在构造函数之前插入Id2和Name2的初始化代码,这样就保证了在构造函数里面或者创建对象之后都可以访问到赋值好的Id2和Name2变量。

其他

看TestMemberVariable.exe反编译之后的IL代码我们可以看到,每个构造函数之前都会被CLR编译器插入已经赋值的成员变量的初始化的代码,那么怎样可以做到复用这一块代码呢?方法就是像ctor_3这样定义构造函数,通过this(Id, Name)可调用到自身的ctor_2构造函数,这样ctor_3在编译之后就不会生成初始化Id2和Name2的IL代码了。

ctor_3构造

.method public hidebysig specialname rtspecialname 
        instance void  .ctor(int32 Id,
                             string Name,
                             int32 Id2,
                             string Name2) cil managed
{
  // Code size       26 (0x1a)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  ldarg.2
  IL_0003:  call       instance void TestMemberVariable::.ctor(int32,
                                                               string)
  IL_0008:  nop
  IL_0009:  nop
  IL_000a:  ldarg.0
  IL_000b:  ldarg.3
  IL_000c:  stfld      int32 TestMemberVariable::Id2
  IL_0011:  ldarg.0
  IL_0012:  ldarg.s    Name2
  IL_0014:  stfld      string TestMemberVariable::Name2
  IL_0019:  ret
} // end of method TestMemberVariable::.ctor

阅读全文 »

C# 的const, static 和 readonly 关键字的理解

发表于 2016-04-09

基本含义

关于c#的const, static, readyonly关键字修饰成员变量时的区别,下面通过一个简单的例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//SomeType.cs

using System;

public sealed class SomeType
{
    public Int32 Id = 1;

    public const Int32 constId = 100; //IL  .field public static literal int32 constId = int32(0x00000032)
    
    public static Int32 staticId = 1000; //IL .field public static int32 staticId

    public readonly Int32 readonlyId = 2000; //IL .field public initonly int32 readonlyId

    public readonly Int32[] readonlyArrayId = new Int32[]{1, 2, 3};//readonlyArrayId本身不能再次write, 但是指向的对象可以改变

    public static readonly Int32 staticReadOnlyId = 5000; //IL .field public static initonly int32 staticReadOnlyId

    public SomeType()
    {
        readonlyId = 20001;
    }
}

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
//TestSomeType.cs

using System;

public class TestConst 
{
    public static void Main(String[] args)
    {
        SomeType someType = new SomeType();

        //test1
        int result = someType.Id + SomeType.constId + SomeType.staticId + someType.readonlyId + SomeType.staticReadOnlyId;

        //test2
        someType.readonlyArrayId[1] = 4;

        Console.WriteLine(someType.readonlyArrayId[1]);//output 4

        //test3
        Console.WriteLine("constId: " + SomeType.constId);//

        Console.WriteLine("staticId: " + SomeType.staticId);
    }
}

一. 结论

  1. readonly

  (1) 修饰成员变量的时候,该变量只能通过变量定义时或者早构造方法里面赋值。

  (2) 修饰的成员变量是实例字段,内存是在构造函数类型的实例时分配的。

  (3) 修饰的成员变量是引用类型的时候,不可改变的是引用,引用指向的对象是可以改变的。

  1. static

  (1) 修饰的成员变量是类型资源,内存是在类型对象中分配的,和具体的实例无关。

  1. const

  (1) 编译成IL代码后由static literal 修饰,由此可见const修饰的变量最终表示成static类型同时被literal修饰( literal修饰的变量必须在变量声明的时候赋值,编译器编译成IL代码时会直接把这个变量的值插入到引用这个变量的位置替代之前的变量,从而程序在运行时不需要再为此变量分配内存)。

const与static的区别

  1. 编译SomeType.cs 和 TestSomeType.cs

//生成SomeType.netmodule csc /t:module SomeType.cs

//生成TestSomeType.exe csc TestSomeType.cs /addmodule:SomeType.netmodule 运行TestSomeType.exe 可以看到test3中的输出为:

constId: 100 staticId: 1000 现在改变SomeType.cs类型的constId和staticId的值分别为50, 500,然后重新生成SomeType.netmodule

接着再次运行TestSomeType.exe可以看到test3中输出为:

constId: 100 staticId: 500 结果发现只有staticId的值引用的新值。再来看看TestSomeType.cs类的Main函数的IL代码:

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 public auto ansi beforefieldinit TestConst
    extends [mscorlib]System.Object
{
    // Methods
    .method public hidebysig static 
        void Main (
            string[] args
        ) cil managed 
    {
       ...

        IL_0000: nop
        IL_0001: newobj instance void SomeType::.ctor()
        IL_0006: stloc.0
        IL_0007: ldloc.0
        IL_0008: ldfld int32 SomeType::Id
        IL_000d: ldc.i4.s 100 //(1)
        IL_000f: add
        IL_0010: ldsfld int32 SomeType::staticId //(2)
     	 
     	 ...
     	 
        IL_003b: ldstr "constId: "
        IL_0040: ldc.i4.s 100 //(3)
        
        ...
        
        IL_0052: ldstr "staticId: "
        IL_0057: ldsfld int32 SomeType::staticId //(4)
        IL_005c: box [mscorlib]System.Int32
        IL_0061: call string [mscorlib]System.String::Concat(object, object)
        IL_0066: call void [mscorlib]System.Console::WriteLine(string)
        IL_006b: nop
        IL_006c: ret
    } // end of method TestConst::Main

    ...

} // end of class TestConst

可以看到上图的四个标记。标记(1)和(3)是constId编译成IL代码的表示,标记(2)和(4)是staticId编译成IL代码的表示。

对于constId 我们可以发现constId其实已经替换成其对应的值了,所以改变SomeType.cs的constId重新编译影响不到已经编译好的TestSomeType程序集。

对于staticId变量标明的是所在模块的信息,当程序运行起来时CLR会加载其对应的netmodule来获取staticId的值。所以改变SomaeType.cs的staticId的值并重新编译,在TestSomeType程序集中即可获取最新的值。

阅读全文 »

C++虚函数的内存结构浅析

发表于 2015-06-20

总结一个下c++虚函数的内存结构问题

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
class A{
public:
    A(){print1();} //在构造函数里面调用virtual函数的做法本身不对,这里只为测试

    virtual void print1(){
        std::cout << "A print1" << std::endl;
    }
};

class B : public A{
public:
    virtual void print1(){
        std::cout << "B print1" << std::endl;
    }
};

class C{
public:
    char ch;
    virtual void print2(){
        std::cout <<"C print2"<<std::endl;
    }
};

class D : public A, public C
{
public:
    int cd;
    virtual void print1(){
        std::cout << "D print1" << std::endl;
    }

    virtual void print2(){
        std::cout << "D print2" << std::endl;
    }
};


int main()
{
    //Q1
    B b; // 输出 A print1

    //Q2
    std::cout << sizeof(D) <<std::endl; //16
    return 0;
}

//A1: 输出A print1的原因是在class A的构造方法里面,对象b的构造方法内部还没有执行, 所以对象b还没有初始化也就没有虚函数表,在A的构造方法里面this指针就是A类型的, 所以是调用A的print1

//A2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//A的内存布局为
----------
__vfptr = 0x00c0ae48 const A::`vftable'{for `A'}

----------

//C的内存布局
----------
__vfptr = 0x00c0abd8 const C::`vftable'{for `C'}
ch = 0xcccccccc
----------

//D的内存布局
---------
A = {__vfptr = 0x00c0ae48 const D::`vftable'{for `A'} }
B = {__vfptr = 0x00c0abd8 const D::`vftable'{for `C'}
     ch = 0xcccccccc
    }
cd = 0xcccccccc
---------

所以 sizeof(D) = 4 + 8 + 4 (C的大小为8是因为内存还要对其)

阅读全文 »
1 … 8 9
© 2025 yiliangduan@qq.com