【问题标题】:Monogame - Create CheckBox objectMonogame - 创建 CheckBox 对象
【发布时间】:2016-03-04 00:11:44
【问题描述】:

我一直在为我的游戏制作 GUI。到目前为止,我已经使用代码创建了看起来像“表单”的对象(不要与 winforms 混淆,我使用的是 monogame)。我还创建了按钮。但现在我很难创建一个复选框。所以这是我的CheckBoxGUI 课程:

// take a look into the recent edits for this post if you need to see my old code.

我的TextOutliner 类用于绘制带边框/轮廓的文本:

// take a look into the recent edits for this post if you need to see my old code.

最后,这是我在TitleScreen 中使用CheckBoxGUI 类的方法:

// take a look into the recent edits for this post if you need to see my old code.

这是我的问题(至少我认为是这样,在处理复选框逻辑的方法中):

// take a look into the recent edits for this post if you need to see my old code.

最后,Draw():

// take a look into the recent edits for this post if you need to see my old code.

所以CheckBoxInputTS 方法允许我通过单击鼠标来选中/取消选中 CheckBoxGUI 项目,但我实际上无法为其附加任何功能。例如,我可以在同一方法中执行以下操作:

// take a look into the recent edits for this post if you need to see my old code.

也许我遗漏了一些简单的东西,或者我不明白如何实际对代码进行进一步改进,我已经必须使其支持checkBoxes 的不同功能......但是,当我测试这个时,如果我打开窗户并选中该框,窗户就会关闭并且不会再次打开。 WindowGUI 类的 IsOpen 成员通过类的 Draw() 方法中的 if 语句控制窗口是否可见。

如果我尝试对复选框使用其他示例,也会发生同样的事情...一旦checkBox.IsChecked 具有用户给定的值,我就无法更改它。

如果有人能发现我做错了什么并帮助我清理这段代码,我们将不胜感激。提前致谢!

编辑:这是我目前所拥有的,基于建议的答案:

public class CheckBoxGUI : GUIElement
{
    Texture2D checkBoxTexEmpty, checkBoxTexSelected;
    public Rectangle CheckBoxTxtRect { get; set; }
    public Rectangle CheckBoxMiddleRect { get; set; }

    public bool IsChecked { get; set; }
    public event Action<CheckBoxGUI> CheckedChanged;

    public CheckBoxGUI(Rectangle rectangle, string text, bool isDisabled, ContentManager content)
    : base(rectangle, text, isDisabled, content)
    {
        CheckBoxTxtRect = new Rectangle((Bounds.X + 19), (Bounds.Y - 3), ((int)FontName.MeasureString(Text).X), ((int)FontName.MeasureString(Text).Y));
        CheckBoxMiddleRect = new Rectangle((Bounds.X + 16), Bounds.Y, 4, 16);
        if (checkBoxTexEmpty == null) checkBoxTexEmpty = content.Load<Texture2D>(Game1.CheckBoxPath + @"0") as Texture2D;
        if (checkBoxTexSelected == null) checkBoxTexSelected = content.Load<Texture2D>(Game1.CheckBoxPath + @"1") as Texture2D;
    }

    public void OnCheckedChanged()
    {
        var h = CheckedChanged;
        if (h != null)
            h(this);
    }

    public override void UnloadContent()
    {
        base.UnloadContent();
        if (checkBoxTexEmpty != null) checkBoxTexEmpty = null;
        if (checkBoxTexSelected != null) checkBoxTexSelected = null;
    }

    public override void Update(GameTime gameTime)
    {
        base.Update(gameTime);
        if (!Game1.IsSoundDisabled)
        {
            if ((IsHovered) && (!IsDisabled))
            {
                if (InputManager.IsLeftClicked())
                {
                    ClickSFX.Play();
                    IsHovered = false;
                }
            }
        }
        if (IsClicked) OnCheckedChanged();
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        if ((FontName != null) && ((Text != string.Empty) && (Text != null)))
        {
            if (IsChecked) spriteBatch.Draw(checkBoxTexSelected, Bounds, Color.White);
            else if (IsDisabled) spriteBatch.Draw(checkBoxTexEmpty, Bounds, Color.Gray);
            else spriteBatch.Draw(checkBoxTexEmpty, Bounds, Color.Gray);
            TextOutliner.DrawBorderedText(spriteBatch, FontName, Text, CheckBoxTxtRect.X, CheckBoxTxtRect.Y, ForeColor);
        }
    }
}

然后是GUIElement 类:

public abstract class GUIElement
{
    protected SpriteFont FontName { get; set; }
    protected string Text { get; set; }
    protected SoundEffect ClickSFX { get; set; }
    public Rectangle Bounds { get; set; }
    public Color ForeColor { get; set; }
    public Color BackColor { get; set; }
    public bool IsDisabled { get; set; }
    public bool IsHovered { get; set; }
    public bool IsClicked { get; set; }

