场景编辑器系列1——将XNA绘制到WinForm中
当游戏变得越来越复杂时,编写一个编辑器简化制作过程就变得越来越重要。比方说在我的引擎StunEngine中,在场景中放置一个模型,需要使用一个Vector3设置它的位置属性,但因为没有直观的参考,位置往往不尽如人意,这时就需要在源代码中重新调整这个位置,再次生成项目,看看是否合适,这个工作往往要进行多次。如果有一个场景编辑器,那么只需像3DS MAX一样,通过光标就可以对模型进行平移、选择、缩放等操作了。然后可以将场景中的信息保存为xml文件,引擎只需调用这个xml文件就可以将编辑器中放置好的对象重现出来。
编辑器界面参考
首先看一下大名鼎鼎的星际争霸2的场景编辑的界面:
可见一个编辑器通常包含菜单栏,工具栏,左侧分栏中通常显示的是各个对象的列表和它们的属性,右侧分栏是编辑器主界面。但我认为因为大多数人是右撇子,所以应该将常用的功能放置在右侧而不是左侧,这一点上Visual Studio的界面更符合用户使用习惯。
接下来是用C++写成的开源引擎Delta3D(http://www.delta3d.org/)所附的关卡编辑器:
这个编辑器类似于3DS Max实现了前、顶、右和透视四个视口,这个效果应该可以实现,即将XNA图像绘制到四个控件中,每个控件相机的朝向不同,我没有尝试过。下面要提到的一个WPF程序就实现了类似效果。
然后是用于XNA开发的商业引擎Torque X(http://www.torquepowered.com/products/torque-x):
右侧分栏中包含场景浏览器、素材浏览器、对象属性三个面板。
最后是一个我认为做得最好的一个XNA编辑器Rough Edges Editor(http://www.visionsofafar.com/dablog/category/XNA.aspx?page=2)的界面,源代码RoughEdges.zip下载:
将XNA嵌入WinForm的方法
如果上网搜索,可以找到很多XNA编辑器的实现,总结下来大致有两种方法:
第一种是xna官网上的例子WinForms Graphics Device示例,简单地说就是自己编写一个从System.Windows.Forms.Control继承的控件,然后自己编写绘制代码,网上的Jemgine(http://jemgine.codeplex.com/)就是这种思路,不过目前源代码还想缺个文件,我无法调试成功,截图如下。
此外还有生成树木的开源软件XNA Procedural Ltrees(http://ltrees.codeplex.com/)也使用了同样方法实现了WinForm界面,源代码LTrees-source-2.0a.zip下载,截图如下:
但使用这种方法的缺点就是你无法再使用Microsoft.Xna.Framework.Game类,导致也无法使用GameComponent等位于此命名空间下的类。我曾经翻译过的InnovationEngine引擎(http://www.innovativegames.net/blog/)就没有使用Game类,而且它的GameComponent组件也是自己编写的,因此它很容易使用第一种方法创建一个场景编辑器,不过它的编辑器教程只开了头就没了下文。网上有Northwind Engine(http://www.northwind-dev.net/)就是基于这个引擎制作的,不过它的编辑器并没有使用WinForm窗体。
但是我的引擎是从Game类继承的,因此要使用第一种方法,可能就需要对引擎进行伤筋动骨地修改,所以只能退而求其次使用第二种方法。
第二种方法的核心思路就是将XNA绘制的输出重新定向到一个WinForm控件中,这种方法要简洁的多。但是因为使用了Game类,所以XNA会自动生成一个Form,但是我们并不想显示这个Form,需要将它隐藏,因此这个实现方法肯定没有第一种方法效率来得高。
我以前也翻译过一篇文章:XNA和WinForm,文中的实现原理就是第二种方法,但是我觉得它比较难用,用我的话说:它使用的方法是以Game类为核心,但我擅长的方法是以Form类为核心,所以具体实现并没有参照上文。不过本网站上的StunEngine的Sample05用的是这个方法,这是因为Sample05比较简单,如果复杂如编辑器的话,代码会难以阅读。
具体实现
具体的实现过程参考了网上的这篇文章http://www.codeproject.com/KB/game/XNA_And_Beyond.aspx,下文简称XNABeyond(源代码下载:XnaAndBeyond_src.zip)。
而且这篇文章不仅介绍了如何将XNA嵌入到WinForm,而且还介绍了如何将XNA嵌入到WPF中,应该说WPF是未来的潮流,而且在Net4.0中还能实现漂亮的Ribbon界面,所以如果时间允许,以后我会将场景编辑器移植到WPF中。这篇文章的第三个部分还通过SilverLight将XNA嵌入到了网页中,但我没有调试成功,以后再研究吧。
同样在codeproject网站上还有个将XNA嵌入WPF的例子http://www.codeproject.com/KB/WPF/XnaInWPF.aspx,可见WPF的界面要比WinForm漂亮,这个例子还实现了类似于3DS MAX的分屏效果,如下图所示(源代码下载:7026_XnaInWpf.zip)。
好了,下面言归正传,虽然原理参照了上述文章,但根据我的引擎做了一些修改。
首先创建一个Windows Game项目,我把它命名为SceneEditor,然后在解决方案中添加引擎项目StunEngine的引用,如下图所示。
接着在SceneEditor项目中添加一个Windows窗体(IDE会自动添加System.Windows.Forms的引用),我命名为MainForm,然后设计以下界面:
其中包括顶部的菜单栏,工具栏,底部的状态栏,中间是一个splitContainer控件,左侧添加一个Panel控件,XNA的输出图像就是绘制在这个控件上的,右侧再添加一个水平拆分的splitContainer控件,此控件上方添加一个TreeView控件,主要用于显示场景中所有对象的信息,下方是一个propertyGrid控件,用于显示被选择的对象的属性。
然后添加一个类,我命名为ScenEditor,这个类就是游戏的主类,它从引擎类StunEngine继承,下面是关键代码,注意我的实现原理与XNABeyond相同,但做了一些修改,使用起来更灵活。
1.引擎类需要对WinForm窗体(本例中是MainForm)和绘制输出其上的Control的引用(本例中是左侧的panel1),为了使用灵活,需在引擎项目StunEngine中实现,所以首先在StunEngine项目中添加一个接口,表示引擎必须实现WinForm和Control,我命名为IWinForm,代码如下:
namespace StunEngine { ////// 通过StunEngine引擎绘制的Windows Form接口 /// public interface IWinForm { ////// 获取绘制XNA图像的winform控件,通常是一个panel /// Control winControl { get; } ////// 获取Windows Form对象 /// Form Form { get; } } }
2.在引擎类StunEngine中,需要实现对WinForm和Control的引用;添加WinForm和Control的几个事件处理程序;隐藏XNA的游戏窗口;将XNA的图像输出重新定向到Control上,这需要修改引擎类的构造函数,为了代码清晰,我使用了部分类partial class。需要再添加一个类,我命名为StunEngineWinForm.cs,此类中的构造函数以IWinForm为参数,包含用于编辑器的其他代码。现在StunEngine.cs中的不带参数的构造函数创建用于XNA游戏,StunEngineWinForm.cs中以IWinForm接口为参数的构造函数用于编辑器:
namespace StunEngine { /*************************************************************************** * 这个部分类只包含用于编辑器的支持Windows.Forms的代码 ******************************************************************************/ ////// 引擎主类 /// public partial class StunXnaGE : Game { #if !XBOX ////// 用于绘制XNA图像的Windows.Forms.Control控件,通常是一个panel控件 /// protected Control winControl; ////// 用于绘制的Windows.Forms.Form窗体 /// protected Form winForm; ////// XNA框架的游戏窗口 /// private Form gameWindowForm; #endif #if !XBOX //========================================================================== ////// 使用IWinForm接口为参数创建一个新引擎对象。 /// 不要创建这个类的示例,而是应该从这个类继承。 /// StunXnaGE派生自Microsoft.Xna.Framework.Game,使用方法与Game对象相同。 /// IWinForm接口使用GraphicsDevice.Present(handle)将绘制重定向到windows窗体的某个控件。 /// /// IWinForm接口,实现了对winForm和winControl的引用 public StunXnaGE(IWinForm form) : this() { if (form != null) { this.winForm = form.Form; this.winControl = form.winControl; // 添加一些处理事件 this.winForm.FormClosing += new FormClosingEventHandler(winForm_FormClosing); this.winControl.Resize += new EventHandler(winControl_Resize); } } ////// 如果使用Winform窗体,则将显示重新定向到一个窗体控件中 /// /// protected override void Draw(GameTime gameTime) { base.Draw(gameTime); // 绘制光标 spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState); if (scm.isShowMouseCursor) spriteBatch.Draw(cursor, new Rectangle(Input.MousePos.X, Input.MousePos.Y, 32, 32), Color.White); spriteBatch.End(); if (winControl != null) GraphicsDevice.Present(winControl.Handle); } ////// 处理主窗体的Closing事件 /// void winForm_FormClosing(object sender, FormClosingEventArgs e) { // 如果主窗体关闭则退出程序 this.Exit(); } ////// 根据WinForm绘制控件的大小重新设置图形设备的大小。 /// /// /// void winControl_Resize(object sender, EventArgs e) { Control c = (Control)sender; if (c != null) { Graphics.PreferredBackBufferHeight = c.ClientSize.Height > 0 ? c.ClientSize.Height : 2; Graphics.PreferredBackBufferWidth = c.ClientSize.Width > 0 ? c.ClientSize.Width : 2; Graphics.ApplyChanges(); } } ////// 在Windows.Forms环境中不需要XNA游戏窗口,所以将它隐藏 /// /// /// private void gameWindowForm_Shown(object sender, EventArgs e) { this.gameWindowForm.Hide(); winControl_Resize(winControl, null); } #endif } }
还需要在引擎类的Initialize方法中添加以下代码(注意Initialize方法仍位于StunEngine.cs文件中而不是在StunEngineWinForm.cs文件中):
#if !XBOX //----------------------------------------- // 用于Windows.Forms支持的代码,添加gameWindowForm调用Shown方法时的处理事件 //----------------------------------------- if (winForm != null) { gameWindowForm = System.Windows.Forms.Form)System.Windows.Forms.Form.FromHandle(this.Window.Handle); gameWindowForm.Shown += new EventHandler(gameWindowForm_Shown); } //----------------------------------------- // 用于Windows.Forms支持的代码:显示主窗体 //----------------------------------------- if (winForm != null) winForm.Show(); #endif
3.在引擎类中做好了准备工作之后,就可以回到SceneEditor项目了。由于在引擎中我们并不知道SceneEditor项目中的引擎类究竟叫什么名称,而在编辑器项目中需要有对这个引擎类的引用,所以还需要创建一个接口实现这个类的引用,我命名为IEditorHost,代码如下:
namespace SceneEditor { public interface IEditorHost : StunEngine.IWinForm { ////// 指向编辑器主类的引用 /// SceneEditor Editor { set; } } }
这个接口非常简单,它派生自引擎的IwinForm接口,在它的基础上添加了对SceneEditor类的引用,因为此时我已经知道我的编辑器的引擎类叫做SceneEditor。
4.然后编写SceneEditor类的代码:
namespace SceneEditor { ////// Game对象 /// public class SceneEditor : StunXnaGE { #region 构造函数和私用成员 // 编辑器使用的Scene类 public Scene viewScene; ////// 以一个IEditorHost接口为参数的构造函数。 /// /// public SceneEditor(IEditorHost winForm) : base(winForm) { winForm.Editor = this; } #endregion #region 重写的方法 ////// 初始化 /// protected override void Initialize() { base.Initialize(); // 将fps设置为30帧每秒,在WinForm中设置更高的fps没什么意义 TargetElapsedTime = new TimeSpan(0, 0, 0, 0, 33); } ////// 设置状态栏的文字 /// /// protected override void Draw(GameTime gameTime) { base.Draw(gameTime); StatusStrip sst = ((MainForm)winForm).statusStrip1; sst.Items[0].Text = "相机位置:" + viewScene.Camera.Pose.Position.ToString(); sst.Items[1].Text = "场景中的节点数量:" + viewScene.NodesCount.ToString(); } #endregion ////// 创建一个默认场景,因为引擎需要至少有一个场景。 /// public void CreateScene() { // 创建默认场景 viewScene = new Scene(this, "DemoScene"); // 将场景添加到场景管理器 SceneManager.AddScene(viewScene); // 创建场景中的相机 CameraSceneNode cam = new CameraSceneNode(this, viewScene, new Vector3(0, 15, 15), Vector3.Zero ); cam.Initialize(); //----------------------------------- // 创建相机控制器 //----------------------------------- DemoCameraController fpsCamCtrl = new DemoCameraController(this); fpsCamCtrl.Elevation = 90; viewScene.Camera.AttachController(fpsCamCtrl); // 在场景中添加一个单向光源 Light sunLight = new Light(this, new Vector3(0.8f, 0.8f, 0.8f),new Vector3(0, -5, -10)); viewScene.AddLight(sunLight); // 手动在场景中添加一个球,一个正方体,一个模型坦克,以后下面的代码会通过菜单实现 Vector3 position = new Vector3(5, 0, 0); SphereSceneNode sphere = new SphereSceneNode(this, viewScene, "Grass"); viewScene.AddNode(sphere); sphere.Name = "球"; sphere.pose.SetPosition(ref position); CubeSceneNode cube = new CubeSceneNode(this, viewScene, "Grass"); viewScene.AddNode(cube); position = new Vector3(-5, 0, 0); cube.Pose.SetPosition(ref position); cube.Name = "方块"; ModelSceneNode tank = new ModelSceneNode(this, viewScene, "SimpleTank"); viewScene.AddNode(tank); tank.Name = "坦克"; Vector3 scale= new Vector3(0.05f, 0.05f, 0.05f); tank.pose.SetScale(ref scale); } } }
这个类非常简单,它从引擎类继承,在构造函数中添加了winform窗体中对本身的引用。要在StunEngien中显示对象首先必须要有一个Scene,然后在Scnen中手动添加一些模型,当然,添加模型的操作会在编辑器以后的版本中改进为通过WinFom界面添加。
5.最后在MainForm.cs类中添加一些代码就完成了全部工作:
namespace SceneEditor { public partial class MainForm : Form,IEditorHost { // 编辑器主类,从引擎类继承 private SceneEditor editor; // 当前使用的场景 private Scene scene; private bool isInitialized = false; public MainForm() { InitializeComponent(); } #region 实现IWinForm接口和IEditorHost接口的成员 public Form Form { get { return this; } } public Control winControl { get { return panel1; } } public SceneEditor Editor { set { editor = value; } } #endregion private void MainForm_Activated(object sender, EventArgs e) { Application.DoEvents(); if (!isInitialized) { isInitialized = true; editor.CreateScene(); this.scene = editor.viewScene; panel1.Select(); } } } }
也非常简单,即在Form窗口激活后调用SceneEidtor类的createScene方法创建一个默认场景。 差点忘了,程序主入口Program.cs代码需要做以下修改:
namespace SceneEditor { static class Program { ////// The main entry point for the application. /// static void Main(string[] args) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); IEditorHost hostForm = new MainForm(); SceneEditor editor = new SceneEditor(hostForm); editor.Run(); } } }
好了,现在运行程序截图如下:
至此,已经成功地将StunEngine引擎实现在WinForm窗体中了,下一步要实现拾取——即通过检测光标的位置获取点击了哪一个对象,并将此对象的属性显示在右下的属性窗口中。
XNA研究工作暂停,2011年1月重启。
文件下载(已下载 1740 次)发布时间:2010/9/6 上午11:57:25 阅读次数:15918