ボールの運動

WPFアプリケーションで、ちょっとした物理シミュレーションを作成する。

ボールが画面上を直線的に動き、画面の境界に達したときに跳ね返る運動をシミュレーションする。 通常WPFアプリケーションでは、XAMLとC#を組みあわせて作るが、今回はほぼすべてC#で記述し、XAML を編集するのは、Gridに "rootGrid" という名前を付けるだけとする。

XAMLコード

以下をWindowタグの中に入れる。

<Grid Name="rootGrid">
</Grid>

XAMLの編集作業はこれだけ。後はすべてC#で記述する。

C#コード

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace WpfRebound
{
    public partial class MainWindow : Window
    {
        Canvas canvas;
        Ellipse ellipse1;
        Ball ball1;
        public MainWindow()
        {
            InitializeComponent();
            SetCompnent();
            CompositionTarget.Rendering += CompositionTarget_Rendering;
        }

        private void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            UpdateBall();
            UpdateEllipseFromBall();
        }

        private void SetCompnent()
        {
            canvas = new Canvas();
            canvas.Background = Brushes.LightBlue;
            rootGrid.Children.Add(canvas);

            ball1 = new Ball();
            ball1.Position = new Vector(70, 70);
            ball1.Velocity = new Vector(1, 2);
            ball1.Size = new Size(50, 50);

            ellipse1 = new Ellipse();
            ellipse1.Stroke = Brushes.Black;
            ellipse1.Fill = Brushes.White;
            UpdateEllipseFromBall();
            canvas.Children.Add(ellipse1);
        }

        private void UpdateBall()
        {
            ball1.Move();
            double h = canvas.ActualHeight;
            double w = canvas.ActualWidth;
            if (h == 0) return; // h が定まってないときにも呼ばれてしまうため。
            double bh = ball1.Size.Height;
            double bw = ball1.Size.Width;
            if (ball1.Position.Y + bh / 2 > h)
            {
                ball1.Back();
                var v = ball1.Velocity;
                ball1.Velocity = new Vector(v.X, -v.Y);
                ball1.Move();
            }
            if (ball1.Position.Y - bh / 2 < 0)
            {
                ball1.Back();
                var v = ball1.Velocity;
                ball1.Velocity = new Vector(v.X, -v.Y);
                ball1.Move();
            }
            if (ball1.Position.X + bw / 2 > w)
            {
                ball1.Back();
                var v = ball1.Velocity;
                ball1.Velocity = new Vector(-v.X, v.Y);
                ball1.Move();
            }
            if (ball1.Position.X - bw / 2 < 0)
            {
                ball1.Back();
                var v = ball1.Velocity;
                ball1.Velocity = new Vector(-v.X, v.Y);
                ball1.Move();
            }

        }
        private void UpdateEllipseFromBall()
        {
            double bw = ball1.Size.Width;
            double bh = ball1.Size.Height;
            ellipse1.Width = bw;
            ellipse1.Height = bh;
            Canvas.SetLeft(ellipse1, ball1.Position.X - bw / 2);
            Canvas.SetTop(ellipse1, ball1.Position.Y - bh / 2);
        }
    }
    class Ball
    {
        public Vector Position;
        public Vector Velocity;
        public Size Size;
        public void Move()
        {
            Position += Velocity;
        }
        public void Back()
        {
            Position -= Velocity;
        }
    }
}

実行結果.

解説.

CanvasはEllipse(楕円)を表示させるためのもの。 Ballクラスは自作クラスで、そのインスタンスball1を動かして、その情報をellipse1に伝えることで ellipse1を動かす。ここで注意したいのは、ball1が直接表示されるわけではないということ。 画面上に表示されるのはellipse1だけである。(わざわざball1を経由してellipse1を更新する意味はもちろんある.)

CompositionTarget.Rendering はタイマーのようなもので、滑らかに動かす場合に用いる。 この中に記述した内容が、時間経過によって繰り返し呼び出されることになる。中身をみると

private void CompositionTarget_Rendering(object sender, EventArgs e)
{
    UpdateBall();
    UpdateEllipseFromBall();
}

となってるが、行っているのは、
UpdateBall()「ball1の位置・速度・サイズの更新」と、
UpdateEllipseFromBall()「ball1の情報を ellipse1 に伝える操作」となる。

ellipse1 と ball1 を分離して管理することは一見ややこしく見えるが、状況が複雑になるとこちらのほうが ずっと管理しやすい。 なぜならば、ball1は自作クラスのものなので、独自にプロパティやメソッドの追加ができ制御しやすいからである。 比較して、Ellipseは与えられたクラスであり、特に物理シミュレーション用に作成されたクラスではないから、扱いが難しい。 実際、ellipse1ではその位置が左上の座標で管理されているという点だけに注目しても、物理シミュレーションを 行う上では考えにくい。そこで、BallクラスではPositionをその(楕円の)中心座標として定義する。

SetComponent() で行われているのは、次の初期化処理である。

  • canvasを生成し、rootGridに子要素として追加.
  • ball1の初期位置・初期速度・サイズを設定し、その情報をellipse1に伝える.
  • ellipse1をcanvasの子要素として追加.

UpdateBall() で行われている処理は、次の処理である。

  • ball1の位置・速度をもとに動かす.
  • ball1がcanvasの境界上に来た時に跳ね返るように速度ベクトルを変更.
跳ね返りの処理が少し複雑である。canvas境界に達したときに ball1.Back()を呼び出す理由は 「ボールの画面はみ出し」を発生しにくくするためである。一般にcanvas境界に達するときは少しはみ出したときであるから、 ball1.Back()でもとの状況に戻して、速度ベクトルを変化させてから、ball1.Move()で動かせば、はみ出しは生じにくくなる。