脳内補完を現実にすることで物理的制約により準優先が決まる

想像とか机上でなんとなくできているような感じがして、それで満足したり試行錯誤したりするわけですが、現実に当てはめれてみればなんということはない「物理的な制約」が先にあって、実現不可能な選択肢が削られて話が簡単になること、というのはプログラムの話。いや、煮詰めすぎると無理が来るってことか、あるいは時間的な制約ということか。

チープな画面を

少しリッチな画面に

変えていきます。

花札の画像は、Windows ストア アプリのサンプル画像からピックアップ。

■UIのプロジェクトを分ける

チープな画面プロジェクト(Hanahuda)とは別にリッチなUI用の HanahudaUI プロジェクトを作ります。

.NET のライブラリ参照が便利なのは、元の Hanahuda アセンブリのクラスを UI プロジェクトでも簡単に参照できるところですね。簡単すぎて、先行き複雑になったとき(特に複数人数で開発になったとき)には困ることが多いのですが、ロジックと UI を強引に分離させたいときは、こういう風にプロジェクトを分けてしまいます。と云いますか、元のチープな画面はロジックテスト用に残しておいて、リッチな UI チェック用は別に作る、って方法ですね。

■花札のリソースを追加

となると、花札の画像はどちらに置くかというと、UI プロジェクトに置くわけで、このリソースの名前と花札の ID とをどうやって結び付けるのかが問題になるのです。同じプロジェクトに置けば、リソースを参照するのも楽なのですが、ここは UI を別にしておきたいし。そうなると、UI の方に花札のリソースが含まれるのは当然なわけで。そうなると、プロジェクトは HanahuhaUI から Hanhuda を参照する一方向しかできないわけで、さて、ID とリソースの関係はどうするのか?ってのが問題で悩みます。

が、現実的なところ

  • ひとつにはまとめたくない(まとめない)
  • 循環参照はできない。

という制約のなかで、以下のような対比表が書かれることになるわけです。
“A1” というのは花札の ID なので、Hanahuda プロジェクトの ID クラスとダブって書かれることになります。C 言語の場合は、インクルードファイルってことになるのですが、果たして .NET の場合はどうするべきなのか?ってところですね。業務的には、別の ID 定義用のプロジェクトをひとつ作って、enum か static を使って Card.A1 とか、ちまちまと定義することになるのですが…まあ、無駄なプロジェクトを増やすよりは、ダブって書くということで、今回はあきらめ。

class CardUI
{
	protected static Dictionary<string, Bitmap> _resNames;
	public static Bitmap GetUra()
	{
		return Properties.Resources.FC097_2;
	}
	public static Bitmap GetResName(string id) 
	{
		if (CardUI._resNames == null)
		{
			CardUI._resNames = new Dictionary<string, Bitmap>();
			CardUI._resNames[&quot;A4&quot;] = Properties.Resources.FC001;
			CardUI._resNames[&quot;A3&quot;] = Properties.Resources.FC002;
			CardUI._resNames[&quot;A2&quot;] = Properties.Resources.FC003;
			CardUI._resNames[&quot;A1&quot;] = Properties.Resources.FC004;
			CardUI._resNames[&quot;B4&quot;] = Properties.Resources.FC005;
			CardUI._resNames[&quot;B3&quot;] = Properties.Resources.FC006;
			CardUI._resNames[&quot;B2&quot;] = Properties.Resources.FC007;
			CardUI._resNames[&quot;B1&quot;] = Properties.Resources.FC008;
			CardUI._resNames[&quot;C4&quot;] = Properties.Resources.FC009;
			CardUI._resNames[&quot;C3&quot;] = Properties.Resources.FC010;
			CardUI._resNames[&quot;C2&quot;] = Properties.Resources.FC011;
			CardUI._resNames[&quot;C1&quot;] = Properties.Resources.FC012;
			CardUI._resNames[&quot;D4&quot;] = Properties.Resources.FC013;
			CardUI._resNames[&quot;D3&quot;] = Properties.Resources.FC014;
			CardUI._resNames[&quot;D2&quot;] = Properties.Resources.FC015;
			CardUI._resNames[&quot;D1&quot;] = Properties.Resources.FC016;
			CardUI._resNames[&quot;E4&quot;] = Properties.Resources.FC017;
			CardUI._resNames[&quot;E3&quot;] = Properties.Resources.FC018;
			CardUI._resNames[&quot;E2&quot;] = Properties.Resources.FC019;
			CardUI._resNames[&quot;E1&quot;] = Properties.Resources.FC020;
			CardUI._resNames[&quot;F4&quot;] = Properties.Resources.FC021;
			CardUI._resNames[&quot;F3&quot;] = Properties.Resources.FC022;
			CardUI._resNames[&quot;F2&quot;] = Properties.Resources.FC023;
			CardUI._resNames[&quot;F1&quot;] = Properties.Resources.FC024;
			CardUI._resNames[&quot;G4&quot;] = Properties.Resources.FC025;
			CardUI._resNames[&quot;G3&quot;] = Properties.Resources.FC026;
			CardUI._resNames[&quot;G2&quot;] = Properties.Resources.FC027;
			CardUI._resNames[&quot;G1&quot;] = Properties.Resources.FC028;
			CardUI._resNames[&quot;H4&quot;] = Properties.Resources.FC029;
			CardUI._resNames[&quot;H3&quot;] = Properties.Resources.FC030;
			CardUI._resNames[&quot;H2&quot;] = Properties.Resources.FC031;
			CardUI._resNames[&quot;H1&quot;] = Properties.Resources.FC032;
			CardUI._resNames[&quot;I4&quot;] = Properties.Resources.FC033;
			CardUI._resNames[&quot;I3&quot;] = Properties.Resources.FC034;
			CardUI._resNames[&quot;I2&quot;] = Properties.Resources.FC035;
			CardUI._resNames[&quot;I1&quot;] = Properties.Resources.FC036;
			CardUI._resNames[&quot;J4&quot;] = Properties.Resources.FC037;
			CardUI._resNames[&quot;J3&quot;] = Properties.Resources.FC038;
			CardUI._resNames[&quot;J2&quot;] = Properties.Resources.FC039;
			CardUI._resNames[&quot;J1&quot;] = Properties.Resources.FC040;
			CardUI._resNames[&quot;K4&quot;] = Properties.Resources.FC041;
			CardUI._resNames[&quot;K3&quot;] = Properties.Resources.FC042;
			CardUI._resNames[&quot;K2&quot;] = Properties.Resources.FC043;
			CardUI._resNames[&quot;K1&quot;] = Properties.Resources.FC044;
			CardUI._resNames[&quot;L4&quot;] = Properties.Resources.FC045;
			CardUI._resNames[&quot;L3&quot;] = Properties.Resources.FC046;
			CardUI._resNames[&quot;L2&quot;] = Properties.Resources.FC047;
			CardUI._resNames[&quot;L1&quot;] = Properties.Resources.FC048;
		}
		return _resNames[id];
	}
}

■場と手札を表示するためのユーザーコントロールを作る

チープな画面では、場と手札は ListBox を使っているので、これに対応するようにリストを扱えるようなユーザーコントロールを作ります。

場には最大12枚、手札は最大8枚なので、PictureBox をペタペタと張り付けてリスト化しておきます。可搬性はないのですが、このぐらいのユーザーコントロールならばペタペタ作ったほうがよいかと。
データバインドするために、DataSource プロパティを作っておくのがミソです。

public partial class BaControl : UserControl
{
	public BaControl()
	{
		InitializeComponent();

		_pics = new List<PictureBox>();
		_pics.Add(this.pictureBox1);
		_pics.Add(this.pictureBox2);
		_pics.Add(this.pictureBox3);
		_pics.Add(this.pictureBox4);
		_pics.Add(this.pictureBox5);
		_pics.Add(this.pictureBox6);
		_pics.Add(this.pictureBox7);
		_pics.Add(this.pictureBox8);
		_pics.Add(this.pictureBox9);
		_pics.Add(this.pictureBox10);
		_pics.Add(this.pictureBox11);
		_pics.Add(this.pictureBox12);
	}
	protected List<PictureBox> _pics;

	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}

	protected void UIUpdate()
	{
		int i = 0;
		if (this.DataSource != null)
		{
			foreach (var c in this.DataSource)
			{
				_pics[i++].Image = CardUI.GetResName(c.ID);
			}
		}
		for (; i < _pics.Count; ++i)
		{
			_pics[i].Image = CardUI.GetUra();
		}
	}
}

手札のほうも似た感じなのですが、PictureBox をクリックしたときのイベントを追加しておきます。

