【问题标题】:Wait until a click event has been fired C#等到点击事件被触发 C#
【发布时间】:2016-02-19 20:48:53
【问题描述】:

我正在开发一个纸牌游戏,但我需要一个功能来停止程序,直到玩家没有点击他的卡片的图片框来丢弃它。 我的游戏算法是这样的:

int nextDrawer = 0; // the players which will discard a card are determinated in counterclockwise starting from the human player
for (int i = 0; i < players; i++) // untill all the players hasn't drawed a card
{
    if (i == 0) .... // the human player has to click on a picture box to discard a card
    else .... // an AI player will discard a card which is selected randomly from the 3 cards which AI has got in its hand
}

问题在于,当手牌结束时,最先弃牌的人可能会改变。如果玩家用 0(人类玩家)、1(第一个 AI 玩家)、2(第二个 AI 玩家)和 3(第三个 AI 玩家)来计算,那么在第一轮最先出牌的是人类玩家,但在第二个最先丢弃的可能是 2 个 AI 玩家,而人类玩家必须等到他之前的所有 AI 玩家丢弃一张牌(在这种情况下,该轮将是 2-3-0-1)。

如果AI玩家还没有弃牌,如何取消点击事件?

更新

我并不总是需要等待 所有 AI 玩家都抽到一张牌:如果 mance 的获胜者是数字 2,则该回合将是 2-3-0-1:这意味着玩家必须等待 AI 玩家 2 和 3 抽牌,然后玩家必须单击一个 PictureBox,循环将返回给 AI 玩家,然后允许 AI 玩家 1 弃牌。

更新 2

我是这么想的:

int leader = 0; // who is going to discard first
int nextDiscarder = leader; // next player who's going to discard
for (int i = 0; i < nPlayers; i++) // until all the players hasn't discarded
{
    if (nextDiscarder == 0) // the human has to discard
    {
        enablePictureBoxClickEvent;
        // now before the loop continue the program has to wait the event click on a picture box
    }
    else
    {
        AI[nextDiscarder].discard(); // the ai player will discard
    }
    if (nextDiscarder == players - 1) // if nextDiscarder has reached the end of the table
        nextDiscarder = 0; // return to the begin until all player has discarded a card
    else
        ++nextDiscarder; // continue to discard with the next player
}

在我的点击事件中,我会做这样的事情:

private myEventClick(object sender, EventArgs e)
{
    .... // do the instructions needed to discard a card
    disableMyEventClick;
    returnToLoop;
}

但主要问题是我不知道如何在代码中编写我的指令returnToLoop

【问题讨论】:

  • 您是否尝试设置bool 标志来帮助您?例如:当人类玩过游戏时将标志设置为 false,然后仅在另外 3 个弃牌时将其设置回 true(可能使用计数器)。然后确保人类只能在标志为真时丢弃他们的牌。我希望这会有所帮助!
  • 我认为“returnToLoop”是不需要的,因为事件并没有终止循环,所以返回循环是自动的,因为事件并没有把你带出线程。
  • 我认为,当您在代码中的“ // 现在在循环继续之前程序必须等待”点处阻塞循环线程时,您可能会不高兴。您的 GUI 将冻结!我认为使用状态机而不是单线程循环会更快乐。
  • 使用事件驱动的方法而不是循环可能更优雅。

标签: c#


【解决方案1】:

我知道大多数人会争辩说你应该使用事件驱动的方法,但是async/await 功能可以用来轻松实现这样的事情,而无需手动实现状态机。

我已经在Force loop to wait for an eventA Better Way to Implement a WaitForMouseUp() Function? 中发布了类似的方法,所以基本上这是与前者相同的助手,将Button 替换为Control

public static class Utils
{
    public static Task WhenClicked(this Control target)
    {
        var tcs = new TaskCompletionSource<object>();
        EventHandler onClick = null;
        onClick = (sender, e) =>
        {
            target.Click -= onClick;
            tcs.TrySetResult(null);
        };
        target.Click += onClick;
        return tcs.Task;
    }
}

现在您只需将您的方法标记为async 并使用await

// ...
if (nextDiscarder == 0) // the human has to discard
{
    // now before the loop continue the program has to wait the event click on a picture box
    await pictureBox.WhenClicked();
    // you get here after the picture box has been clicked
}
// ...

【讨论】:

【解决方案2】:

我喜欢 Ivan 解决方案,因为它看起来不错,并且可以在您需要等待控件的其他任何地方轻松重复使用。

但是,我想提供另一种解决方案,因为我觉得这样做的方式要复杂得多。

让我们重新开始吧:

  • 在游戏的某个时刻,您需要玩家选择一张他们不想扔掉的牌
  • 有一个人类玩家,在您的数组中是第 0 号
  • 人类玩家并不总是第一个决定扔掉哪张牌。
  • 要决定扔掉哪张卡片,您会向玩家显示一个图片框,然后等待他点击它。

