13

I have a Dog class with a method Run which is supposed to move pictures across the screen:

public bool Run()
{
    Point p = PictureBoxDog.Location;

    while(p.X < 530)
    {
        int movement = Randomizer.Next(0, 3);
        p.X += movement;
        PictureBoxDog.Location = p;
    }

    if (Location == 4) //Incomplete section.
        return true;
    else
        return false;
}

This method is called from a button click event in which 4 dog objects are created and the each object calls the Run method:

private void button1_Click(object sender, EventArgs e)
{
    Dog dog1 = new Dog(pictureDog1);
    Dog dog2 = new Dog(pictureDog2);
    Dog dog3 = new Dog(pictureDog3);
    Dog dog4 = new Dog(pictureDog4);

    dog1.Run();
    dog2.Run();
    dog3.Run();
    dog4.Run();
}

The problem is that each method executes one by one, not simultaneously. I want each method to run at the same time. If I remove the while statement, then all methods execute at the same time, but with the while loop, they execute one after another. Any suggestions on how to fix this problem are greatly appreciated. Run method without the while loop:

public bool Run() //Dog1.Run()
{
    Point p = PictureBoxDog.Location;

    int movement = Randomizer.Next(0, 30);
    //Location += movement;

    p.X += movement;
    PictureBoxDog.Location = p;

    if (Location == 4) //Incomplete code.
    return true;
    else
    return false;
}
Mohit S
  • 13,723
  • 6
  • 34
  • 69
grammer
  • 245
  • 3
  • 14
  • 2
    Add a timer and call the methods from there. Then you will not have to mess with threads, which cause problems with UI elements if you don't know what you're doing. – Sami Kuhmonen Oct 07 '15 at 04:24

3 Answers3

13

Animation and WinForms is generally not straightforward. What programmers usually do is set up a game loop. A game loop does three things - fetch user input, update new position of sprites, and then draw the sprites on screen.

using System.Threading;

public partial class Form1
{ 
   private Timer _timer;
   private Dog _dog1, _dog2, _dog3, _dog4;

   public void InitializeComponent()
   {
      SetupDogs();

      // Every quarter of a second, run the function GameLoop
      _timer = new Timer(GameLoop, null, 
        TimeSpan.FromSeconds(0.25),
        TimeSpan.FromSeconds(0.25));
   }

   private void SetupDogs()
   {
      _dog1 = new Dog(PictureBoxDog1);
      _dog2 = new Dog(PictureBoxDog2);
      _dog3 = new Dog(PictureBoxDog3);
      _dog4 = new Dog(PictureBoxDog4);

   }

   public void GameLoop(object state)
   {
       GetUserInput();
       Update();
       Draw();
   }

   public void GetUserInput()
   {
     // You don't need this now. But if you need to
     // process user input later, you can do it here.
     //
     // e.g. if Last key 
     //   pressed  was arrow-left or 
     //   arrow-right etc.
   }

   public void Update()
   {
     _dog1.Update();
     _dog2.Update();
     _dog3.Update();
     _dog4.Update();
   }

   public void Draw()
   {
      // Draw on the main UI thread
      Dispatcher.BeginInvoke(() => 
      {
         _dog1.Draw();
         _dog2.Draw();
         _dog3.Draw();
         _dog4.Draw();
      });
   }

}

Then your Dog class looks like this. It needs to Update its location everytime the timer ticks, and then draw its position:

public class Dog
{

  bool _isRunning = true;

  Point Location { get; set; }

  Point NextLocation { get; set; }

  PictureBox PictureBoxDog { get; set; }

  public Dog(PictureBox pictureBox) 
  {
     PictureBoxDog = pictureBox;

     Location = GetRandomLocation();

     NextLocation = GetRandomLocation();
  }

  private Point GetRandomLocation()
  {
     var random = new Random();
     return new Point(random.Next(800), random.Next(800));
  }

  public void Update()
  {
    // Calculates the new coordinates for a dog

    // The dog starts from a random position, and is 
    // given a new random position to run towards.

    // If the dog has arrived at the new random position, then
    // give the dog a new random position to run to again
    if (NextLocation.X == Location.X && NextLocation.Y == Location.Y)
    {
      NextLocation = GetRandomLocation();
    }

    if (_isRunning)
    {
       // Move the dog closer to its destination
       // dx and dy can be -1, 0, or 1
       var dx = Math.Sign(NextLocation.X - Location.X);
       var dy = Math.Sign(NextLocation.Y - Location.Y);

       Location = new Point(Location.X + dx, Location.Y + dy);
    }
  }

