场景编辑器系列1——将XNA绘制到WinForm中

当游戏变得越来越复杂时,编写一个编辑器简化制作过程就变得越来越重要。比方说在我的引擎StunEngine中,在场景中放置一个模型,需要使用一个Vector3设置它的位置属性,但因为没有直观的参考,位置往往不尽如人意,这时就需要在源代码中重新调整这个位置,再次生成项目,看看是否合适,这个工作往往要进行多次。如果有一个场景编辑器,那么只需像3DS MAX一样,通过光标就可以对模型进行平移、选择、缩放等操作了。然后可以将场景中的信息保存为xml文件,引擎只需调用这个xml文件就可以将编辑器中放置好的对象重现出来。

编辑器界面参考

首先看一下大名鼎鼎的星际争霸2的场景编辑的界面:

星际争霸2编辑器

可见一个编辑器通常包含菜单栏,工具栏,左侧分栏中通常显示的是各个对象的列表和它们的属性,右侧分栏是编辑器主界面。但我认为因为大多数人是右撇子,所以应该将常用的功能放置在右侧而不是左侧,这一点上Visual Studio的界面更符合用户使用习惯。

接下来是用C++写成的开源引擎Delta3D(http://www.delta3d.org/)所附的关卡编辑器:

Delta3D编辑器

这个编辑器类似于3DS Max实现了前、顶、右和透视四个视口,这个效果应该可以实现,即将XNA图像绘制到四个控件中,每个控件相机的朝向不同,我没有尝试过。下面要提到的一个WPF程序就实现了类似效果。

然后是用于XNA开发的商业引擎Torque X(http://www.torquepowered.com/products/torque-x):

Torque X编辑器

右侧分栏中包含场景浏览器、素材浏览器、对象属性三个面板。

最后是一个我认为做得最好的一个XNA编辑器Rough Edges Editor(http://www.visionsofafar.com/dablog/category/XNA.aspx?page=2)的界面,源代码RoughEdges.zip下载

Rough Edges Editor

 将XNA嵌入WinForm的方法

如果上网搜索,可以找到很多XNA编辑器的实现,总结下来大致有两种方法:

第一种是xna官网上的例子WinForms Graphics Device示例,简单地说就是自己编写一个从System.Windows.Forms.Control继承的控件,然后自己编写绘制代码,网上的Jemgine(http://jemgine.codeplex.com/)就是这种思路,不过目前源代码还想缺个文件,我无法调试成功,截图如下。

Jemgine编辑器

此外还有生成树木的开源软件XNA Procedural Ltrees(http://ltrees.codeplex.com/)也使用了同样方法实现了WinForm界面,源代码LTrees-source-2.0a.zip下载,截图如下:

XNA Procedural Ltrees

 但使用这种方法的缺点就是你无法再使用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)。

XNAInWpf

 好了,下面言归正传,虽然原理参照了上述文章,但根据我的引擎做了一些修改。

首先创建一个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  阅读次数:14877

2006 - 2023,推荐分辨率1024*768以上,推荐浏览器Chrome、Edge等现代浏览器,截止2021年12月5日的访问次数:1872万9823 站长邮箱

沪ICP备18037240号-1

沪公网安备 31011002002865号