    public GUIElement(Rectangle newBounds, string newText, bool isDisabled, ContentManager content)
    {
        Bounds = newBounds;
        Text = newText;
        IsDisabled = isDisabled;
        FontName = Game1.GameFontSmall;
        ForeColor = Color.White;
        BackColor = Color.White;
        ClickSFX = content.Load<SoundEffect>(Game1.BGSoundPath + @"1") as SoundEffect;
    }

    public virtual void UnloadContent()
    {
        if (Bounds != Rectangle.Empty) Bounds = Rectangle.Empty;
        if (FontName != null) FontName = null;
        if (Text != string.Empty) Text = string.Empty;
        if (ClickSFX != null) ClickSFX = null;
    }

    public virtual void Update(GameTime gameTime)
    {
        if (!IsDisabled)
        {
            if (Bounds.Contains(InputManager.MouseRect))
            {
                if (InputManager.IsLeftClicked()) IsClicked = true;
                ForeColor = Color.Yellow;
                IsHovered = true;
            }
            else if (!Bounds.Contains(InputManager.MouseRect))
            {
                IsHovered = false;
                ForeColor = Color.White;
            }
        }
        else ForeColor = Color.Gray;
    }
}

这是我的用法:

// Fields
readonly List<GUIElement> elements = new List<GUIElement>();
CheckBoxGUI chk;
bool check = true;
string text;

// LoadContent()
chk = new CheckBoxGUI(new Rectangle(800, 200, 16, 16), "Hide FPS", false, content);
chk.CheckedChanged += cb => check = cb.IsChecked;
elements.Add(chk);

// UnloadContent()
foreach (GUIElement element in elements) element.UnloadContent();

// Update()
for (int i = 0; i < elements.Count; i++) elements[i].Update(gameTime);
foreach (CheckBoxGUI chk in elements) chk.Update(gameTime);

// Draw()
if (chk.IsChecked) check = true;
else check = false;
if (check) text = "True";
else text = "False";
spriteBatch.DrawString(Game1.GameFontLarge, text, new Vector2(800, 400), Color.White);
if (optionsWindow.IsOpen) chk.Draw(spriteBatch);

【问题讨论】:

  • 公开一个事件并挂钩它?
  • 抱歉,我从来没有接触过高级的面向事件的编程。我已经将 win 表单用于简单的事情,但我从来没有实际需要处理事件,(当然,直到现在)除了 winforms 的预定义的,所以我从来没有学过它们......
  • 然后在谷歌上搜索一下事件如何运作的教程,事件将成为你的下一个最好的朋友,它会改变你看待事物的方式;)
  • 另外,如果您使用表单,所有这些都是事件,单击、鼠标移动等,也许您知道它们但不知道它们是事件。
  • 我确信事件将有助于清理我代码中的所有 bool 混乱......但首先我必须学习它们。 @Gusman 我知道它们是事件,我只是从未涉足高级的东西,例如创建自己的事件!我刚开始制作自己的 GUI 对象。

标签: c# checkbox monogame


【解决方案1】:

作为@craftworkgames 建议的替代方案,您还可以通过复选框构造函数传递一个委托,并直接调用它。这与事件之间的差异是微不足道的。一个事件基本上是一个委托列表(即指向函数的托管指针),并且“触发事件”只是连续调用您附加的所有处理程序(使用+=)。使用事件将提供一种更统一的方式(至少对 .NET 开发人员来说更舒服),但我也会在我的回答中提到其他一些方面。

这是一般的想法:

public class CheckBoxGUI : GUIElement
{
    public bool IsChecked { get; set; }

    // instead of using the event, you can use a single delegate
    readonly Action<CheckBoxGUI> OnCheckedChanged;

    public CheckBoxGUI(Action<CheckBoxGUI> onCheckedChanged, Rectangle rectangle)
        : base(rectangle) // we need to pass the rectangle to the base class
    {
        OnCheckedChanged = onCheckedChanged;
    }

    public override bool Update(GameTime gameTime, InputState inputState)
    {
        if (WasClicked(inputState)) // imaginary method
        {
            // if we are here, it means we need to handle the inputState
            IsChecked = !IsChecked;

            // this method will be invoked 
            OnCheckedChanged(this);

            return true;
        } 
        else 
        {
            return false;
        }
    }

    ... drawing methods, load/unload content
}

当您实例化复选框时,您可以简单地传递一个匿名方法,该方法将在状态更改时被调用:

var checkbox = new CheckBoxGUI(
    // when state is changed, `win.IsOpen` will be set to a new value
    cb => win.IsOpen = cb.IsChecked, 
    new Rectangle(0, 0, 100, 100)
);