public partial class TefudaControl : UserControl
{
	public TefudaControl()
	{
		InitializeComponent();

		_pics = new List<PictureBox>();
		_pics.Add(this.pictureBox1);
		_pics.Add(this.pictureBox2);
		_pics.Add(this.pictureBox3);
		_pics.Add(this.pictureBox4);
		_pics.Add(this.pictureBox5);
		_pics.Add(this.pictureBox6);
		_pics.Add(this.pictureBox7);
		_pics.Add(this.pictureBox8);
	}
	protected List<PictureBox> _pics;

	protected List<Card> _data;
	public List<Card> DataSource
	{
		get { return _data; }
		set
		{
			_data = value;
			UIUpdate();
		}
	}

	protected void UIUpdate()
	{
		int i = 0;
		if (this.DataSource != null)
		{
			foreach (var c in this.DataSource)
			{
				_pics[i++].Image = CardUI.GetResName(c.ID);
			}
		}
		for (; i < _pics.Count; ++i)
		{
			_pics[i].Image = CardUI.GetUra();
		}
	}

	private void pict_Click(object sender, EventArgs e)
	{
		int i=0;
		foreach (var p in _pics)
		{
			if (p == sender)
			{
				if (p.Image != CardUI.GetUra())
				{
					if (ClickCard != null)
					{
						ClickCard(_data[i]);
						break;
					}
				}
			}
			i++;
		}
	}
	public event Action<Card> ClickCard;
}

■リッチな画面のコードを追加する

手札でカードをクリックしたときのイベントと、

/// <summary>
/// player1のカードを場に出す
/// </summary>
/// <param name=&quot;card&quot;></param>
void tefudaControl1_ClickCard(Card c)
{
	_board.Player1.IntoBa(c);
	_board.Ba.PutCard(c);
	_curPlayer = _board.Player1;
	// 画面の更新
	ScrUpdate();
}

画面を更新するところのコードを少しだけ修正します。

/// <summary>
/// 画面の更新
/// </summary>
void ScrUpdate()
{
	listBox1.Items.Clear();
	listBox2.Items.Clear();
	listBox3.Items.Clear();
	listBox4.Items.Clear();
	listBox5.Items.Clear();
	listBox1.Items.AddRange(_board.Ba.Cards.ToArray());
	listBox2.Items.AddRange(_board.Player1.MyCards.ToArray());
	listBox3.Items.AddRange(_board.Player1.TakenCards.ToArray());
	listBox4.Items.AddRange(_board.Player2.MyCards.ToArray());
	listBox5.Items.AddRange(_board.Player2.TakenCards.ToArray());

	label7.Text = _board.Ba.PlayerCard.ToString();

	// 点を表示
	Yaku yaku1 = _board.Game.CalcYaku(_board.Player1.TakenCards);
	Yaku yaku2 = _board.Game.CalcYaku(_board.Player2.TakenCards);
	int ten1 = _board.Game.CalcTen(yaku1);
	int ten2 = _board.Game.CalcTen(yaku2);
	label10.Text = ten1.ToString();
	label11.Text = ten2.ToString();
	// ここだけ追加
	baControl1.DataSource = _board.Ba.Cards; 
	tefudaControl1.DataSource = _board.Player1.MyCards;
	tefudaControl2.DataSource = _board.Player2.MyCards;
}

こうするだけで、いや「だけ」ってほどではないですが、あまりコードを修正をせずに「ゲームらしい画面」ができがあるということで。

あと、追加するのは、

  • 手札や山から場に1枚だしたときのコントロール(忘れてた)
  • 持ち札のコントロール
  • 役ができたときのコントロール(必要?)

なところです。これを追加すると「静的なデータの表現」が終わるわけで、アニメーションとかありませんが、画面キャプチャ的にはそれなりが画面ができます。そのあとに「動的なデータの表現」ってプロセスが待っていて、ゲームらしい「動き」を追加します。

カテゴリー: C# | 脳内補完を現実にすることで物理的制約により準優先が決まる はコメントを受け付けていません

消えてしまった時間が消えてしまわないように足跡を残す

正月を潰して仕事、そのあた気疲れ(?)とインフルエンザの打撃…ってな感じで1月があっという間に過ぎてしまったわけですが。「あっという間」かどうかは、過去を振り替えてみるとわかるかも、ということで、しばらくは、日記風に blog を書き綴るということで、調子を戻しましょう > 自分、うんうん。

最初はチープな画面を作ってロジックを確認を | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3982

の続きです。

  1. データクラスを作成する
  2. チープな画面を作成する

ってことで、Model と View が揃ったので、ゲームロジックを作っていきます。データ形式の持ち方はこれでいいのか?ってのを確かめるためでもあるし、ざっとゲームロジックを作ることで、それが「遊べるものかどうか?」ってのが確認できます。ええ、「遊べなそう」ってのが分かったら清く捨ててしまえばいいのです。そのために、この手のデータクラスとか、チープな画面をさっくりと作るのは大切かと。
この方法は、業務アプリでもよくやる方法で、「捨て去ってもいい時間」を使ってプロトタイプ的なものを作ります。世の中のプロトタイプだとお客に見せるために、結構手を込んだものを作る場合がありますが(プレゼンテーション的な意味があるので、それはそれで重要なのです)、自分で確認するプロトタイプ≒これが本当に動くのかどうかのプロトタイプの場合は、「捨てても惜しくない時間を割り当てます」。2,3時間というところでしょうか。長くて1日ですね。ただ、最近気づいたのは、1日でさっくりできるようになるための「実力」を付けるのは結構鍛錬がいるのではないかな、ということ。20年前のパソコン通信の頃の「この人の開発スピードってすごいな!」って程度には、やっとこさ、達することができたような気が…するんだけど、どうでしょう?いやいや、本当のところが、がっと集中して作れる程度が2,3時間ってところなんですけどね。ってことかも。

■データを作成する

さて、通常はそれらしいサンプルを作成するとこから(データクラスの操作性の確認も兼ねて)始めるわけですが、花札の場合は枚数が限られているので、データも作ってしまいます。
Card クラスに持たせてしまうのは異論がある!(私もある!)のですが、ざっと作ったということで。ゲームとしては「こいこい」を作る予定なので、点数自体には意味がないのですが、カス札とか短冊とかを区別するために必要ってことです。このあたりはユーティリティクラスを作っても同じですね。データ量が多い場合は、検索時の効率を考えて、ってのもあるのですが、このぐらいならば全件検索しても特に問題ないし。

/// <summary>
/// カード
/// </summary>
public class Card
{
	string _id;
...
	static protected Dictionary<string,int> _tens;
	/// <summary>
	/// 点数を取得
	/// </summary>
	public int Ten
	{
		get
		{
			if (Card._tens == null)
			{
				Card._tens = new Dictionary<string, int>();
				Card._tens[&quot;A1&quot;] = 20; // 松に鶴
				Card._tens[&quot;A2&quot;] = 5;
				Card._tens[&quot;A3&quot;] = 1;
				Card._tens[&quot;A4&quot;] = 1;
				Card._tens[&quot;B1&quot;] = 10; // 梅に鶯
				Card._tens[&quot;B2&quot;] = 5;
				Card._tens[&quot;B3&quot;] = 1;
				Card._tens[&quot;B4&quot;] = 1;
				Card._tens[&quot;C1&quot;] = 20; // 桜に幕
				Card._tens[&quot;C2&quot;] = 5;
				Card._tens[&quot;C3&quot;] = 1;
				Card._tens[&quot;C4&quot;] = 1;
				Card._tens[&quot;D1&quot;] = 10; // 藤にホトトギス
				Card._tens[&quot;D2&quot;] = 5;
				Card._tens[&quot;D3&quot;] = 1;
				Card._tens[&quot;D4&quot;] = 1;
				Card._tens[&quot;E1&quot;] = 10; // 菖蒲に八つ橋
				Card._tens[&quot;E2&quot;] = 5;
				Card._tens[&quot;E3&quot;] = 1;
				Card._tens[&quot;E4&quot;] = 1;
				Card._tens[&quot;F1&quot;] = 10; // 牡丹に蝶
				Card._tens[&quot;F2&quot;] = 5;
				Card._tens[&quot;F3&quot;] = 1;
				Card._tens[&quot;F4&quot;] = 1;
				Card._tens[&quot;G1&quot;] = 10; // 萩に猪
				Card._tens[&quot;G2&quot;] = 5;
				Card._tens[&quot;G3&quot;] = 1;
				Card._tens[&quot;G4&quot;] = 1;
				Card._tens[&quot;H1&quot;] = 20; // ススキに月
				Card._tens[&quot;H2&quot;] = 10;
				Card._tens[&quot;H3&quot;] = 1;
				Card._tens[&quot;H4&quot;] = 1;
				Card._tens[&quot;I1&quot;] = 10; // 菊に盃
				Card._tens[&quot;I2&quot;] = 5;
				Card._tens[&quot;I3&quot;] = 1;
				Card._tens[&quot;I4&quot;] = 1;
				Card._tens[&quot;J1&quot;] = 10; // 銀杏に鹿
				Card._tens[&quot;J2&quot;] = 5;
				Card._tens[&quot;J3&quot;] = 1;
				Card._tens[&quot;J4&quot;] = 1;
				Card._tens[&quot;K1&quot;] = 20; // 小野道風に蛙
				Card._tens[&quot;K2&quot;] = 10;
				Card._tens[&quot;K3&quot;] = 5;
				Card._tens[&quot;K4&quot;] = 1;
				Card._tens[&quot;L1&quot;] = 50; // 桐に鳳凰
				Card._tens[&quot;L2&quot;] = 1;
				Card._tens[&quot;L3&quot;] = 1;
				Card._tens[&quot;L4&quot;] = 1;
			}
			return Card._tens[_id];
		}
	}
	public override string ToString()
	{
		return _id;
	}
}