  public void Draw()
  {
     PictureBoxDog.Location = Location;
  }
}
Chui Tey
  • 5,436
  • 2
  • 35
  • 44
  • Like this approach, but I'd re-use the `Random` instance and dispatch the drawing calls using `BeginInvoke` on the form/picture box. – Gene Oct 07 '15 at 06:26
  • Thanks for your help, but this seems too advanced for me. I can't really make sense of it. I'm not very experienced with C#. – grammer Oct 07 '15 at 13:43
  • To write animation code in C#, sometimes you have to think back to front. You should first read and understand what the idea a game loop is. It is a very small but important idea. In the sample, every time a timer ticks, you'll be asked where is the dog? Next, you'll be asked to draw/position the picturebox where the dog is. – Chui Tey Oct 07 '15 at 20:33
4

Try this

Dispatcher.BeginInvoke((Action) (() =>
{
     dog1.Run();
}));
Dispatcher.BeginInvoke((Action) (() =>
{
     dog2.Run();
}));
Dispatcher.BeginInvoke((Action) (() =>
{
     dog3.Run();
}));
Dispatcher.BeginInvoke((Action) (() =>
{
     dog4.Run();
}));

and for the While Loop as well use this

Dispatcher.BeginInvoke((Action) (() =>
{
    while(p.X < 530)
    {
        int movement = Randomizer.Next(0, 3);
        p.X += movement;
        PictureBoxDog.Location = p;
    }
}));

BeginInvoke is asynchronous; therefore, control returns immediately to the calling object after it is called. BeginInvoke returns a DispatcherOperation object that can be used to interact with the delegate when the delegate is in the event queue. The DispatcherOperation object returned by BeginInvoke can be used in several ways to interact with the specified delegate

Mohit S
  • 13,723
  • 6
  • 34
  • 69
  • It says I need an object reference to use Dispatcher. What am I supposed to do? Sorry, I'm not very experienced with this stuff. – grammer Oct 07 '15 at 04:40
  • 1
    The question involved PictureBoxes, which are WinForms. You're talking about Dispatcher, which is WPF -- I doubt it would play well with the WinForms event loop. – Joe White Oct 07 '15 at 04:41
  • @JoeWhite: According to Martin Konicek [You can use Dispatcher even in a WinForms app](http://stackoverflow.com/a/1160035/3796048) – Mohit S Oct 07 '15 at 04:44
  • @grammer: If you are using WinForms than you can probably have a look on [convert wpf dispatcher to winforms](http://stackoverflow.com/a/18134874/3796048) and [use Dispatcher.BeginInvoke properly](http://stackoverflow.com/a/15926580/3796048) – Mohit S Oct 07 '15 at 04:46
  • This makes Dispatcher work: Dispatcher d = Dispatcher.CurrentDispatcher; d.BeginInvoke((Action)(() => { dog1.Run(); })); – grammer Oct 07 '15 at 06:19
  • Dispatcher works, but the methods still execute one at a time, not all at the same time. – grammer Oct 07 '15 at 06:31
  • @grammer: Try using the BeginInvoke overload that also takes a DispatcherPriority, and passing in a lower priority than the default. WPF doesn't always run the delegate passed into BeginInvoke asynchronously. If that doesn't work, consider calling BeginInvoke from another thread to force asynchronous execution. – ninjeff Oct 07 '15 at 08:23
  • This won't work. The dispatcher is just delaying the execution to run in a callback from the UI thread. It's still just one thread so it still finishes that entire while loop before moving on to the next one. – James Oct 07 '15 at 08:43
0

Why don't try this?

Task.Factory.StartNew( () => Parallel.ForEach<Dog>(Dogs, dog=> dog.run()));

To use correctly this instruction you should create a list of Dog.

 List<Dog> Dogs = new List<Dog>();
        Dogs.Add(dog1);
        Dogs.Add(dog2);
        Dogs.Add(dog3);
        Dogs.Add(dog4);
        Task.Factory.StartNew(() => Parallel.ForEach<Dog>(Dogs, dog => dog.Run()));

If you don't like list:

Task.Factory.StartNew(() => dog1.Run());
Task.Factory.StartNew(() => dog2.Run());
Task.Factory.StartNew(() => dog3.Run());
Task.Factory.StartNew(() => dog4.Run());

To interact with the UI from a different thread, you need to use a delegate and calling Control.Invoke/BeginInvoke. You can test whether or not you need to call Invoke using the InvokeRequired property. So in your case:

if (PictureBoxDog.InvokeRequired){
PictureBoxDog.Invoke((MethodInvoker)(() => PictureBoxDog.location = p));}
Donald
  • 534
  • 1
  • 10
  • 18
  • Tried it. It resulted in an another error message:Additional information: Cross-thread operation not valid: Control 'pictureDog4' accessed from a thread other than the thread it was created on – grammer Oct 07 '15 at 14:36
  • when you perform a cross-thread operation on control created in other thread must use if (control.InvokeRequired) { control.Invoke(handler); } where handler is typeof Action – Donald Oct 07 '15 at 16:07