(顺便说一句,这种方法和事件之间的差异可以忽略不计:)

// if you had a 'CheckedChanged' event, instantiation would look something like this
var checkbox = new CheckBoxGUI(...);
checkbox.CheckedChanged += cb => win.IsOpen = cb.IsChecked;

创建具有共享功能的基 gui 类

我还会将大部分功能提取到基类中。这与Control 类作为 WinForms 中的基类存在的原因相同,除非您使用基类,否则您将一遍又一遍地重复这一点。

如果没有别的,所有元素都有一个边界矩形,并且检查鼠标点击的方式相同:

public class GUIElement
{
    public Rectangle Bounds { get; set; }

    public GUIElement(Rectangle rect)
    {
        Bounds = rect;
    }

    public virtual bool Update(GameTime game, InputState inputState)
    {
        // if another element already handled this click,
        // no need to bother
        if (inputState.Handled)
            return false;           

        // you should actually check if mouse was both clicked and released 
        // within these bounds, but this is just a demo:
        if (!inputState.Pressed)
            return false;

        // within bounds?
        if (!this.Bounds.Contains(inputState.Position))
            return false;

        // mark as handled: we don't want this event to 
        // propagate further
        inputState.Handled = true;
        return true;
    }

    public virtual void Draw(GameTime gameTime, SpriteBatch sb) { }
}

当您决定希望在鼠标向上事件时触发鼠标点击(就像通常所做的那样),您的代码中只有一个地方应该发生这种变化。

顺便说一句。此类还应实现所有 UI 元素之间共享的其他属性。您不需要通过构造函数指定它们的值,但必须指定默认值。这类似于 WinForms 所做的:

public class GUIElement
{
    public Rectangle Bounds { get; set; }
    public SpriteFont Font { get; set; }
    public Color ForeColor { get; set; }
    public Color BackColor { get; set; }

    // make sure you specify all defaults inside the constructor 
    // (except for Bounds, of course)
}

为了抽象鼠标/触摸/游戏板输入,我在上面使用了一个相当简单的InputState(您可能希望稍后对其进行扩展):

public class InputState
{
    // true if this event has already been handled
    public bool Handled;

    // true if mouse is being held down 
    public bool Pressed;

    // mouse position
    public Point Position;
}

从基类继承

有了这个,你的CheckBoxGUI现在继承了GUIElement的好处:

// this is the class from above
public class CheckBoxGUI : GUIElement
{
    public bool IsChecked { get; set; }

    readonly Action<CheckBoxGUI> OnCheckedChanged;

    public CheckBoxGUI(Action<CheckBoxGUI> onCheckedChanged, Rectangle rectangle)
        : base(rectangle) // we need to pass the rectangle to the base class
    {
        OnCheckedChanged = onCheckedChanged;
    }

    // Note that this method returns bool, unlike 'void Update'.
    // Also, intersections should be handled here, not outside.
    public override bool Update(GameTime gameTime, InputState inputState)
    {
        var handled = base.Update(gameTime, inputState);
        if (!handled)
            return false;

        // if we are here, it means we need to handle the inputState
        IsChecked = !IsChecked;
        OnCheckedChanged(this);
        return true;
    }

    ... drawing methods, load/unload content
}

您的游戏/场景类

在您的主游戏(或场景)更新方法开始时,您只需创建InputState 实例并为所有元素调用HandleInput

public void Update(GameTime gameTime)
{
    var mouse = Mouse.GetState();
    var inputState = new InputState()
    {
        Pressed = mouse.LeftButton == ButtonState.Pressed,
        Position = mouse.Position
    };

    foreach (var element in this.Elements)
    {
        element.Update(gameTime, inputState);
    }
}

基于事件的替代方案

使用基于事件的方法,您可以将复选框更改为:

// this is the class from above
public class CheckBoxGUI : GUIElement
{
    public bool IsChecked { get; set; }
    public event Action<CheckBoxGUI> CheckedChanged;

    protected virtual void OnCheckedChanged()
    {
        var h = CheckedChanged;
        if (h != null)
            h(this);
    }

    public CheckBoxGUI(Rectangle rectangle)
        : base(rectangle) // we need to pass the rectangle to the base class
    { }

    // the rest of the class remains the same
}

并实例化它:

var cb = new CheckBoxGUI(new Rectangle(0, 0, 100, 100));
cb.CheckedChanged += cb => win.IsOpen = cb.IsChecked;

更新

这就是您的 Update 调用堆栈的外观:

  • Game.Update

    • 致电TitleScreen.Update
  • TitleScreen.Update

    • gui 元素(不仅仅是复选框)调用 element.Update
  • GuiElement.Update

    • 检查该元素是否被点击(被派生类重用)
  • CheckBoxGUI.Update

    • 调用 GuiElement.Update (base.Update) 检查是否刚刚被点击
    • 如果单击,则更改其状态、视觉属性并触发事件