■役を作る

ざっくりとデータができたら、役を作ります。いわゆるカードゲームの場合は、同じパターンで作るのではないかな、と。最初は「役を計算する」部分と「点数を計算する」部分とを独立させて作っていたのですが、結局のところ「役」を使って「点数」を計算することになるので、CalcYaku と CalcTen は無関係になっていません。まあ、これはこれでよいかと。

/// <summary>
/// 役クラス
/// </summary>
public class Yaku
{
	public List<Card> GoKou;
	public List<Card> ShiKou;
	public List<Card> AmeShiKou;
	public List<Card> SanKou;
	public List<Card> InoShika;
	public List<Card> AkaTan;
	public List<Card> AoTan;
	public List<Card> Hanami;
	public List<Card> Tukimi;
	public List<Card> Tane;
	public List<Card> Tan;
	public List<Card> Kasu;
}

/// <summary>
/// ゲームルールライブラリ
/// </summary>
public class Game
{
...
	/// <summary>
	/// 役を計算する
	/// </summary>
	/// <param name=&quot;cards&quot;></param>
	/// <returns></returns>
	public Yaku CalcYaku(List<Card> cards)
	{
		Yaku yaku = new Yaku();
		// 菊に盃の有無をチェック
		bool I1 = cards.Find(n => n.ID == &quot;I1&quot;) != null;

		// 20点札をチェック
		var cs = cards.FindAll(n => n.Ten == 20);
		if (cs.Count == 5)
			yaku.GoKou = new List<Card>( cs );
		else if ( cs.Count == 4 ) 
			if (cs.Find(n => n.ID == &quot;K1&quot;) != null ) 
 					yaku.AmeShiKou = new List<Card>( cs );
			else 
				yaku.ShiKou = new List<Card>( cs );
		else if ( cs.Count == 3 ) 
			yaku.SanKou = new List<Card>( cs );

		// 花見、月見
		cs = cards.FindAll(n => n.ID == &quot;I1&quot; || n.ID == &quot;C1&quot;);
		if ( cs.Count == 2 )
			yaku.Hanami = new List<Card>(cs);
		cs = cards.FindAll(n => n.ID == &quot;I1&quot; || n.ID == &quot;H1&quot;);
		if (cs.Count == 2)
			yaku.Hanami = new List<Card>(cs);

		// 猪鹿蝶
		cs = cards.FindAll(n => n.ID == &quot;G1&quot; || n.ID == &quot;J1&quot; || n.ID == &quot;F1&quot;);
		if (cs.Count == 3)
		{
			yaku.InoShika = new List<Card>(cs);
			// タネに追加
			yaku.Tane = cards.FindAll(n => n.Ten == 10);
		}
		// 赤短
		cs = cards.FindAll(n => n.ID == &quot;A2&quot; || n.ID == &quot;B2&quot; || n.ID == &quot;C2&quot;);
		if ( cs.Count == 3 ) 
			yaku.AkaTan = new List<Card>(cs);
		// 青短
		cs = cards.FindAll(n => n.ID == &quot;I2&quot; || n.ID == &quot;J2&quot; || n.ID == &quot;F2&quot;);
		if ( cs.Count == 3 )
			yaku.AoTan = new List<Card>(cs);
		if (yaku.AkaTan != null || yaku.AoTan != null)
		{
			// タンに追加
			yaku.Tan = cards.FindAll(n => n.Ten == 5); 
		}
		// タネ
		if ( yaku.Tane == null ) {
			cs = cards.FindAll(n => n.Ten == 10);
			if ( cs.Count >= 5 ) 
				yaku.Tane = new List<Card>(cs);
		}
		// タン
		if ( yaku.Tan == null ) {
			cs = cards.FindAll(n => n.Ten == 5);
			if ( cs.Count >= 5 ) 
				yaku.Tane = new List<Card>(cs);
		}
		// カス
		cs = cards.FindAll(n => n.Ten == 1);
		if ( I1 == true && yaku.Tane.Find(n=>n.ID== &quot;I1&quot;) == null ) 
			cs.Add( cards.Find( n=>n.ID == &quot;I1&quot; ));
		if ( cs.Count >= 10 ) 
			yaku.Kasu = new List<Card>(cs);

		return yaku;
	}

	/// <summary>
	/// 点数を計算する
	/// </summary>
	/// <param name=&quot;yaku&quot;></param>
	/// <returns></returns>
	public int CalcTen( Yaku yaku )
	{
		int ten = 0;
		// 20点札をチェック
		if (yaku.GoKou != null) ten += 10;	// 五光
		if (yaku.SanKou != null) ten += 8;	// 四光
		if (yaku.AmeShiKou != null) ten += 7;	// 雨四光
		if (yaku.SanKou != null) ten += 5;	// 三光
		if (yaku.Hanami != null) ten += 5;	// 花見
		if (yaku.Tukimi != null) ten += 5;	// 月見
		if (yaku.InoShika != null)	// 猪鹿蝶
		{
			ten += 5;
			// タネ分を加算
			if (yaku.Tane != null)
				ten += yaku.Tane.Count - 3;
		}
		if (yaku.AkaTan != null) ten += 5;	// 赤短
		if (yaku.AoTan != null) ten += 5;	// 青短
		if (yaku.AoTan != null || yaku.AkaTan != null)
		{
			// タン分を加算
			if (yaku.Tan != null)
				ten += yaku.Tan.Count - yaku.AkaTan.Count - yaku.AoTan.Count;
		}
		// タネ
		if (yaku.Tane != null && yaku.InoShika == null)
			ten += yaku.Tane.Count - 4;
		// タン
		if (yaku.Tan != null && yaku.AkaTan == null && yaku.AoTan == null)
			ten += yaku.Tan.Count - 4;
		// カス
		if (yaku.Kasu != null)
			ten += yaku.Kasu.Count - 9;
		return ten;
	}
}

で、これを組み込んで、チープな画面を使ってテストをすると、そこそこ「遊べる」ことがわかります。昔ならばコマンドラインでぽちぽちとキーボードでテストをしていたのですが、Windows フォームが簡単にできるようになったので、これはこれでよいかと。

■お次は

  1. データクラスを作成する
  2. チープな画面を作成する
  3. ゲームロジックを作成する

ってところまでできたので、この度は View の部分を少しリッチにしてみます。
リッチにするところは、

  1. 花札の画像を使って表示する。
  2. 花札ゲームのアクションを作る。

ってところです。花札の画像自体は、Windows ストア アプリのゲーム画像を流用するとして、アクションの部分は、MVVM とユーザーコントロールとの兼ね合いもあるので、ちょっと試行中。

カテゴリー: 雑談 | 2件のコメント

最初はチープな画面を作ってロジックを確認を

今更ながら年始の雑文で穴埋めを | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3978

の続き。

ロジックの確認のために、下記のようなチープな画面を作成する。

この手のロジックをチェックするのは、UnitTest を使うのがいいのですが、今回は UI のチェックも兼ねるので、Windows フォームを使っています。最初は、いきなり Windows ストア アプリにしようかと思ったのですが、UI のコーディングがバインドと混ざってややこしくなるので、フォームアプリで作成ということで。

フォームに書いたコードは使い捨てになるので、できるだけ簡潔に…と云いますか、フォームでもWindows ストア アプリでもそれなりに動くようにしたいので(コードレベルでは無理なので、ロジックの手続きレベルで)、ロジックの呼び出しテストも兼ねて。

public partial class Form1 : Form
{
	public Form1()
	{
		InitializeComponent();

		_board.Player1.EventSelCard += Player1_EventSelCard;

		listBox1.Sorted = true;
		listBox2.Sorted = true;
		listBox3.Sorted = true;
		listBox4.Sorted = true;
		listBox5.Sorted = true;

	}