我相信一个简单的解决方案可能是:

  1. 您首先在人类之前为 AI 玩家移除卡片(如果人类先出牌,这将无济于事,如果人类最后出牌,所有 AI 都会在这里出牌)
  2. 您启用 PictureBox 并结束您的功能
  3. 在PictureBox的点击事件中,你移除了用户卡,然后你移除了在人类之后的剩余AI玩家的卡(如果人类是第一个,那么所有AI都会在这里移除一个卡,如果人类是最后一个,你什么都不做)

完成...

所以这看起来像这样:

//We need an instance variable, to keep track of the first player
int _firstPlayerToDiscard = 0;

private void StartDiscardingProcess(int FirstToDiscard)
{
    _firstPlayerToDiscard = FirstToDiscard;
    if (FirstToDiscard != 0) //If the player is the first, we do nothing
    {
        //We discard for every other AI player after the human player
        for (int i = FirstToDiscard; i < nPlayers; i++)
        {
            AI[i].Discard(); 
        }
    }
    //Now we fill the PictureBox with the cards and we display it to the player
    DiscardPictureBox.Enabled = true;
    //or DiscardPictureBox.Visible = true;
    //and we are done here, we know basically wait for the player to click on the PictureBox.
}

private void pictureBox_click(Object sender, EventArgs e)
{
    //Now we remove the card selected by the player
    // ...
    //And we remove the cards from the other AI players
    //Note that if the player was first to discard, we need to change the instance variable
    if (_firstPlayerToDiscard == 0) { _firstPlayerToDiscard = nbPlayers; }
    for (int i = 1; i < _firstPlayerToDiscard; i++)
    {
        AI[i].Discard();
    }
}

你已经完成了...

注意:抱歉,如果语法不好或不寻常,我通常在 VB .Net 中编写代码...随意编辑语法问题...