如果您的操作正确,您的屏幕类中应该有一个gui 元素 列表,而不是实际实例。屏幕不应该知道或关心正在绘制的元素的确切类型:

readonly List<GuiElement> elements = new LIst<GuiElement>();
var chk = new CheckBoxGUI(new Rectangle(800, 200, 16, 16), "Window", false, content);
chk.CheckedChanged += cb => win.IsOpen = cb.IsChecked;

// from now on, your checkbox is just a "gui element" which knows how to
// update itself and draw itself
elements.Add(chk);

// your screen update method
foreach (var e in elements) 
    e.Update(gameTime);

// your screen draw method
foreach (var e in elements) 
    e.Draw(gameTime);

【讨论】:

  • Game/Scene 类是指Game1.cs
  • @JohnyP.:是的,很可能。我写了Scene,因为人们经常有某种“场景管理器”来将屏幕组织成不同的类。
  • 哦,我也有这个,我叫它ScreenManager :) 你很亲密。另外,InputState。我有自己的InputManager 类,用于检查鼠标点击。到目前为止,我一直在使用此代码来处理点击:if (newMouseState.LeftButton == ButtonState.Pressed &amp;&amp; oldMouseState.LeftButton == ButtonState.Released),然后制作oldMouseState = newMouseState。尽管如此,它不会在鼠标向上时检测到点击,而是在按下左键时...
  • @JohnyP.:应该反过来,当前状态应该是Released,之前的状态应该是Pressed,以检测鼠标向上(按下 -> 释放)。完整的方法实际上是检查从释放更改为按下时是否在范围内,保存此标志,然后如果您检测到从按下到释放的更改并且仍在范围内,则触发单击事件。重点只是将此信息抽象为一个简单的类,以便您可以轻松切换到TouchPanel 或其他形式的输入。
  • 不,我现在只打算从事 PC 项目。 CheckBoxGUI 类中的 bool Update(GameTime gameTime, InputState inputState) 也没有返回值......我假设我必须 return true; 对吗?另外,帽子是OnCheckedChanged = setter; 部分吗?编辑:哦,我不敢相信我把它们颠倒了!现在它就像一个魅力!
【解决方案2】:

听起来您正在努力解决的主要问题是弄清楚如何实现事件。所以我会用一个非常简单的例子来复习一下。

假设我们想要实现一个简单的Click 事件。首先在你的类上创建一个事件成员,如下所示:

public event EventHandler Click;

然后在您的 Update 方法中,您需要以与播放声音效果相同的方式引发该事件。

public void Update(GameTime gameTime)
{
    if (!Game1.IsSoundDisabled)
    {
        if ((IsHovered) && (!IsDisabled))
            if (InputManager.Instance.IsLeftClicked())
            {
                clickSFX.Play();
                IsHovered = false;

                if(Click != null)
                    Click(this, EventArgs.Empty);
            }
    }
}

这就是它的全部内容。您现在可以在代码的其他部分注册该活动。例如:

checkBox.Click += CheckBox_Click;

并像在 WinForms 中一样实现事件。

private void CheckBox_Click(object sender, EventArgs args)
{
    // do something when the check box is clicked.
}

您无需了解太多关于活动的其他信息即可开始。我唯一想说的是,如果线程安全是一个问题,你应该养成这样提出你的事件的习惯:

var eventHandler = Click;

if(eventHandler != null)
    eventHandler(this, EventArgs.Empty);

但那是另一个故事了。

您的代码还有很多其他可以改进的地方,但我认为这超出了这个问题的范围。如果您想获得有关您的代码的更多反馈,我建议您将其发布到code review

【讨论】:

  • 虽然这个答案确实更容易实现,但它基于我有缺陷的 (WET) GUI 代码结构,因此它会变得越来越难以维护......我将不得不重新做很多事情我所有的GUIElements,目的是添加一个abstract class,以保持它们的共同属性,让我的生活更轻松。
  • 是的,您绝对可以使用一些代码重构,但我无法在一个答案中解决所有问题。编写 GUI 很难。
  • 我绝对同意编写 GUI 很难,因此提出了这个问题。我也会尝试自己弄清楚一些事情。
  • 嘿@craftworkgames!我也试过你的解决方案。虽然它有效,但每次点击时它都没有注册点击。任何想法为什么?
  • 您的更新方法中的逻辑很可能有问题,或者与其中使用的变量有关。首先,我可以看到如果声音被禁用,点击不会触发。不过,这可能不是这个错误。使用断点查看这 2 个嵌套的 if 语句发生了什么。
猜你喜欢
  • 2012-05-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多