	/// <summary>
	/// 2枚から選択する
	/// </summary>
	/// <param name=&quot;c1&quot;></param>
	/// <param name=&quot;c2&quot;></param>
	/// <returns></returns>
	Card Player1_EventSelCard(Card c1, Card c2)
	{

		string msg = string.Format(
			&quot;{0} と {1} があります。{2} を選択しますか?&quot;,
			c1.ID, c2.ID, c1.ID);
		var btn = MessageBox.Show(msg, &quot;&quot;, MessageBoxButtons.YesNo);
		if (btn == System.Windows.Forms.DialogResult.Yes)
		{
			return c1;
		}
		else
		{
			return c2;
		}
	}

	GameBoard _board = new GameBoard();
	Player _curPlayer;
	/// <summary>
	/// 山から1枚取得
	/// </summary>
	/// <param name=&quot;sender&quot;></param>
	/// <param name=&quot;e&quot;></param>
	private void button1_Click(object sender, EventArgs e)
	{
		Card c = _board.Yama.GetCard();
		_board.Ba.PutCard(c);
		// 画面の更新
		ScrUpdate();
	}

	/// <summary>
	/// マッチしたカードを場から取得
	/// </summary>
	/// <param name=&quot;sender&quot;></param>
	/// <param name=&quot;e&quot;></param>
	private void button2_Click(object sender, EventArgs e)
	{
		var lst = _board.Game.MatchBa(_board.Ba, _board.Ba.PlayerCard, _curPlayer);
		_board.Ba.GetCard(lst);
		_curPlayer.GetCard(lst);
		// 画面の更新
		ScrUpdate();
	}

	/// <summary>
	/// 花札を配る
	/// </summary>
	/// <param name=&quot;sender&quot;></param>
	/// <param name=&quot;e&quot;></param>
	private void button5_Click(object sender, EventArgs e)
	{
		// 花札を配る
		_board.Reset();
		_curPlayer = _board.Player1;
		// 画面の更新
		ScrUpdate();
	}

	/// <summary>
	/// player1のカードを場に出す
	/// </summary>
	/// <param name=&quot;sender&quot;></param>
	/// <param name=&quot;e&quot;></param>
	private void button3_Click(object sender, EventArgs e)
	{
		if (listBox2.SelectedIndex == -1) 
			return;

		var c = (Card)listBox2.SelectedItem;
		_board.Player1.IntoBa(c);
		_board.Ba.PutCard(c);
		_curPlayer = _board.Player1;
		// 画面の更新
		ScrUpdate();
	}

	/// <summary>
	/// player2のカードを場に出す
	/// </summary>
	/// <param name=&quot;sender&quot;></param>
	/// <param name=&quot;e&quot;></param>
	private void button4_Click(object sender, EventArgs e)
	{

		if (listBox4.SelectedIndex == -1)
			return;

		var c = (Card)listBox4.SelectedItem;
		_board.Player2.IntoBa(c);
		_board.Ba.PutCard(c);
		_curPlayer = _board.Player2;
		// 画面の更新
		ScrUpdate();
	}

	/// <summary>
	/// 画面の更新
	/// </summary>
	void ScrUpdate()
	{
		listBox1.Items.Clear();
		listBox2.Items.Clear();
		listBox3.Items.Clear();
		listBox4.Items.Clear();
		listBox5.Items.Clear();
		listBox1.Items.AddRange(_board.Ba.Cards.ToArray());
		listBox2.Items.AddRange(_board.Player1.MyCards.ToArray());
		listBox3.Items.AddRange(_board.Player1.TakenCards.ToArray());
		listBox4.Items.AddRange(_board.Player2.MyCards.ToArray());
		listBox5.Items.AddRange(_board.Player2.TakenCards.ToArray());

		label7.Text = _board.Ba.PlayerCard.ToString();
	}
}

画面のロジックは基本はユーザーからのイベントドリブンで、ユーザーがなんらかの選択をした時に発生します。昔は業務ロジックと画面からのイベントを分離させるために、「イベントごとに関数を作る」ってなことをやってたりしますが、煩雑なので、最近はパスですね。ですが、NUnit などの自動テストを有効に働かせるためには、画面のほうに業務ロジックを入れないようにするのがベター…なのですが、いまのところこれといった定番がありません。
業務アプリのような簡単な(?)画面の場合には、MVVM か MVC で分離させて、業務ロジックを NUnit でテストして、くっつけた上でアプリ上で再テストって手順がよいのですが、ゲームの場合はどうなんでしょうね?ってのが、今の私の(解消しなければいけない)疑問点です。

そんな訳で、ユーザーがアクションをするボタンの類と、画面更新 ScrUpdate という簡単な組み合わせにしています。このボタンイベントのところが長くなったら、適宜ロジックのほうへ移行ってな雰囲気で組み立てるとよいかと。

カテゴリー: 雑談 | 最初はチープな画面を作ってロジックを確認を はコメントを受け付けていません

今更ながら年始の雑文で穴埋めを

年末年始も二週間が過ぎて皆様お仕事ご学業お試験などは如何でございましょうか…などと年始の挨拶をしたいわけでもなく、なんとなく過ぎてしまった2週間ではありますが、いえ、なんか、だらだと仕事をしていただけなんですけどね。やっぱり休むときは休まないと、捗るものも捗らないのかと思い至ったり。

ひとまず無事、Microsoft MVP の Visual C#(今回はC#)を頂き、その MSDN は引き続き使えるわけで、その先に Windows ストア アプリがありーのという訳で、さて、去年の「親馬鹿アプリ」にするのか、「化粧男子シリーズ」にするのか、それとも何かきちっとしたものを?と考えてあぐねたまま数日が過ぎ、という具合で今日に至っております。

で、なんか手頃なゲーム用コンポーネント作り、というか、Windows ストア アプリのゲームを作る中でゲームらしいコンポーネントの組み合わせを作れないかと思案していたのですが、結局のところ「花札」を作ってみようかという路線で落ち着いております。ええ、MVVM というか、MVC というか、ゲームロジックの部分をきちんと分類させておいて、UI の部分は切り替えられるように、かつ、ゲームロジックを差し替え可能なように(ネットワーク対戦とか、コンピュータ対戦とか)という実用的なサンプルですね。ねらいとしては、

  • ゲームロジックの使い回しが可能(Win8, iPhoneで)
  • UI の切り替えが可能
  • UI のアクション部分が切り替えが可能

ってな風にしたいわけです。
「ゲームロジックの使いまわし」ってのは、誰もが考えることなのですが、これはゲームアプリを発表した後にも「定期的にバージョンアップ」できるようにするためです。ええ、Windows ストア アプリとiPadアプリの両方に対応するという意味もあります。
ハードルを高くして、C# と Objective-C のコード共有…というか、C++で書けば、両方で共有できるわけですが、それはせずに、UML レベルで共有という感じで。そもそも「ロジック」部分というのは、それほど言語に差がないわけで(関数型言語を使うと、かなり違いますが)、C#, Objective-C, Java あたりをターゲットにしている間は、どれも UML レベルで統括が可能でしょう。

そのなかで「定期的なバージョンアップ」ってのがあって、昨今のゲームアプリなりを発表した後で、そのままってのはちょっと難しいかなと思っています。かといって、次々と別のゲームロジックを考えるのも大変で(いろいろあるにはあるんでしょうが)、そうなると、なんらかのスライドしたアイデアを盛り込めるような作りをしておく、いや、アイデアをコードレベルで追加しやすいようにしておくというのがポイントかと思います。ええ、少なくとも自分には。

UI の切り離しってのは、定番ではありますが、札のデザインもそうなのですが、

  • 札の並び方(ばらばらっと置くような感じとか)
  • なんとなく 3D っぽく置くような感じとか
  • ルールが変わっても、あまり UI 部分で苦労しないような作りにするとか

ってのを目指します。このあたり、XAML ならば、かなり手軽に作れそうな感じがしています。手札なり場なりをコンポーネントで作っておいて、それぞれを storyboard でアニメーションさせるとか、そんな感じですね。

で、ゲームロジックのほうなのですが、いわゆる「いかさまロジック」をきっちりと入れるのが目標です。「いかさまロジック」をわざわざ入れるというのも変ですが、コンピューター対戦の場合は、それなりの「人格」みたいなのがあったほうがおもしろいですよね。で、たいていの場合はランダム値を上げるとか、ユーザーの引きを弱くするとか、という不当な方法に走ってしまうわけですが、そういう「納得いかーん」というものではなくて、「自摸、嶺上開花」とかそいういうやつです。まあ、最終的には麻雀ロジックのいかさまバージョンを作りたいのですが、ちょっと今の私には難しいので、手始めに花札から、という具合。花札アプリを作っている間に、なんらかの UI コンポーネントも徐々に揃えられるかなと。

ひとまず、任天堂の花札のページを参考替わりに。

花札の歴史・遊び方
http://www.nintendo.co.jp/n09/hana-kabu_games/index.html

■ゲームロジックを作ってみる

「手四」の場合、配り直しになっていますが、まあこんな感じで。

  • Card
  • Player
  • Yama
  • Ba

