最近看了看一个C#游戏开发的公开课,在该公开课中使用面向对象思想与Unity3D游戏开发思想结合的方式,对一个简单的赛车游戏场景进行了实现。原本在C#中很方便地就可以完成的一个小场景,使用Unity3D的设计思想(即一切游戏对象皆空对象,拖拽组件才使其具有了活力)来实现却需要花费大量时间与精力,究竟它神奇在什么地方?本文通过实现这个小例子来看看。
一、空对象与组件
在Unity3D最常见的就是GameObject,而一个GameObject被实例化后确啥特性与行为都没有,只有当我们往其中拖拽了一个或多个组件(Component)后才会有行为。例如上图中,我们创建了一个Cube球体,我们想要它能够具有重力,这时我们可以为其添加一个刚体组件,该组件帮我们实现了重力的效果,如下图所示,该球体具有了重力,会进行*落体运动。
组件(Component)是用来绑定到游戏对象(Game Object)上的一组相关属性。本质上每个组件是一个类的实例。Unity3D常见的组件有:MeshFilter、MeshCollider、Renderer、Animation等等。其实不同的游戏对象,都可以看成是一个空的游戏对象,只是绑定了不同的组件。比如:Camera对象,就是一个空对象,加上Camera、GUILayer、FlareLayer、AudioListener等组件。其他对象绑定的组件,可自行观察。
下面的代码则展示了在Unity3D中实现为GameObject加入刚体组件,可以看到GameObject提供了一个实例方法:AddComponent<T>
GameObject goCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
goCube.transform.position = new Vector3(, , );
// 为Cube添加刚体组件
goCube.AddComponent<Rigidbody>();
到底有哪些组件可以添加呢?可以说有无数种组件,只是有一些特别常用的,被Unity3D预先弄好了。组件的目的是为了控制游戏对象,通过改变游戏对象的属性,以便同用户或玩家进行交互。不同的游戏对象可能需要不同的组件,甚至有些需要自定义的组件才能实现。
二、设计思路
2.1 GameObject—基本对象
在GameObject的设计中,首先定义了一个Transform类,定义游戏对象的Position(坐标位置)、Scale(缩放比例)等基本信息,然后提供方法供接受拖拽到自己身上的游戏组件并记录到集合中。利用事件的特性(事件链),当GameObject的特定事件(这里主要是KeyDown、KeyUp与Update三个事件)被触发时,会依次触发注册到该GameObject的所有组件的特定事件方法。
可以从类图中看出,GameObject作为基本对象,没有实现具体的表现和行为,而是提供了可供添加组件的方法来实现让我们可以将组件拖拽到其上边,让组件来控制GameObject的行为和展现。
2.2 Component—万能组件
在对组件的设计中,采用了完全的面向对象思想设计。首先,IComponent接口定义了在本游戏中各个组件需要实现的一个或多个方法,各个组件只需要实现IComponent接口便可以被注册到GameObject中。其次,由于各个组件都具有一些公有的特性,因此设计了一个组件基类BaseComponent,它实现了一个Start()方法,并确保该方法只被调用一次。最后,继承于BaseComponent设计实现各个不同的游戏组件,他们重写了一个或多个基类中实现IComponent中的方法。有了这些组件,我们就可以将其注册到游戏对象上,游戏也就因此有了活力。
三、实现流程
3.1 实现GameObject类
(1)设计Delegates类,它定义了游戏中需要的所有的委托定义,方便了事件的实现。
public class Delegates
{
public delegate void UpdateEventHandler(GameObject sender, Rectangle rect, Graphics g); public delegate void KeyDownEventHandler(GameObject sender, KeyEventArgs e); public delegate void KeyUpEventHandler(GameObject sender, KeyEventArgs e);
}
(2)在GameObject中定义所有Delegates中的委托为事件实例,并提供执行事件的公有方法。
(3)在GameObject中定义AddComponet方法,提供对为游戏对象添加组件的代码实现。(PS:这里方法定义时需要使用泛型)
public class GameObject
{
// 控制游戏对象变换的属性Transform
public Transform Transform { get; set; }
// 控制能够拖拽到游戏对象的组件集合
public IList<IComponent> Components { get; set; } public GameObject()
{
this.Transform = new Transform()
{
Position = new Position(),
Scale = new Scale(, ),
Rotation =
};
this.Components = new List<IComponent>();
} // Update事件
public event Delegates.UpdateEventHandler Update;
// KeyDown事件
public event Delegates.KeyDownEventHandler KeyDown;
// KeyUp事件
public event Delegates.KeyUpEventHandler KeyUp; // 执行Update事件
public void OnUpdate(object sender, PaintEventArgs e)
{
if (Update != null)
{
Update(this, e.ClipRectangle, e.Graphics);
}
} // 执行KeyDown事件
public void OnKeyDown(object sender, KeyEventArgs e)
{
if (KeyDown != null)
{
KeyDown(this, e);
}
} // 执行KeyUp事件
public void OnKeyUp(object sender, KeyEventArgs e)
{
if (KeyUp != null)
{
KeyUp(this, e);
}
} // 提供方法供接受拖拽到自己身上的游戏组件
public TResult AddComponent<TResult>() where TResult : IComponent, new()
{
// 创建游戏组件
TResult component = new TResult();
// 为游戏对象注册组件的事件
this.Update += component.Update;
this.KeyDown += component.KeyDown;
this.KeyUp += component.KeyUp; return component;
}
}
3.2 实现游戏对象的事件
(1)设计BaseComponent类,它是各个游戏组件的基类,实现了IComponent接口,并定义了Start方法(该方法只会在开始时被执行一次)。
public abstract class BaseComponent : IComponent
{
public GameObject GameObject { get; set; }
protected bool isStarted = false; // 游戏组件启动时的事件(该事件只被执行一次)
public virtual void Start(GameObject sender, Rectangle rect, Graphics g)
{
// 记录当前的游戏对象
this.GameObject = sender;
} // 游戏组件每一帧都要执行的Update事件
public virtual void Update(Common.GameObject sender, System.Drawing.Rectangle rect, System.Drawing.Graphics g)
{
// 首先确保Start方法只被执行一次
if (!isStarted)
{
Start(sender, rect, g);
isStarted = true;
}
} // 当用户按下键盘某个键时触发的KeyDown事件
public virtual void KeyDown(Common.GameObject sender, System.Windows.Forms.KeyEventArgs e)
{ } // 当用户松开键盘某个键时触发的KeyUp事件
public virtual void KeyUp(Common.GameObject sender, System.Windows.Forms.KeyEventArgs e)
{ }
}
(2)实现游戏组件子类:BackgroudBehavior(游戏背景组件)、SpriteRender(对象渲染组件)、UserControl(用户控制组件):为BackgroudBehavior添加一个SpriteRender组件已实现渲染游戏背景图片,SpriteRender则负责将图片属性进行渲染到窗体界面中,UserControl则负责实现玩家控制赛车的上下左右移动。这里以UserControl组件为例,通过重写KeyDown和KeyUp两个事件完成对玩家小车方向的控制(通过改变x,y两个滑动值,然后再窗体中通过定时器迅速地更新坐标值,最后重绘整个窗体界面,只不过刷新地频率很快)。
public class UserControl : BaseComponent
{
private int x;
private int y;
private Timer timer; public override void Start(Common.GameObject sender, System.Drawing.Rectangle rect, System.Drawing.Graphics g)
{
base.Start(sender, rect, g);
timer = new Timer();
// 为Timer注册Tick事件让玩家可以进行移动的操作
timer.Tick += (s, e) =>
{
Move(this.x, this.y);
};
timer.Interval = ;
timer.Start();
} // 实现控制玩家赛车的移动->当前坐标+=x,y这两个滑动值的值
private void Move(int x, int y)
{
var pos = GameObject.Transform.Position;
pos.X += x;
pos.Y += y;
// 将改变后的坐标重新赋值给游戏对象的坐标
this.GameObject.Transform.Position = pos;
} // 实现玩家控制赛车的上下左右移动->为x,y这两个滑动值赋值
public override void KeyDown(Common.GameObject sender, System.Windows.Forms.KeyEventArgs e)
{
if (e.KeyCode == Keys.W)
{
this.y = ;
}
else if (e.KeyCode == Keys.S)
{
this.y = -;
}
else if (e.KeyCode == Keys.A)
{
this.x = -;
}
else if (e.KeyCode == Keys.D)
{
this.x = ;
}
} // 当键盘键抬起时将x,y这两个滑动值均赋为0
public override void KeyUp(Common.GameObject sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.W)
{
this.y = ;
}
else if (e.KeyCode == Keys.S)
{
this.y = ;
}
else if (e.KeyCode == Keys.A)
{
this.x = ;
}
else if (e.KeyCode == Keys.D)
{
this.x = ;
}
}
}
3.3 实现游戏窗体与游戏场景
(1)BaseForm为所有Form的基类,它重写了OnLoad方法,使用双缓冲解决屏幕闪烁问题。MainForm为BaseForm的子类,作为游戏的主界面显示。
(2)GameScene类为游戏场景类,这里只有一个场景,所以只有一个GameScene类。GameScene通过记录当前的游戏场景与当前场景中所有的游戏对象(通过集合记录),通过Timer定时使窗体触发重绘,还提供了AddGameObject与RemoveGameObject方法供窗体添加和移除游戏对象使用。
public class GameScene
{
// 记录当前正在运行的游戏窗体
public BaseForm target { get; set; } // 记录游戏场景中的所有游戏对象
public IList<GameObject> GameObjects { get; set; } public GameScene(BaseForm target, int fps)
{
// 初始化当前正在运行的游戏窗体
this.target = target;
// 初始化游戏对象集合
GameObjects = new List<GameObject>();
// 启动一个定时器不停的刷新当前场景使其发生重绘
var timer = new Timer();
timer.Interval = / fps;
timer.Tick += (s, e) =>
{
// 使界面无效并发生重绘
this.target.Invalidate();
};
timer.Start();
} // 将游戏对象添加到集合中并且注册相应的事件给窗体
public void AddGameObject(GameObject go)
{
GameObjects.Add(go);
// 为游戏场景窗体添加相应的事件
this.target.Paint += go.OnUpdate;
this.target.KeyDown += go.OnKeyDown;
this.target.KeyUp += go.OnKeyUp;
} // 将游戏对象从集合中移除并移除相应的组件事件
public void RemoveGameObject(GameObject go)
{
GameObjects.Remove(go);
// 为游戏场景窗体移除相应的事件
this.target.Paint -= go.OnUpdate;
this.target.KeyDown -= go.OnKeyDown;
this.target.KeyUp -= go.OnKeyUp;
}
}
3.4 初始化游戏
(1)创建一个游戏场景对象,传入主窗体实例与FPS帧率;
(2)创建一个GameObject作为游戏背景对象(GameObject最初都是空对象),然后加入BackgroundBehavior组件,最后加入游戏场景的GameObjects集合中。
(3)创建一个GameObject作为玩家对象,设置其Position与Scale,并为其加入UserControl组件与SpriteRender组件,最后加入游戏场景的GameObjects集合中。
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e); // Step1.创建一个游戏场景
this.GameScene = new GameScene(this, FPS); // Step2.创建游戏背景对象(空对象)
var background = new GameObject();
// U3D精妙之处:为空对象添加背景组件即变成了游戏背景对象
background.AddComponent<BackgroundBehavior>();
// 将游戏背景添加到游戏场景中的集合中
this.GameScene.AddGameObject(background); // Step3.创建游戏玩家对象(空对象)
var player = new GameObject();
player.Transform.Position = new Position(,-);
player.Transform.Scale = new Scale(0.15, 0.15);
player.AddComponent<CrazyCar.Component.UserControl>();
var render = player.AddComponent<SpriteRender>();
// 设置渲染组件要显示的图片
render.Source = Resources.Player1;
this.GameScene.AddGameObject(player);
}
最终的运行效果如下图所示:
这里一个简单的赛车游戏场景就实现完毕,虽然这样一个场景十分简单,但是通过将面向对象思想与Unity3D中的组件化思想结合起来,我们发现实现一个游戏会很麻烦。但是,Unity3D正是帮我们做了这样的基础工作,所以才有了我们可以方便的拖拽组件的便利,在扩展性方面展现了很好的威力。
附件下载
CrazyCar v0.2 : http://pan.baidu.com/s/1o61MDv0
参考资料
(1)赵剑宇,《借助Unity思想开发C#版赛车游戏》
(2)腾云驾雾,《Unity3D之GameObject与Component的联系》
(3)饭后温柔,《Unity3D笔记二:基于组件的设计》