【讨论】:

    【解决方案3】:

    以下代码演示了一个简单的基于计时器的状态机。在这种情况下,机器的状态是当前玩家的回合。此示例通过将状态设置为下一个玩家,让每个 Play 决定何时让下一个玩家轮到她。为程序应检查的其他内容添加其他状态。这种程序架构运行相对平稳,因为程序线程没有被阻塞在紧密的循环中。每个玩家完成并退出回合的“速度”越快越好 - 即使玩家的回合重复 10000 次,在让下一个玩家玩之前没有做任何事情。

    在下面的示例中,Click 事件处理程序将机器状态从轮到人类推进到轮到 AI。这有效地暂停了游戏,直到人类点击。由于转弯没有在紧密循环中被阻塞,因此您可以设置其他按钮供人类点击,例如“通过”、“重新开始”和“退出”。

    using System;
    using System.Windows.Forms;
    using System.Timers;
    
    namespace WindowsFormsApplication1
    {
      public partial class Form1 : Form
      {
        private System.Timers.Timer machineTimer = new System.Timers.Timer();
    
        // These are our Machine States
        private const int BEGIN_PLAY = 0;
        private const int HUMAN_PLAYER_TURN = 1;
        private const int AI_PLAYER_TURN = 2;
    
        // This is the Current Machine State
        private int currentPlayer = BEGIN_PLAY;
    
        // Flag that lets us know that the Click Event Handler is Enabled
        private bool waitForClick = false;
    
        // The AI members, for example 100 of them
        private const int AIcount = 100;
        private object[] AIplayer = new object[AIcount];
        private int AIcurrentIndex = 0;    // values will be 0 to 99
    
    
        public Form1()
        {
            InitializeComponent();
            this.Show();
    
            // The Timer Interval sets the pace of the state machine. 
            // For example if you have a lot of AIs, then make it shorter
            //   100 milliseconds * 100 AIs will take a minimum of 10 seconds of stepping time to process the AIs
            machineTimer.Interval = 100;  
            machineTimer.Elapsed += MachineTimer_Elapsed;
    
            MessageBox.Show("Start the Game!");
            machineTimer.Start();
        }
    
    
        private void MachineTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            // Stop the Timer
            machineTimer.Stop();
            try
            {
                // Execute the State Machine
                State_Machine();
    
                // If no problems, then Restart the Timer
                machineTimer.Start();
            }
            catch (Exception stateMachineException)
            {
                // There was an Error in the State Machine, display the message
                // The Timer is Stopped, so the game will not continue
                if (currentPlayer == HUMAN_PLAYER_TURN)
                {
                    MessageBox.Show("Player Error: " + stateMachineException.Message, "HUMAN ERROR!",
                                    MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                else if (currentPlayer == AI_PLAYER_TURN)
                {
                    MessageBox.Show("Player Error: " + stateMachineException.Message, "AI ERROR!",
                                    MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                else
                {
                    MessageBox.Show("Machine Error: " + stateMachineException.Message, "Machine ERROR!",
                                    MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }
    
    
    
        private void State_Machine()
        {
            // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
            switch (currentPlayer)
            {
                case HUMAN_PLAYER_TURN:
                    Play_Human();
                    break;
    
                case AI_PLAYER_TURN:
                    Play_AI();
                    break;
    
                default:
                    Play_Begin();
                    break;
            }
        }
    
    
        private void Play_Human()
        {
            // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
            // My Turn!
            if (!waitForClick)
            {
                // Please Wait until I take a card...
                // I am using this.Invoke here because I am not in the same thread as the main form GUI
                // If we do not wrap the code that accesses the GUI, we may get threading errors.
                this.Invoke((MethodInvoker)delegate
                {
                    pictureBox1.Click += PictureBox1_Click;
                });
    
                // set this flag so we do not re-enable the click event until we are ready next time
                waitForClick = true;
            }
        }
    
    
        private void PictureBox1_Click(object sender, EventArgs e)
        {
            // This routine is executing in the Main Form's Thread, not the Timer's Thread
    
            // Stop the game for a little bit so we can process the Human's turn
            machineTimer.Stop();
    
            // Disable the Click Event, we don't need it until next time
            pictureBox1.Click -= PictureBox1_Click;
            waitForClick = false;
    
            // To Do:  Human's Turn code...
    
            // Let the AI Play now
            currentPlayer = AI_PLAYER_TURN;
            machineTimer.Start();
        }
    
    
        private void Play_AI()
        {
            // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
            if (AIcurrentIndex < AIcount)
            {
                // If we do not wrap the code that accesses the GUI, we may get threading errors.
                this.Invoke((MethodInvoker)delegate
                {
                    // To Do:  AI Player's Turn code...
                });
    
                // Advance to the next AI
                AIcurrentIndex++;
            }
            else
            {
                // Reset to the beginning
                AIcurrentIndex = 0;
                currentPlayer = BEGIN_PLAY;
            }
        }
    
    
        private void Play_Begin()
        {
            // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
            // If we do not wrap the code that accesses the GUI, we may get threading errors.
            this.Invoke((MethodInvoker)delegate
            {
                // ... do stuff to setup the game ...
            });
    
            // Now let the Human Play on the next Timer.Elapsed event
            currentPlayer = HUMAN_PLAYER_TURN;
    
            // After the Human is done, start with the first AI index
            AIcurrentIndex = 0;
        }
    
      }
    }
    

    【讨论】:

      【解决方案4】:

      我会根据没有循环的事件以不同的方式设计流程,但是按照您的方式,您应该使用自动重置事件来通知您的循环 myEvent 已被触发。

      AutoResetEvent clickEventFired = new AutoResetEvent(false); // instanciate event with nonsignaled state
      AutoResetEvent clickEventFired = new AutoResetEvent(true); // instanciate event with signaled state
      
      clickEventFired.Reset(); // set state to nonsignaled
      clickEventFired.Set();  // set state to signaled
      clickEventFirect.WaitOne(); // wait state to be signaled
      

      https://msdn.microsoft.com/en-us/library/system.threading.autoresetevent(v=vs.110).aspx

      public static void yourLoop()
      {
          int leader = 0; // who is going to discard first
          int nextDiscarder = leader; // next player who's going to discard
      
          // instanciate auto reset event with signaled state
          AutoResetEvent clickEventFired = new AutoResetEvent(true);
      
          for (int i = 0; i < nPlayers; i++) // until all the players hasn't discarded
          {
              if (nextDiscarder == 0) // the human has to discard
              {
                  enablePictureBoxClickEvent;
                  clickEventFired.WaitOne(); // wait for event to be signaled
              }
              else
              {
                  AI[nextDiscarder].discard(); // the ai player will discard
                  clickEventFired.Reset(); // set event state to unsignaled
              }
      
              if (nextDiscarder == players - 1) // if nextDiscarder has reached the end of the table
                  nextDiscarder = 0; // return to the begin until all player has discarded a card
              else
                  ++nextDiscarder; // continue to discard with the next player
          }
      }
      
      private myEventClick(object sender, EventArgs e)
      {
          .... // do the instructions needed to discard a card
          disableMyEventClick;
          clickEventFired.Set(); // signal event
      }
      

      【讨论】:

      • 问题可能是,如果您使用 UI 线程进入 yourLoop,您将 WaitOne() 并因此阻塞 UI 线程,直到用户单击,这将导致死锁...除非WaitOne() 不会阻塞 UI 线程,但我不知道这怎么可能……
      • 我问这个是因为在docs 中声明:“阻塞当前线程,直到当前 WaitHandle 收到信号。”他们从不谈论它是完全异步的。
      • 它不是真正的异步但感觉一样,waitHandle 等待当前线程而不像 UI 线程那样锁定另一个。
      • 就是我说的,如果你在UI线程中输入yourLoop()方法,就会造成死锁……
      • 要使用 UI 线程启动 yourLoop 方法,您必须使用 UI Dispatcher,为什么除了更新 UI 之外还要使用它?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-27
      • 2013-03-10
      • 1970-01-01
      • 1970-01-01
      • 2015-04-28
      • 2011-11-19
      相关资源
      最近更新 更多