というクラスを作っておきます。これはいわゆる Value Class ですね。Player クラスが、思考ロジックを含んでいるので、ちょっと妙ですが、このあたりは実際に XAML にバインドをさせたときに工夫していきましょう。

GameBoard は、Player, Yama, Ba をまとめたクラスです。これは、UI のほうに持っていくとコードが煩雑になる…というか分離できなくなるので、コントロール系のクラスとして使います。あと、ゲームのルール自体を Game クラスが受け持つ感じ。これは、ライブラリ的に使います。

namespace Hanahuda
{
	/// <summary>
	/// カード
	/// </summary>
	public class Card
	{
		string _id;

		protected Card() { }
		/// <summary>
		/// コンストラクタ
		/// </summary>
		/// <param name=&quot;id&quot;></param>
		public Card(string id)
		{
			_id = id;
		}

		public string ID
		{
			get { return _id; }
		}
		/// <summary>
		/// 月を取得
		/// </summary>
		public string Month
		{
			get { return _id.Substring(0, 1); }
		}
		/// <summary>
		/// 番号を取得
		/// </summary>
		public string Num
		{
			get { return _id.Substring(1, 1); }
		}

		public override string ToString()
		{
			return _id;
		}
	}

	/// <summary>
	/// 山
	/// </summary>
	public class Yama
	{
		/// <summary>
		/// 花札のコレクション
		/// </summary>
		public List<Card> Cards = new List<Card>();

		/// <summary>
		/// カードを山から取る
		/// </summary>
		/// <returns></returns>
		public Card GetCard()
		{
			if (Cards.Count == 0)
				return null;
			Card c = Cards[0];
			Cards.RemoveAt(0);
			return c;
		}
	}
	/// <summary>
	/// 場
	/// </summary>
	public class Ba
	{
		/// <summary>
		/// 花札のコレクション
		/// </summary>
		public List<Card> Cards = new List<Card>();

		// プレイヤーが出したカード
		public Card PlayerCard = new Card(&quot;&quot;);

		/// <summary>
		/// 場に1枚カードを出す
		/// </summary>
		/// <param name=&quot;c&quot;></param>
		public void PutCard(Card c)
		{
			PlayerCard = c;
		}

		/// <summary>
		/// マッチしたカードを取り除く
		/// </summary>
		/// <param name=&quot;cs&quot;></param>
		public void GetCard(IEnumerable<Card> cs)
		{
			if (cs.Count() == 0)
			{
				Cards.Add(PlayerCard);
			}
			else
			{
				foreach (var c in cs)
				{
					this.Cards.Remove(c);
				}
			}
			PlayerCard = new Card(&quot;&quot;);

		}
	}

	/// <summary>
	/// プレイヤー
	/// </summary>
	public class Player
	{
		// 手持ちのカード
		public List<Card> MyCards = new List<Card>();
		// 取ったカード
		public List<Card> TakenCards = new List<Card>();

		/// <summary>
		/// 手持ちにカードを加える
		/// </summary>
		/// <param name=&quot;c&quot;></param>
		public void GetCard(Card c)
		{
			MyCards.Add(c);
		}
		public void GetCard(IEnumerable<Card> cs)
		{
			TakenCards.AddRange(cs);
		}

		/// <summary>
		/// カードを場に捨てる
		/// </summary>
		/// <param name=&quot;c&quot;></param>
		/// <returns></returns>
		public Card IntoBa(Card c)
		{
			MyCards.Remove(c);
			return c;
		}

		public delegate Card HandlerSelCard( Card c1, Card c2 );
		public event HandlerSelCard EventSelCard;
		/// <summary>
		/// 場に2枚あるときに選択する
		/// </summary>
		/// <param name=&quot;c1&quot;></param>
		/// <param name=&quot;c2&quot;></param>
		/// <returns></returns>
		public Card SelCard(Card c1, Card c2)
		{
			if (EventSelCard != null)
			{
				return EventSelCard(c1, c2);
			} else {
				return c1;
			}
		}

	}

	/// <summary>
	/// ゲームルールライブラリ
	/// </summary>
	public class Game
	{
		/// <summary>
		/// 場と1枚のカードをチェックする
		/// </summary>
		/// <param name=&quot;ba&quot;></param>
		/// <param name=&quot;card&quot;></param>
		/// <returns></returns>
		public List<Card> MatchBa(Ba ba, Card card, Player pl)
		{
			var lst = new List<Card>();

			var cs = ba.Cards.Where(n => n.Month == card.Month);
			if (cs.Count() == 0)
				return lst;
			lst.Add(card);

			if (cs.Count() == 1) {
				lst.Add(cs.First());
			} else if ( cs.Count() == 2 ) {
				// どちらを取るか選択させる
				// lst.Add(cs.First());
				Card c1 = cs.First();
				Card c2 = cs.Last();
				Card c = pl.SelCard(c1, c2);
				lst.Add(c);
			} else if ( cs.Count() == 3 ) {
				lst.AddRange(cs);
			}
			return lst;
		}
	}

	/// <summary>
	/// ゲームボード
	/// </summary>
	public class GameBoard
	{
		Player _player1 = new Player();
		Player _player2 = new Player();
		Ba _ba = new Ba();
		Yama _yama = new Yama();
		Game _game = new Game();

		public Player Player1 { get { return _player1; } }
		public Player Player2 { get { return _player2; } }
		public Ba Ba { get { return _ba; } }
		public Yama Yama { get { return _yama; } }
		public Game Game { get { return _game; } }

		/// <summary>
		/// コンストラクタ
		/// </summary>
		public GameBoard()
		{
		}

		/// <summary>
		/// 花札を配る
		/// </summary>
		public void Reset()
		{
			Yama.Cards.Clear();
			Ba.Cards.Clear();
			Player1.MyCards.Clear();
			Player1.TakenCards.Clear();
			Player2.MyCards.Clear();
			Player2.TakenCards.Clear();

			// カードを作成
			List<Card> cards = new List<Card>();
			for (int m = 1; m <= 12; m++)
			{
				for (int n = 1; n <= 4; n++)
				{
					Card c = new Card(string.Format(&quot;{0}{1}&quot;, Convert.ToChar('A'+m-1) , n));
					cards.Add(c);
				}
			}
			while (true)
			{
				// カードをシャッフル
				Random rnd = new Random();
				while (cards.Count > 0)
				{
					int n = rnd.Next(cards.Count);
					Yama.Cards.Add(cards[n]);
					cards.RemoveAt(n);
				}
				// player1, player2, ba の順に8枚ずつ配る
				for (int i = 0; i < 4; i++)
				{
					Player1.GetCard(Yama.GetCard());
					Player1.GetCard(Yama.GetCard());
					Player2.GetCard(Yama.GetCard());
					Player2.GetCard(Yama.GetCard());
					Ba.Cards.Add(Yama.GetCard());
					Ba.Cards.Add(Yama.GetCard());
				}
				// 4枚まとまっていないかチェック
				bool flag1 = Check4Cards(Player1.MyCards);
				bool flag2 = Check4Cards(Player2.MyCards);
				bool flag3 = Check4Cards(Ba.Cards);
				if (flag1 == false && flag2 == false & flag3 == false)
					break;
			}
		}

		bool Check4Cards(List<Card> cs)
		{
			var mon = new Dictionary<string, int>();
			foreach (var c in Player1.MyCards)
			{
				if (mon.ContainsKey(c.Month) == false)
				{
					mon[c.Month] = 1;
				}
				else
				{
					mon[c.Month]++;
				}
			}
			bool flag = false;
			foreach (var v in mon.Values)
			{
				if (v == 4)
				{
					flag = true;
					break;
				}
			}
			return flag;
		}
	}
}

ひとつ悩みどころが、Player.SelCard メソッドです。これは、いわゆる「ユーザーに問い合わせ」をするところで、MVVM, MVC のロジックでも悩みどころですね。コンピュータ思考ルーチンの場合は、直接結果を返せばよいのですが、ユーザーが担当している場合は問い合わせのダイアログを出して、応答待ちになります。この問いあわせの部分をどう作るか、です。

特に分離を考えなければ、UI のほうでチェックしてもよいのですが、そうなると思考ルーチンのほうが変なことになるし…ということで、EventSelCard というイベントを使っています。これが定番なのかどうかは微妙なのですが、

  1. 手札を場に出すアクション
  2. 出した札に対して、2枚マッチすることを判断
  3. ユーザーに問い合わせをする。
  4. ユーザーが選択した札とマッチさせる。
  5. Model で Player.MyCards と Ba.Cards 差し替え
  6. 手札と場の UI を更新

という手順になるので、

  1. UI
  2. Logic
  3. UI
  4. Logic
  5. Model
  6. UI

