写在前面

在尝试用 MonoGame 来实现游戏的时候遇到了一些问题,其中首当其冲的便是按钮控件。于是便想着用 C# 的委托和匿名方法来实现一个至少类似 VB 的按钮吧……(

实现!

控件当然不只有按钮一种,而绝大多数控件都有一些公共属性(例如可见性,坐标和尺寸等),所以可以考虑让所有控件都继承自一个 GameControl 类。

同时定义一个委托作为触发器来处理控件事件,而咱暂时没有想到控件事件有什么必要的输入参数所以这里就不设定参数好了。

最后还有一个枚举类来表示控件的状态,例如鼠标移入,点击等等。

    class GameControl
    {
        public bool Visibility { get; set; } = true;

        public string Tag { get; set; } = "";

        public Trigger Click { get; set; }

        public ControlState State { get; set; } = ControlState.None;

        public Rectangle Position
        {
            get { return new Rectangle(Location, Size); }
            set 
            { 
                Location = value.Location;
                Size = value.Size; 
            }
        }

        public Point Location { get; set; } = Point.Zero;

        public Point Size { get; set; } = Point.Zero;

        public int Width
        {
            get { return Size.X; }
            set { Size = new Point(value, Size.Y); }
        }

        public int Height
        {
            get { return Size.Y; }
            set { Size = new Point(Size.X, value); }
        }
    }
    public delegate void Trigger();

    public enum ControlState
    {
        None,
        MouseMove,
        MouseDown
    }

那么接下来让新的 GameButton 类继承它就可以很方便地集成一些属性了。同时, GameButton 也具有一些区别于其他控件的地方,例如文本框和按钮背景。

    class GameButton : GameControl
    {
        public GameLabel Content { get; set; } = new GameLabel();

        public List<Texture2D> ButtonTextures { get; set; }

        public List<System.Drawing.Color> FontColors { get; set; }
	}

这里我打算用三个 Texture2D 来储存按钮的三种状态贴图(无,鼠标移入和按下),三个字体颜色同上。

而 GameLabel 是自定义的文本框类,同样继承自 GameControl 类。这里先把这个类贴出来,以后记录文字渲染的时候再详细讲讲好了。

    class GameLabel : GameControl
    {
        public Font Font { get; set; }

        public string Content { get; set; } = "";

        public Color FontColor { get; set; } = Color.Black;

        public Point PrintedSize
        {
            get
            {
                Graphics g = Graphics.FromImage(new Bitmap(1, 1));
                SizeF realSzie = g.MeasureString(Content, Font);
                return (new Point((int)realSzie.Width, (int)realSzie.Height));
            }
        }

        public void Draw(SpriteBatch spriteBatch, GraphicsDevice graphicsDevice)
        {
            if (!Visibility) return;

            Texture2D texture2dContent;
            Brush brush = new SolidBrush(FontColor);
            
            Bitmap img = new Bitmap(PrintedSize.X, PrintedSize.Y);
            Graphics.FromImage(img).DrawString(Content, Font, brush, new PointF(0, 0));
            
            MemoryStream ms = new MemoryStream();
            img.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
            texture2dContent = Texture2D.FromStream(graphicsDevice, ms);

            spriteBatch.Draw(texture2dContent, new Vector2(Location.X, Location.Y), Microsoft.Xna.Framework.Color.White);
        }
    }

同时,按钮除了有这些固有属性外,还需要能在显示器上显示一个矩形,并且处理点击事件。现在实现 Draw 和 Update 方法。

public void Update(Point MousePosition, ButtonState MouseButtonState)
{
  if (GameMath.Contain(Position, MousePosition))
    if (MouseButtonState == ButtonState.Pressed)
      State = ControlState.MouseDown;
  	else
   {
    //Click event detect
      if (State == ControlState.MouseDown)
        Click();
      State = ControlState.MouseMove;
	}
  else
    State = ControlState.None;

  Content.Location = new Point(Position.X + (Size.X - Content.PrintedSize.X) / 2, Position.Y + (Size.Y - Content.PrintedSize.Y) / 2);
  Content.FontColor = FontColors[(int)State];
}

public void Draw(SpriteBatch spriteBatch, GraphicsDevice graphicsDevice)
{
  if (!Visibility) return;

  spriteBatch.Draw(ButtonTextures[(int)State], Position, Color.White);

  Content.Draw(spriteBatch, graphicsDevice);
}

其中, Update 方法负责根据鼠标状态来更新控件状态,同时判断点击事件。简单来讲,就是如果鼠标处于抬起状态而按钮处于按下状态,则认为触发了一次点击。

其中有一个自定义方法 GameMath.Contain ,用于判断一个坐标是否被包含在一个矩形内部。

还有什么问题?

在实际游戏中肯定不止会只涉及到一个按钮,而这样的定义使得初始化一个按钮十分繁琐(还需要挨个指定贴图,字体等等),那么咱们来写一个 Manager 类来管理游戏中的所有按钮。

这里我本来想要用泛型来实现一个通用的 Mnager 类,但是因为各种原因最后没能在写这篇文章之前完成它,所以这里先演示一下特化的 ButtonManager 类。

为了简化按钮的初始化操作, ButtonManager 内除了储存一个 List<GameButton> 之外,还需要储存按钮默认将会采用的属性,例如字体,贴图等等。

    class ButtonManager
    {
        private List<GameButton> buttons { get; set; } = new List<GameButton>();

        public List<Texture2D> DefaultTextures { get; set; }

        public List<System.Drawing.Color> DefaultColors { get; set; } = new List<System.Drawing.Color>() 
        { System.Drawing.Color.White, System.Drawing.Color.White, System.Drawing.Color.Wheat };

        public Font DefaultFont { get; set; } = new Font("微软雅黑", 80);
    }

同时,还需要同时管理所有按钮的 Update 和 Draw 事件。事件的具体逻辑已经在 GameButton 内部实现过了,所以这里只需要传入所需的参数就可以了。

public void Draw(SpriteBatch spriteBatch, GraphicsDevice graphicsDevice)
{
  foreach (GameButton button in buttons)
    button.Draw(spriteBatch, graphicsDevice);
}

public void Update(Point MousePosition, ButtonState MouseButtonState)
{
  foreach (GameButton button in buttons)
    button.Update(MousePosition, MouseButtonState);
}

到这里,如果想要向 Manager 类添加或管理按钮,还是非常繁琐并且根本没有办法利用到默认属性的优势。所以要在这个类内加入 Add 等其他管理方法。

        public void RemoveButton(string Tag)
        {
            foreach (GameButton button in buttons)
                if (button.Tag == Tag) buttons.Remove(button);
        }

        public GameButton ButtonTag(string Tag)
        {
            foreach (GameButton button in buttons)
                if (button.Tag == Tag)
                    return button;
            return null;
        }

        public void AddButton(string Content, string Tag, Rectangle Position, Trigger Click)
        {
            buttons.Add(new GameButton()
            {
                Content = new GameLabel()
                {
                    Content = Content,
                    Font = DefaultFont
                },
                Tag = Tag,
                Position = Position,
                Click = Click,
                ButtonTextures = DefaultTextures,
                FontColors = DefaultColors
            });
        }

到这里,按钮类基本算是实现完成了。

试试实现?

定义 ButtonManager ,然后在 LoadContent 加载默认属性,再在 Update 和 Draw 内分别调用 ButtonManager 的 Update 和 Draw 函数即可。

发表评论

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据