最近看了看一个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.2http://pan.baidu.com/s/1o61MDv0

参考资料

(1)赵剑宇,《借助Unity思想开发C#版赛车游戏

(2)腾云驾雾,《Unity3D之GameObject与Component的联系

(3)饭后温柔,《Unity3D笔记二:基于组件的设计

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

使用Unity3D的设计思想实现一个简单的C#赛车游戏场景的更多相关文章

  1. 用C写一个简单的推箱子游戏(一)

    我现在在读大二,我们有一门课程叫<操作系统>,课程考查要求我们可以写一段程序或者写Windows.iOS.Mac的发展历程.后面我结合网上的资料参考,就想用自己之前简单学过的C写一关的推箱 ...

  2. 用C写一个简单的推箱子游戏(二)

    下面接着上一篇随笔<用C写一个简单的推箱子游戏(一)>来写 tuidong()函数是用来判断游戏人物前方情况的函数,是推箱子游戏中非常重要的一个函数,下面从它开始继续介绍推箱子的小程序怎么 ...

  3. 如何设计Java框架----一个简单的例子【翻译】

    原文:http://www.programcreek.com/2011/09/how-to-design-a-java-framework/ 原文和翻译都只是参考,如有不对,欢迎指正. 你可能会好奇框 ...

  4. 用MFC完成一个简单的猜数字游戏: 输入的四位数中,位置和数字都正确为A,数字相同而位置不同的为B。

    最近学习了MFC一些比较基础的知识,所以打算通过做一个简单的数字游戏来理解MFC的流程并进一步熟悉其操作. 在这里,我做了一个猜数字的小游戏.第一步当然是设计主界面,先给大家展示一下游戏界面: 主界面 ...

  5. Unity3d 面向对象设计思想(六)(Unity3d网络异步数据)

    在MonoBehavior类中有一个方法是StartCoroutine.里面要求的是一个接口为IEnumerator协同的返回值, 在Unity3d中,协同的作用是马上返回结果的.而不影响其它程序的运 ...

  6. 从一个简单的Delete删数据场景谈TiDB数据库开发规范的重要性

    故事背景 前段时间上线了一个从Oracle迁移到TiDB的项目,某一天应用端反馈有一个诡异的现象,就是有张小表做全表delete的时候执行比较慢,而且有越来越慢的迹象.这个表每次删除的数据不超过20行 ...

  7. 用while循环写一个简单的猜数字游戏

    import random #练习:模拟猜数字的游戏 """ 计算机出一个1~100之间的随机数由人来猜 计算机根据人猜的数字分别给出 大一点/小一点/猜中了 的提示 & ...

  8. 教你如何用python和pygame制作一个简单的贪食蛇游戏,可自定义

    1.效果图 2.完整的代码 #第1步:导出模块 import pygame, sys, random from pygame.locals import * # 第2步:定义颜色变量,在pygame中 ...

  9. 用Python实现一个简单的猜数字游戏

    import random number = int(random.uniform(1,10)) attempt = 0 while (attempt < 3): m = int(input(' ...

随机推荐

  1. Notepad++ 使用nppexec插件配置简易开发环境

    notepad++  采用nppexec插件来配置简易开发环境,而不需要笨重的IDE以及麻烦.重复的命令行.控制台输入: 以下为本人最近用到的脚本配置: //编程语言脚本中$(NAME_PART).x ...

  2. 一个轻量级分布式RPC框架--NettyRpc

    1.背景 最近在搜索Netty和Zookeeper方面的文章时,看到了这篇文章<轻量级分布式 RPC 框架>,作者用Zookeeper.Netty和Spring写了一个轻量级的分布式RPC ...

  3. redis数据类型及使用场景

    Redis数据类型  String: Strings 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字. 常用命令:  set,get,decr,incr,mge ...

  4. [Git] 还原Git上commit,但是没有push代码

    直接在Idea上操作2步解决: 1. 找到: 2. 在To Commit里面填写:HEAD^,表示将commit的信息还原为上一次的,需要多次直接reset多次即可: 使用命令行:原理一样 以下内容转 ...

  5. Error:Execution failed for task ':clean'. > Unable to delete directory :\build\intermediates (转)

    第一种方法: build文件夹,可以使用360文件粉碎机删除,然后重启Android Studio即可! 转自 第二种方法: 进入studio,进入settings,搜索instant run,进入该 ...

  6. 限制input只能输入金额(类似:100.00|100.9|100)

    $(".inputmoney").keyup(function () {    var reg = $(this).val().match(/\d+\.?\d{0,2}/);    ...

  7. FFmpeg纯净版解码 av_parser_parse2

    主要是通过av_parser_parse2拿到AVPaket数据,跟av_read_frame类似. 输入必须是只包含视频编码数据“裸流”(例如H.264.HEVC码流文件),而不能是包含封装格式的媒 ...

  8. jave ee之 servlet 记录

    1:没有自动生成web.xml文件 解决方法:新建web工程的时候最后会选择是否创建web.xml文件 2:通过url映射无法打开对应网站 <servlet> <servlet-na ...

  9. android 常用URI

    关于联系人的一些URI: 管理联系人的Uri: ContactsContract.Contacts.CONTENT_URI 管理联系人的电话的Uri: ContactsContract.CommonD ...

  10. hdu3068马拉车

    其实马拉车还真是最好理解的算法(感觉初中的时候好像讲过类似的,但是当时就没有认真听) 没想到一个简单的优化能变成O(n),感觉碉堡 不说了,马拉车裸题,我在写的时候只保留了id,没保留mx,希望能形成 ...