という流れで UI が間に挟まるパターンですね。この手のユーザー問い合わせはゲームでは頻繁にあるので、ここをうまくクリアに作れないと、MVVM なり MVC なりがうまく組み込めないわけです。業務ロジックの場合は、それほど複雑ではないのと場所が限ら得ているので、そこだけ特殊に作ってしまいますが、ゲームの場合は頻繁にありそう。
それに、コンピュータの思考ルーチンであっても、2枚のマッチを判断するのは Logic の問い合わせになるわけで、この「思考ルーチン」の差し替えを有効にするために、ここはきちんと UI と分離させておかないと困るわけです。そんなわけで、わざわざイベントハンドラにしています…と云いますか、イベントハンドラにしてみました。

これが有効に働くかどうかは、後日。

カテゴリー: C# | 今更ながら年始の雑文で穴埋めを はコメントを受け付けていません

Storyboard の値をコードで動的に変更する

引き続き、
Windows 8 Store apps Advent Calendar : ATND
http://atnd.org/events/33803
の16日目のネタです。

ちょっと、人数が足りないようなので、じゃあ「タイマーを使わない時計アプリの紹介を」ってのを思いついたので立候補したのですが…

アナログ時計を作ろう! (プログラミングの知識がある方向け)
http://technet.microsoft.com/ja-jp/subscriptions/hh524813.aspx

にすでにあるじゃん orz ってことで、自己却下。storyboard を使うと、プログラム内でタイミング制御をしないから楽なんですよね、ってのと、(たぶん)グラボの性能を引き出せるからプログラム内で制御するよりも軽くなるのでは?ってな想像です。

で、仕方がないので、手元の原稿から「Visual Studio 2012 でスナップ対応の画面を作る」ってネタを出そうと思ったのですが、

WinRT/Metro TIPS:ビューの切り替えを実装するには?[Win 8] – @IT
http://www.atmarkit.co.jp/ait/articles/1209/27/news139.html

ってのに、すでに詳しいページがあってですね。ええ、これで十分だからまあ良いかと…。LayoutAwarePage クラスを継承する「基本ページ」に切り替えておくと、VisualStateManager が、XAML の中のコードから引き出してくれる、ってのと、Visual Studio 2101 と Blend には、これを作成するための特別な「デバイス」表示があって、そこで「状態記録の有効化」をしておくと、見た目通りに作れて便利。ってな具合なのです。

で、ネタかぶりってことで、storyboard の値をプログラム内で、動的に変えるって技…というかノウハウを紹介します。

麻雀パイコントロールを作ったときに悩んだのが、「捨て牌」の動作です。「一萬」を捨ているときと、左端の「1筒」を捨てる時では、捨てる開始位置が違うわけで、これそれぞれに storyboard を作らないとだめなのか…と最初、うんざりしてしまました。まあ、Blend でちまちまと 13 牌分作ってもよいのですが、ちょっとでも場所がずれるとだめとか、そもそも自牌コントロールとして、13 牌はまとめてつくってあるわけで、捨てる先の河の位置を考えると、ちまちま作るのはダメなのかと。

で、次に考えたのが、このあたりの storyboard をプログラム内で作る方法です。
捨て牌のアニメーションを blend で作って後に、ちまちまと C# のプログラムコードに直して、Storyboard を起動させればよいかと。

しかしですね、

<Storyboard x:Name=&quot;sbAnimePai&quot;>
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=&quot;(UIElement.RenderTransform).(CompositeTransform.TranslateX)&quot; Storyboard.TargetName=&quot;animeImagePai&quot;>
		<EasingDoubleKeyFrame x:Name=&quot;startPosX&quot;  KeyTime=&quot;0&quot; Value=&quot;892.537&quot;>
			<EasingDoubleKeyFrame.EasingFunction>
				<BackEase EasingMode=&quot;EaseInOut&quot;/>
			</EasingDoubleKeyFrame.EasingFunction>
		</EasingDoubleKeyFrame>
		<EasingDoubleKeyFrame KeyTime=&quot;0:0:0.5&quot; Value=&quot;0&quot;>
			<EasingDoubleKeyFrame.EasingFunction>
				<BackEase EasingMode=&quot;EaseInOut&quot;/>
			</EasingDoubleKeyFrame.EasingFunction>
		</EasingDoubleKeyFrame>
	</DoubleAnimationUsingKeyFrames>
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=&quot;(UIElement.RenderTransform).(CompositeTransform.TranslateY)&quot; Storyboard.TargetName=&quot;animeImagePai&quot;>
        <EasingDoubleKeyFrame x:Name=&quot;startPosY&quot;  KeyTime=&quot;0&quot; Value=&quot;458.208&quot;>
			<EasingDoubleKeyFrame.EasingFunction>
				<BackEase EasingMode=&quot;EaseInOut&quot;/>
			</EasingDoubleKeyFrame.EasingFunction>
		</EasingDoubleKeyFrame>
		<EasingDoubleKeyFrame KeyTime=&quot;0:0:0.5&quot; Value=&quot;0&quot;>
			<EasingDoubleKeyFrame.EasingFunction>
				<BackEase EasingMode=&quot;EaseInOut&quot;/>
			</EasingDoubleKeyFrame.EasingFunction>
		</EasingDoubleKeyFrame>
	</DoubleAnimationUsingKeyFrames>
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=&quot;(UIElement.RenderTransform).(CompositeTransform.Rotation)&quot; Storyboard.TargetName=&quot;animeImagePai&quot;>
		<EasingDoubleKeyFrame KeyTime=&quot;0&quot; Value=&quot;-360&quot;>
			<EasingDoubleKeyFrame.EasingFunction>
				<BackEase EasingMode=&quot;EaseInOut&quot;/>
			</EasingDoubleKeyFrame.EasingFunction>
		</EasingDoubleKeyFrame>
		<EasingDoubleKeyFrame KeyTime=&quot;0:0:0.5&quot; Value=&quot;0&quot;>
			<EasingDoubleKeyFrame.EasingFunction>
				<BackEase EasingMode=&quot;EaseInOut&quot;/>
			</EasingDoubleKeyFrame.EasingFunction>
		</EasingDoubleKeyFrame>
	</DoubleAnimationUsingKeyFrames>
</Storyboard>

動きをチェックしながら作った、storyboard を手作業で C# のプログラムコードに直すは、結構手間です。しかも、Blend で回転やら移動やら BackEase やらと、いろいろなエフェクトを加えたので、なにがなにやら…って具合。しかも、プログラムのコードに落とした後、さらに Blend で直した日には、もう一度プログラムコードを手作業で直さないといけない、という辛い作業が待っています。これじゃあ、とても手軽に storyboard を使う、とは言えませんね。

で、最終的にどうしたのかというと、結果的には簡単でした。
Storyboard の変更したい箇所に「名前」(x:Name)を付ければ良かったのです。先の XAML をよくみると、「x:Name=”startPosX”」と「x:Name=”startPosY”」というふたつの名前がついてあります。自分の牌から出す場所が変更になるので、開始位置だけをプログラムで変更できれば良いのです。

XAML コントロールに名前を付けられるのは普通なのですが、この手の storyboard の要素にも名前が付けられるし、それをコードから参照できます。と言いますか、XAML コードのどの要素についても名前が付けられるわけですね、なるほど。

private void OnPaiToKawa(double x, double y, ImageSource image)
{
	// 移動開始位置を設定
    x += Canvas.GetLeft(myPai);
    y += Canvas.GetTop(myPai) ;
    startPosX.Value = x;
    startPosY.Value = y;
	// 牌の画像を設定
    animeImagePai.Source = image;
	// アニメーション開始
    sbAnimePai.Begin();
}

そんな訳で、開始位置を指定するだけになるので、プログラムコードは簡単になります。
Blend で作った Storyboard もそのまま活用できます、という両方の良いところが使えるようになります。

storyboard でアニメーションを作ったときの手順としては、

  1. 大まかな動きを Blend で作成する。
  2. 開始位置など、プログラムで制御したい部分に名前を付けておく。
  3. 再び、Blend で細かいアニメーションを追加していく。

という具合です。

こんな風に、Blend の Storyboard とプログラムコードの直書き(というか値の変更)ができますよ、っていう話です。
本来は、この storyboard 部分を別リソースにしたいんですけどね。このあたりは調査中。

カテゴリー: 雑談 | Storyboard の値をコードで動的に変更する はコメントを受け付けていません

Windows ストア アプリでパラパラアニメを動かそう

Windows 8 Store apps Advent Calendar : ATND
http://atnd.org/events/33803

の 15 日目のネタですね。本当は「親馬鹿アプリ」のほうにする予定だったのですが、ずるずる状態になってしまったので、以前ブログに書こうと思っていたネタを掘り出してきました。

最初に言うと「書こうと思っていた」というのは、まだ調査途中で「本当にこれが効率的なのか?」、「定番なのか?」ってのがわからないので、保留にしていたわけで…ええ、忘れていたのもぽちぽちなのですが、まあ、年末だし蔵出しということで。

さて、パラパラアニメといえば「アニメgif」なのですが、いろいろ調べてみると、WPF でうまく表示する方法がありません。いや、探せば「MediaElement」を使う方法がでているので、Windows ストア アプリでも使えるのかもしれません(試しておりません)。ですが、今回はもうちょっと違った方法を取ります。

よくゲームアプリで使われている手法で、俗に「うなぎの画像」と呼ばれる…って呼びませんね、なんと言うのかわからないのですが、横に長い画像を使います。ひとつひとつのセルを横につなげて、表示したい位置をスライドさせるという手法ですね。実は、C++/CX のほうには、これが使える Bitmap オブジェクトがあってそれが「ウリ」だったりすのですが、果たして C# にはあるのか?って、のを夏のセッションを聞いたときに思ったのですが、そのところはちょっと不明です。

ちなみに C# で WritableBitmap を作る方法は、

byte配列からWriteableBitmapオブジェクトを作成する – 酢ろぐ
http://d.hatena.ne.jp/ch3cooh393/20120802/1343892131

を参照ということで。

この「うなぎ画像」を使うと便利なのは、

  • それぞれのセルがひとつに連なっているので、管理が簡単?
  • 複数のセルを読み込まないで済むので、ロード時間が少なくて済む。

ってところです。実際、やってみるとわかるのですが、60 枚近い画像を切り替えるのと、1枚の画像をクリップしながら表示するのでは、CPU への影響が格段に違い、アニメーションもスムースになります。

というわけで、ざっと作り方を紹介。

■うなぎ画像を作る。

適当に Form アプリで「うなぎ画像」を作るツールを作ります。

private void button1_Click(object sender, EventArgs e)
{
    int w = 46*60;
    int h = 46;

    for (int n = 1; n <= 6; n++)
    {
        Bitmap allBmp = new Bitmap(w, h);
        Graphics g = Graphics.FromImage(allBmp);
        for (int i = 1; i <= 60; i++)
        {
            string p = string.Format(@&quot;\temp\balls\{0}\{1:00}.png&quot;, n,i);
            Bitmap bmp = new Bitmap(p);
            g.DrawImage(bmp, 46 * (i - 1), 0, 46, 46);
        }
        allBmp.Save(string.Format( @&quot;\temp\ball{0}.png&quot;,n));
    }
}

画像自体は、とあるゲームからパクってきたものです。
画像サイズが、46×46 固定なのは、まあ、ツールなので。

■アニメ用のユーザーコントロールを作る

いくつか試したのですが、ユーザーコントロールを作るのが一番楽です。

<UserControl
    x:Class=&quot;AniBallControls.AniBall&quot;
    xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:local=&quot;using:AniBallControls&quot;
    xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot;
    xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
    mc:Ignorable=&quot;d&quot;
    d:DesignHeight=&quot;92&quot;
    d:DesignWidth=&quot;92&quot;>
	<UserControl.Resources>
        <Storyboard x:Name=&quot;sbAnime&quot; RepeatBehavior=&quot;Forever&quot;>
			<DoubleAnimationUsingKeyFrames x:Name=&quot;keyFrames1&quot; 
				Storyboard.TargetProperty=&quot;(Canvas.Left)&quot; 
				Storyboard.TargetName=&quot;img&quot;>
                <DiscreteDoubleKeyFrame KeyTime=&quot;0:0:0&quot; Value=&quot;0&quot;/>
            </DoubleAnimationUsingKeyFrames>
		</Storyboard>
	</UserControl.Resources>
    <Canvas>
        <Image x:Name=&quot;img&quot; Source=&quot;ms-appx:///balls/ball1.png&quot; 
			Width=&quot;5520&quot; Height=&quot;92&quot; 
			Canvas.Left=&quot;0&quot; Canvas.Top=&quot;0&quot;/>
        <Canvas.Clip>
            <RectangleGeometry Rect=&quot;0,0,92,92&quot;></RectangleGeometry>
        </Canvas.Clip>
    </Canvas>
</UserControl>

46×46 では、デモ用には小さかったので、96×96 に拡大しています。
storyboard を使ってアニメーションさせていますが、中身は後からプログラムで書きます。最初はツールを使って XAML に書いていたのですが、プログラムで書けることが分かったので実行時に書き込みます。
Canvas の上に乗っけて、Canvas.Clip しているところがミソですね。たぶん、WPF とか Silverlight での定番の処理だと思うのですが、似たようなは見つかりませんでした。他にいい方法があるのかも? この clip 位置を、スライドさせる(実際には Image オブジェクトの位置をずらす)ことで、アニメーションができます。

デザイナで見ると、うまくクリップされていることがわかります。本当は横に長い画像なのですが、一番左のセルだけがクリップされています。

で、実際にアニメーションさせるコードがこちら。

public sealed partial class AniBall : UserControl
{
    public AniBall()
    {
        this.InitializeComponent();
    }

    private Size _imageSize = new Size(92, 92);
    private int _curIndex = -1;
    private int _imageCount = 60;

    public ImageSource Source 
    {
        get { return this.img.Source; }
        set { this.img.Source = value; }
    }

    public void Start()
    {
        var kf = this.keyFrames1;
        kf.KeyFrames.Clear();
        for (int i = 0; i < _imageCount; i++)
        {
            var dd = new DiscreteDoubleKeyFrame();
            dd.Value = -_imageSize.Width * i;
            dd.KeyTime = new TimeSpan(10 * 1000 * i * (1000/60)); // 1/60 sec
            kf.KeyFrames.Add(dd);
        }
        this.sbAnime.Begin();
    }
    public void Stop()
    {
        this.sbAnime.Stop();
    }

    private void onTimer(object sender, object e)
    {
        _curIndex++;
        if (_curIndex >= _imageCount)
            _curIndex = 0;
        Canvas.SetLeft(this.img, - _imageSize.Width * _curIndex);
    }
}

普通の storyboard では、開始位置と終了位置を連続につなぎますが(直線とかスプラインとか)、ぱらぱらアニメの場合は、とびとびの値にします。この飛び飛びの値を作るのが DiscreteDoubleKeyFrame クラスですね。これは Blend の Storyboard でも変更できるので、ちまちま 60 フレーム作ることも可能なのですが…面倒なので、プログラムでやります。

この ontimer の負荷がどのくらいかというと、

なところで、3% 以下ですね。ひとつのアニメなので、複数配置したときはどうなるのかは検証していませんが、普通のアニメGIFっぽいことをしたいのであれば、これで OK かと。
ちなに、60 枚の画像切り替えをすると、CPU は 3% 程度で同じなのですが、画面の駒落ちが発生します。どうやら、Image.Source に設定するときが重いらしく、そのあたりの負荷を減らすためにも、一枚のうなぎ画像を使ったほうがよいみたいです。

■メインページを作る

テスト用のメインページには、こんな風に貼り付けます。ユーザーコントロールはツールバーに出てくるので、普通にドラッグ&ドロップすれば ok.

<local:AniBall x:Name=&quot;aniBall1&quot; Source=&quot;ms-appx:///balls/ball1.png&quot;
    HorizontalAlignment=&quot;Left&quot; Height=&quot;100&quot; 
                Margin=&quot;251,266,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;100&quot;/>

タップイベントを設定しておいて、開始と終了を制御します。

AniBall curBall = null;
private void aniBall_Tapped(object sender, TappedRoutedEventArgs e)
{
	if (curBall != null)
	{
		curBall.Stop();
		curBall = null;
	}
	else
	{
		curBall = sender as AniBall;
		curBall.Start();
	}
}

実は、Storyboard が実行中かどうかを取れるので、それをプロパティにすれば良いのですが…まあ、デモなので、これで。

■実行してみる

動かしてみると、結構スムースにくるくると回ります。これだったらゲームに使えるよねっていう感じで、これぐらいだったら、DirectX + C++/CX を使う必要はあるまい(本来はこれが目的)、っていう感じで動きます。

ちなみに、Image コントロールは、透過PNG を扱えるので、背景を付けることもできます。

これができると、パズルゲームぐらいならば、C# で書けそうですよね…って感じですかね。年末年始の休みを利用して、ひとついかがでしょうか? ええ、私は、たぶんお仕事かと orz.

カテゴリー: C#, windows 8 | Windows ストア アプリでパラパラアニメを動かそう はコメントを受け付けていません

Windows ストア アプリで親馬鹿アプリを作ろうッ!!!(準備編)

ふと、思いついたネタです…続くかどうかわかりませんが…ちなみに、アプリ自体はできています。

クリスマスに向けて、親馬鹿アプリを作ってクリスマスプレゼントの代わりにしよう企画~~~ぱぶぱふ~、ってことで、思いついたのでブログネタにしておきます。

絵本を読んでいて常々気になっていたのが、自分の娘や息子向けに作るというパターンです。絵の場合は、絵本を作ればいいけど、じゃあ、プログラマの親の場合はどうするのか?ってことで、じゃあ、自分の子供向けにアプリを作ればいいじゃないか?、ってところに至りました。

が、ノートパソコンは壊されると怖いし、デスクトップアプリだといまいち食いつきが悪いし、iPad のアプリでもいいんですが実機に乗せるには年会費が掛かるし(まあ、私の場合は払っているからいいんでけど)…ってことで、Windows 8 のタブレット PC の子供用アプリを作ります。

タブレット PC の値段が~、ってことなのですが、私の手元にあるのは acer w500。android のほうだと20,000円ってところなのですが、そこは windows 7 のほうの 40,000円のほうを買って、windows 8 に乗せ換えます。

[win8] Acer w500 に windows 8 cp をクリーンインストール(続き) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3304

なんかを参考にしてドライバーを入れ替えてください。

さて、どうせ家庭内で使うアプリ(しかも子供向け)なので、準備するものとしては、

  • タブレット PC 実機(もちろん windows 8)
  • Visual Studio 2012(Express でもok) + Widnows 8 マシン
  • 子供の写真(デジカメとか)
  • 子供の食いつきそうな版権もの(きかんしゃトーマスとかミッフィーとか)

ってところですかね。どこにも公開しないので、個人情報とか版権とか無視です。ええ、私家版ですから。

で、作ったアプリがこれ。スライドパズルです。

image

image

よく見ると、英語で「Shaffle」とか「Reset」とか書いてありますが、まあ、小2の娘はボタンを押せばなんとかわかるみたい。2歳児の息子は、16ピースだと難しいらしいのですが、ぽちぽちと正しい位置に置く(2つのピースをタップして、交換させます)をやろうとはしています。なので、4ピースぐらいにするのを計画中。

プログラム的には、MVVM っぽい形にしているのですが、失敗中。そのあたりも含めて、ソースコードを後日に公開します。プログラム自体は、C# で 300行弱といったところです。ピースと、ピースをまとめたボード、というユーザーコントロールを作っているので、画像の差し替えは比較的楽かな、とおもっているのですが…プログラム上

  • 画像の大きさが固定になっている
  • ピースの数が16ピース固定になっている

という問題があって(私家版なのでこれで良いのですが)、さきほどの2歳児用の4ピース版を作るためには…うーんと唸っている途中です。あと、画像自体はリソース組み込みなので、本当は SkyDrive に配置して、子供に選ばせたいですよね、とか考えてみたり。

と、サンデープログラマっぽいことができるのが、Windows ストア アプリの良いところかなあと気づいたりしております。

カテゴリー: C#, windows 8 | Windows ストア アプリで親馬鹿アプリを作ろうッ!!!(準備編) はコメントを受け付けていません

[win8] パンダ福笑いのインターフェースを考える

[win8] パンダ福笑いを作るために ManipulationDelta を使う | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3877

の続きをざっくりと。プログラム的には、ManipulationDelta あたりを使って作るので、パーツのドラッグは簡単にできます。なぜ、手あるいは指でドラッグすることを考えるのかというと、iPad の初代を買ったときの動機から変わっていないのですが、

image

  • ディスプレイに直接タッチするのは、「直感的」である。
  • ディスプレイの操作に「記号」を持ち込まない。

というのが、子供用(あるいは幼児用)のインターフェースを考えるときの基本かなと感じています。うちの小2の娘は、生まれたころからパソコンがあるわけで、そのあと保育園のころから iPad が出てきたという感じです。つまりインターフェースとしては、マウスで画面を操作するの次に、タッチオペレーションが入るわけです。

マウスを使ってパソコンを操作できる時期ってのは、内田春菊の「わたしたちは繁殖している」を読むと分かる通り、2歳児でもできます。うちの場合は、3歳ごろからできたかなって感じです。マウスだと、マウス自体とマウスポインターが離れているから、という話が多いのですが、子供のとっては、

  • 無意識にマウス自体と、マウスポインタの動きを結びつける。

な感じで「無意識に」結び付けています。車のハンドルが、車自身を左右に動かすと直感的にわかる通り、マウスを動かすとマウスポインターが動くというのは比較的わかりやすいようです。マウスのクリックも意外とわかりやすいみたいです。手が小さいので、マウスボタンを押すときにマウス自体が動いてしまうのが工学的な難点ですかね。

ところが、もっと3歳児をみていると、もっと問題があって、

  • ボタンなどに書かれている「記号」がわからない

というのがあります。記号というのは「START」とか「STOP」とかのアルファベットもありますが、「1」とか「0」とかも記号です。実は「○」はわかりやすいのですが、「×」がわからなかったり「△」が難しかったりします。「×」がわからないので、ウィンドウを閉じる、という操作がなかなか覚えられないのですね。このあたり「記号」≒「言語」のところに関連していくので、年齢があがってきて、ひらがなを覚えると、「×」の意味がわかってきます。なので、目の前にある「記号」が大人にとって、どれだけ簡単であっても、それは「記号」としての「意味」がある限り、言語的に通じないために、コンピュータを操作できないというジレンマに陥ります。これは、Windows でも Mac でも同じみたいです。

その中で比較的わかりやすいのが、

  • 色を配置させる
  • ピクト(簡略化された絵を利用する)

色のほうは、まずは安全のために「信号機」を覚えさせます。赤がとまれ、緑あるいは青ですすめですね。このあたり、各国で色に対する「意味」が異なるので、それに沿う必要はないのですが、おおまかに「色」付きのボタンを配置させると、「何色のボタンを押す」ということで教えやすいのです。

もうひとつは、ピクトを使います。うちの2歳児はなぜかパス亭に表示されている「バスの絵」と実際のバスを即座に結び付けました。形が似ているという意識のなかの「似ている」を即座に判断するわけです。本物の写真から、ある特徴を誇張させたもの、ある特徴を取り出したもの、という具合ですね。

この「色」とか「ピクト(特徴の漫画化)」のところは、実は「記号」よりも成長の前段階にあるので、4歳児ぐらいまでには最適だと思っています。ある程度、ひらがなが「ひらがな」であることを理解して、「読む」という行為と別のものに置き換えて「理解する」という行為ができると、デザイン的な記号がわかってくるのですが、それ以前は、それが「記号」かどうかすらわからないのです。なので、子供向けには、下手なデザインをするよりも、「色」なり「ピクト」なりが重要かなと。

このあたり、もう別の日にもうちょっと続けます。

カテゴリー: windows 8 | [win8] パンダ福笑いのインターフェースを考える はコメントを受け付けていません

12/1 に八王子で「フューチャーワークス カンファレンスを開催」

直前の告知…と云いますか、準備不足と言いますか。時間の都合のつく方は是非。

13:00 から受付開始、13:30 からスタートという具合です。

講演もそうなんですが、リンクスさん提供のデモ機で実演するのと、その後の懇親会が主目的だったりします。日経BPさん、秀和システムさん、コルセアさんにプレゼントなど頂きましたので、それだけ欲しいッ!!! って物欲の方も歓迎です。もちろん、iPad mini のプレゼン目当てでも ok。

では、明日、八王子の「日本工学院八王子専門学校」にて。アクセスマップなんかは↓からどうぞ。

PP-Club Future work Conference 会場までのアクセス
http://musty2.hac.neec.ac.jp/events/ppc1_access/access.html

カテゴリー: 勉強会 | 12/1 に八王子で「フューチャーワークス カンファレンスを開催」 はコメントを受け付けていません

[win8] QX エディタを windows 8 のスタート画面に追加する方法

何も、QX Editor に限らないのですが、普通のデスクトップアプリのツール(インストーラがなくて、Program Files フォルダに入らないツール)は、Win キーの検索では引っかからないので、ならば、スタート画面にショートカットを置けばよいのだ、ということに気づいた、という話。

[win8] Windows 8 でアプリの検索 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3897

これの応用技です。

まず、QX Editor のショートカットを作ります。私の場合、c:\tools\newqx フォルダにおいてあるので、アプリの検索に引っかからないんですよね。

image

右クリックして「スタート画面にピン留め」を選択します。

image

こんな風にスタート画面に

image

ここで、アイコンをクリックしてもいいのですが、面倒なので、Win キーを押したあとに、いきなり「qx」と打てば、

image

と検索にマッチしたら「Enter」キーで実行という具合。

image

なるほど、これだとわざわざデスクトップに戻らなくてもいいし、他の名前でマッチングさせるのも簡単です。たとえば「リモートデスクトップ接続」って、「remote」では検索ができなくて「リモート」とカタカナで打たないと検索にマッチングしないのですが、「remote desktop」とかいう名前でショートカットを作って、スタート画面に置いておけばアプリの検索にマッチングさせることができる訳です。

カテゴリー: windows 8 | [win8] QX エディタを windows 8 のスタート画面に追加する方法 はコメントを受け付けていません