Community Open Day 2013 実況ライブコーディング
C#でクロスプラットフォーム開発 Windows から Android まで
2013年5月11日、全国の技術コミュニティが集まって共同開催された大規模勉強会 Community Open Day 2013 が各地で開催されました。私、赤坂玲音も日本マイクロソフト本社の東京会場にて、「日本 C# ユーザー会」の枠で、岩永さんと一緒に登壇させていただきました。
ufcpp こと岩永さんと言えば「++C++;// 未確認飛行 C」の中の人で、C# (または .NET Framework)関連のキーワードで検索したことがあれば、一度でもお世話になったことがあるでしょう。Visual C# の Microsoft MVP であり、日本の C# コミュニティに大きく貢献されている第一人者です。そんな岩永さんが実況解説をしながら、赤坂がライブコーディングするセッション、その名も「実況ライブコーディング」を行いました。
昨年の Community Open Day では、Aiming(当時)の細田さんが Unity を使ったライブコーディングを岩永さんと行っており、日本 C# ユーザー会としては 2 回目となります。現在、両氏とは同じ職場でお世話になっており、何とも業界は狭いものであります。
東京会場では、我々のセッションが唯一の上級者向けとなっていました。ライブコーディングでは細かいコードの解説や技術紹介は行われないため、コードを読めない人や未経験者には難解なためですが、多少なりのプログラミング経験があれば、細部はともかく何をしているかは掴めるはずです。
今年のライブコーディングでは、C# が多様な環境でビルド可能なクロスプラットフォーム性の高い言語であることを証明するため、わずか 50 分という時間の中でコードを再利用しながら、同じアプリケーションを WPF(Windows デスクトップ)、Windows ストアアプリ、ASP.NET MVC(Web アプリケーション)、そして Xamarin を用いた Android で実行するというものです。作成するプログラムは、ソーシャルゲームでよくある、ガチャガチャの類いで、ランダムに引いたカードを画面に表示させるというものです。
岩永さんが解説しつつ実況しているので、赤坂はほとんど話していませんが、画面を操作して一生懸命コードを書いているのは赤坂でございます。
このライブコーディングにあたって、何を作るかイメージを固めるために簡単に一度だけ実装しましたが、それ以外は用意や練習はしておらず、岩永さんとも本番の1時間ほど前に軽く確認の打ち合わせをした程度(時間の計測などは一度もしていない)でした。用意されたシナリオではなく、実際の開発のように正しく動作せずグダグダするようなことも含めてライブコーディングの楽しいところ、という岩永さんの考えでしたが、書いている自分は時間を見てはハラハラしていました。何とか、最後までお見せできてよかったです。
以下、会場では駆け足で書いたコードと解説です。本当は手直ししたいところもあるのですが、ここはあえて会場で書いたコードをできるだけそのままに掲載いたします。
モデル層
時間が限られているので、カードにはシンプルなテスト用のデータを用いながら、データの取得にはインターフェイスを介して本番用のコードを容易に切り替えられるように設計しています。モデル側は、単にカードのコレクションからランダムで引いてくるだけですが、処理が複雑になるほど UI 層から分離することが重要になります。
どうしても目に見える部分からコードを考えてしまうため、多くの開発者は思考がビューに引っ張られます。その結果、イベントハンドラなどビューに依存するコードビハインドに大量のロジックが含まれ、変更が難しいコードが生み出されます。正しくコードの依存関係を分離できる技術者であれば、今回のコーディングでお見せしたように、まずビューを含まない純粋なモデルから記述します。
namespace GachaModel { public class Card { public int CardId { get; set; } public string Name { get; set; } public int HitPoint { get; set; } public int Attack { get; set; } public int Defence { get; set; } } }
最初に書いたコードです。ガチャで引くカードそのものを表す Card クラスです。表示される画像のイメージは CardId に対応させるという仕様で、名前、ヒットポイント、攻撃力、防御力をプロパティで持ちます。実際のソーシャルゲームだと、もう少し複雑になり、レアリティや特殊能力、属性(アイドルマスターシンデレラガールズで例えるとキュート、クール、パッション的な)などが加わります。
あと、ソーシャルゲームの場合はカードにレベルがあり、カード合成で成長させるため、実際にはカードの大本の情報(マスター)と、ユーザーが保持しているインスタンスの情報を分ける必要があったりします。
こうしたカードのデータは、最終的な運用ではデータベースなどの別のサーバーに分散されます。しかし、開発(デバッグ)時はスタンドアロンで開発用のサンプルデータを使って実行する必要があります。この仕組みがないと単体テストができません。その後、本番環境に近いステージング環境でデバッグし、最終的に本番環境で動かします。このように、段階に応じて環境を切り替えるわけですが、可能な限りプログラムは変更したくはありません。
そこで、リポジトリパターンと呼ばれる、データー操作を行う処理をインターフェイスで分離する手法を用います。インターフェイスでデータを操作するメソッドのみを公開し、実際のデータ処理は実装に委ねます。
namespace GachaModel { public interface ICardRepository { Card Random(); Card Get(int id); } }
今回のライブコーディングではランダムでカードを引く処理があるだけなので、カードをランダムで取得する Random() メソッドと、カード ID でカードを取得する Get() メソッドだけを公開しています。今、読み返すと GetById() のような名前の方がよかったと思いますが、その辺はリファクタリングで後から書き直すことができるので、まずは、必要な機能をどんどん書いていきます。実際のデータ処理であれば、この他にデータを追加するメソッドや、データを編集するメソッド、データを削除するメソッド、条件によって検索するメソッドなどが必要になります。
続いて、この ICardRepository インターフェイスをデバッグ用のモックデータで実装する MockCardRepository クラスを作成しています。
using System; using System.Collections.Generic; using System.Linq; namespace GachaModel { public class MockCardRepository : ICardRepository { private List<Card> cards = new List<Card>() { new Card() { CardId = 1, Name="C++たん", HitPoint = 100, Attack = 300, Defence = 10 }, new Card() { CardId = 2, Name="C#たん", HitPoint = 300, Attack = 200, Defence = 200 },new Card() { CardId = 3, Name="F#たん", HitPoint = 150, Attack = 200, Defence = 400 },new Card() { CardId = 4, Name="VBたん", HitPoint = 500, Attack = 200, Defence = 100 }, }; private Random random = new Random(); public Card Random() { var i = random.Next(cards.Count); return cards[i]; } public Card Get(int id) { return cards.Where((c) => c.CardId == id). Select((c) => c).FirstOrDefault(); } } }
見ての通り、List クラスでカードのコレクションを保持しているだけです。デバッグや単体テストなどで外部のサービスやデータベースに接続するのは適切ではなく、このようなテスト用のデータを使う必要があります。今回はライブコーディングという限られた時間だったため、カードのデータもコードにベタ打ちしていますが、実際の開発では JSON などのテキストから読み込むと編集しやすく便利です。
コーディングを見ていただければ分かりますが、これらのモデルは最初に作ってから、以降は一切変更することなく最後まで使い回しています。
Windows デスクトップアプリケーション(WPF)
続いて、最初のアプリケーションとして WPF の XAML を書きます。もう少し Visual Studio のデザイナー機能をエレガントに使いこなしたいのですが、岩永さんもおっしゃっているように XAML を理解していると XAML コードで書いてしまうんですよね。
<Window x:Class="GachaForDesktop.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="50"/> </Grid.RowDefinitions> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Image Source="{Binding Image}" /> <Grid Grid.Column="1"> <Grid.RowDefinitions> <RowDefinition Height="40" /> <RowDefinition Height="40" /> <RowDefinition Height="40" /> <RowDefinition Height="40" /> </Grid.RowDefinitions> <TextBlock TextWrapping="Wrap" Text="{Binding Name}" FontSize="24"/> <TextBlock Grid.Row="1" TextWrapping="Wrap" FontSize="24" ><Run Text="HP:"/><InlineUIContainer> <TextBlock TextWrapping="Wrap" Text="{Binding HitPoint}"/> </InlineUIContainer></TextBlock> <TextBlock Grid.Row="2" TextWrapping="Wrap" FontSize="24"><Run Text="AT:"/><InlineUIContainer> <TextBlock TextWrapping="Wrap" Text="{Binding Attack}"/> </InlineUIContainer></TextBlock> <TextBlock Grid.Row="3" TextWrapping="Wrap" FontSize="24"><Run Text="DF:"/><InlineUIContainer> <TextBlock TextWrapping="Wrap" Text="{Binding Defence}"/> </InlineUIContainer></TextBlock> </Grid> </Grid> <Button Content="ガチャを引く" Grid.Row="1" Margin="10" Click="Button_Click" /> </Grid> </Window>
時間も限られているので、できるだけマウス操作だけでペタペタとコントロールを貼り付けた結果、カードの攻撃力や防御力を表示するテキスト部分がインラインになってしまいました。WPF 上では問題ないのですが、Windows ストアアプリでは使えないので、後の移植で問題になりました。
続いてビューモデルです。モデルは純粋なデータであり、ビューに依存できません。しかし、ビューとモデルの関係は必ずしも 1 対 1 ではなく、モデルからビューへの変換などが必要になります。そこで、モデルとビューを接続し、ビューに依存するデータを提供するものがビューモデルです。この場では、カード画像がモデル側に含まれていないので、ビューモデルで Image プロパティを追加しています。
using GachaModel; using System; using System.ComponentModel; namespace GachaForDesktop { public class CardView : INotifyPropertyChanged { public Card Card { get; private set; } public Uri Image { get; private set; } private void ImageValidate() { Image = new Uri( string.Format("Cards/{0:000}.jpg", Card.CardId), UriKind.Relative ); if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("Image"); PropertyChanged(this, e); } } public CardView(Card card) { Card = card; ImageValidate(); } public event PropertyChangedEventHandler PropertyChanged; public int CardId { get { return Card.CardId; } set { Card.CardId = value; ImageValidate(); if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("CardId"); PropertyChanged(this, e); } } } public string Name { get { return Card.Name; } set { Card.Name = value; if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("Name"); PropertyChanged(this, e); } } } public int HitPoint { get { return Card.HitPoint; } set { Card.HitPoint = value; if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("HitPoint"); PropertyChanged(this, e); } } } public int Attack { get { return Card.Attack; } set { Card.Attack = value; if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("Attack"); PropertyChanged(this, e); } } } public int Defence { get { return Card.Defence; } set { Card.Defence = value; if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("Defence"); PropertyChanged(this, e); } } } } }
ビューモデルの大部分は、元のモデルのラッパーでしかありません。ライブコーディングではコピペで作っていますが、岩永さんの解説のように T4 テンプレートなどを用いて自動コード生成すると、より効率的で間違いがありません。
最後に、「ガチャを引く」ボタンが押されたときの処理です。
using GachaModel; using System.Windows; namespace GachaForDesktop { public partial class MainWindow : Window { private ICardRepository cards = new MockCardRepository(); public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { var card = cards.Random(); DataContext = new CardView(card); } } }
ビューのコードビハインドに、ほとんどコードを書いていません。WPF を知らなければ、どうやって画面に反映させているのか不思議に思えるかもしれません。XAML の {Binding プロパティ名} となっている部分に注目してください。XAML にはデータバインディングの仕組みがあり、DataContext プロパティに設定したオブジェクトのプロパティが対応しています。
これらの機能のおかげで「ビューを薄く」することに成功しています。
Windows ストアアプリ
続いて Windows 8 の Windows ストアアプリを作成しています。C# と XAML で開発する限り、Windows ストアアプリは WPF と大きな違いはありません。まずは、WPF で開発したコード4の XAML コードをコピーします。
<Page x:Class="GachaForWinStore.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:GachaForWinStore" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="200"/> </Grid.RowDefinitions> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Image Source="{Binding Image}" /> <Grid Grid.Column="1"> <Grid.RowDefinitions> <RowDefinition Height="100" /> <RowDefinition Height="100" /> <RowDefinition Height="100" /> <RowDefinition Height="100" /> </Grid.RowDefinitions> <TextBlock TextWrapping="Wrap" Text="{Binding Name}" FontSize="40"/> <StackPanel Grid.Row="1" Orientation="Horizontal"> <TextBlock TextWrapping="Wrap" FontSize="40" Text="HP:" /> <TextBlock TextWrapping="Wrap" Text="{Binding HitPoint}"/> </StackPanel> <StackPanel Grid.Row="2" Orientation="Horizontal"> <TextBlock TextWrapping="Wrap" FontSize="40" Text="AT:" /> <TextBlock TextWrapping="Wrap" Text="{Binding Attack}"/> </StackPanel> <StackPanel Grid.Row="3" Orientation="Horizontal"> <TextBlock TextWrapping="Wrap" FontSize="40" Text="DF:" /> <TextBlock TextWrapping="Wrap" Text="{Binding Defence}"/> </StackPanel> </Grid> </Grid> <Button Content="ガチャを引く" FontSize="40" Grid.Row="1" Margin="10" Click="Button_Click" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" /> </Grid> </Page>
Windows ストアアプリの XAML は、WPF のインラインテキストなどリッチな表現に対応していないため、単純にコピーしただけではエラーが発生していました。そのため、上記のようにパラメータを表示するテキスト部分を StackPanel で表示するように書き換えています。最初から、このように作っておけば、この手間は避けられました。
あと、動画ではフォントサイズなどをコピー&ペーストで書き換えていますが、これも本来ならばスタイルを使った方がスマートでした。その結果、パラメータを表示するテキストのフォントサイズを設定し忘れ、数値が小さく表示されてしまいました。
続いてビューモデル。WPF と大きな違いはありませんが、Windows ストアアプリの XAML ではイメージのバインディングに Uri を使えないため、Uri ではなく ImaneSource を返すように変更しています。
using GachaModel; using System; using System.ComponentModel; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Imaging; namespace GachaForWinStore { public class CardView : INotifyPropertyChanged { public Card Card { get; private set; } public ImageSource Image { get; private set; } private void ImageValidate() { Image = new BitmapImage( new Uri( string.Format("ms-appx:///Cards/{0:000}.jpg", Card.CardId), UriKind.Absolute )); if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("Image"); PropertyChanged(this, e); } } public CardView(Card card) { Card = card; ImageValidate(); } public event PropertyChangedEventHandler PropertyChanged; public int CardId { get { return Card.CardId; } set { Card.CardId = value; ImageValidate(); if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("CardId"); PropertyChanged(this, e); } } } public string Name { get { return Card.Name; } set { Card.Name = value; if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("Name"); PropertyChanged(this, e); } } } public int HitPoint { get { return Card.HitPoint; } set { Card.HitPoint = value; if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("HitPoint"); PropertyChanged(this, e); } } } public int Attack { get { return Card.Attack; } set { Card.Attack = value; if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("Attack"); PropertyChanged(this, e); } } } public int Defence { get { return Card.Defence; } set { Card.Defence = value; if (PropertyChanged != null) { var e = new PropertyChangedEventArgs("Defence"); PropertyChanged(this, e); } } } } }
最後にビューのコードビハインドでボタンが押されたときの処理を書いています。ここは WPF と完全に同じです。
using GachaModel; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; namespace GachaForWinStore { public sealed partial class MainPage : Page { private ICardRepository cards = new MockCardRepository(); public MainPage() { this.InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { var card = cards.Random(); DataContext = new CardView(card); } } }
コードの大部分をコピーするだけで WPF から Windows ストアアプリに移植できることが確認できます。完全な互換ではないため部分的な変更は必要ですが、コードを再利用できるため実際に記述した量は少ないです。
Web アプリ(ASP.NET MVC)
これまでは Windows 上で動作するアプリケーションでしたが、次は HTML (ブラウザ)で動作する Web アプリケーションにします。.NET Framework で Web アプリケーションを開発する場合、Windows に付属している Web サーバーの IIS (Internet Information Services)で動かす ASP.NET を使います。今回は、比較的新しいフレームワークである ASP.NET MVC を用いて開発しています。
ASP.NET MVC では、URI がコントローラーに対応しており、コントローラーがビューを返すという設計になっています。モデルはすでに構築してあるので、まずはコントローラーから作成しています。
using GachaModel; using System.Web.Mvc; namespace GachaWeb.Controllers { public class CardController : Controller { private ICardRepository cards = new MockCardRepository(); public ActionResult Index() { return View(); } public ActionResult View(int id) { var card = cards.Get(id); ViewBag.Image = string.Format("/Cards/{0:000}.jpg", card.CardId); return View(card); } public ActionResult Gacha() { var card = cards.Random(); return Redirect("~/Card/View/" + card.CardId); } } }
コントローラーに含まれている ActionResult クラスを返す公開メソッドをアクションと呼び、コントローラー名とアクション名が URI に対応します。
この例では、Index アクションが入り口のページで「ガチャを引く」リンクを表示し、Gacha アクションがランダムでカードを抽出し、View アクションにリダイレクトさせています。従って、画面に表示されるページは Index ビューと View ビューの 2 つです。
@{ ViewBag.Title = "Index"; } <h2>Index</h2> <div> <a href="Card/Gacha">ガチャを引く</a> </div>
Index アクションのビューは、単に Gacha アクションを呼び出すリンクを持ったページを返すだけです。Gacha アクションは、ランダムで引いたカードの ID と共に View アクションにリダイレクトします。View アクションは、パラメータから受け取った ID からカードを選択し、そのカードの情報を表示します。
@{ ViewBag.Title = "View"; } <h2>@Model.Name</h2> <div> <img src="@ViewBag.Image" /> </div> <div> HP:@Model.HitPoint </div> <div> HP:@Model.Attack </div> <div> HP:@Model.Defence </div>
Gacha アクションが実行されてカードが選択されると、View アクションをリダイレクトしてカードを表示します。
振り返ってみると View というアクション名は、ビューという名前が汎用的すぎて良くないですね。そもそも Index アクションと分ける必要が無く、単に Index アクションに null 許容の int? id パラメータを持たせれば、シンプルに書けたと思います。例えば、コントローラーは以下のような形の方が良いでしょう。
public ActionResult Index(int? id) { if (id == null) return View(); var card = cards.Get(id.Value); ViewBag.Image = string.Format("/Cards/{0:000}.jpg", card.CardId); return View(card); } public ActionResult Gacha() { var card = cards.Random(); return Redirect("~/Card/Index/" + card.CardId); }
こうすれば、ビューも 1 つで済みます。
@{ if (Model == null) { ViewBag.Title = "Index"; } else { ViewBag.Title = Model.Name; } } <h2>@ViewBag.Title</h2> @if(Model != null) { <div> <img src="@ViewBag.Image" /> </div> <div> HP:@Model.HitPoint </div> <div> HP:@Model.Attack </div> <div> HP:@Model.Defence </div> } <div> <a href="/Card/Gacha">ガチャを引く</a> </div>
これで、連続でガチャが引けるようになりますね。
Android アプリ
最後に、Android アプリで同じものを作ります。C# と .NET Framework は Microsoft 環境のためだけの技術だと誤解をしている人も多いのですが、仕様はオープンになっており、国際標準化もされています。オープンソースで .NET Framework 互換の Mono と呼ばれる環境が開発されており、Mono をベースに Windows 以外の多様な環境で C# を利用できます。
ライブコーディングでは、C# で iOS と Android のアプリケーションを開発できる Xamarin Studio を用いています。Xamarin については、こちらの記事でも解説しています。ライセンスを購入すれば Visual Studio で開発できますが、このライブコーディングでは無償のライセンスでできる範囲で、Xamarin Studio だけを使って開発しています。
Xamarin Studio で Android アプリを開発する場合、Xamarin Studio に含まれている UI デザイナを用います。プロジェクトを生成した状態で Main.axml というファイルが含まれており、これが起動時のアクティビティで表示されます。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/myButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> <ImageView android:src="@android:drawable/ic_menu_gallery" android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/imageView1" /> </LinearLayout>
この時点で残された時間が少なかったため、攻撃力などカードのパラメータを表示するテキストは省略し ImageView だけを追加しています。
そして、上の画面を読み込んで表示する Activity を書きます。WPF のようなリッチなデータバインディングの仕組みが無いため、ランダムで引いたカードから画像データを選択し、ImageView オブジェクトに設定しています。画像はファイル名から読み込む必要があるため、Drawable ではなく Asset に追加しています。
using System; using Android.App; using Android.OS; using Android.Widget; using GachaModel; namespace Gacha { [Activity (Label = "Gacha", MainLauncher = true)] public class Activity1 : Activity { private ICardRepository cards = new MockCardRepository(); protected override void OnCreate (Bundle bundle) { base.OnCreate (bundle); SetContentView (Resource.Layout.Main); Button button = FindViewById<Button> (Resource.Id.myButton); var imageView = FindViewById<ImageView> (Resource.Id.imageView1); button.Click += delegate { var card = cards.Random(); var fileName = string.Format ("{0:000}.jpg", card.CardId); var stream = Assets.Open (fileName); var image = Android.Graphics.BitmapFactory.DecodeStream(stream); imageView.SetImageBitmap(image); }; } } }
ボタンを押すとランダムでカードが変化します。ここでも、冒頭で作成した Card クラスや MockCardRepository クラスを、変更することなく再利用できています。ビューに依存しない純粋なモデルを抽出することで、ビューやプラットフォームの変更に強くなります。
雑感
実際に書き起こして振り返ると、想像していたよりもずっとコード量が多くて自分で驚きました。当然、Visual Studio の入力補完が非常に優秀なので、実際にキー入力している量はこの半分以下でしょう。動画を見ても、開発生産性に IDE が深く関わっているということが理解できると思います。
C# と Visual Studio に慣れていれば、このように 50 分という短時間で、実際に動くコードをこれだけ作ることができます。もちろん、実際の開発では設計や命名などに、より多くの時間をかけるため、このようにスラスラと書けるわけではありませんが、C# はリファクタリングも簡単なので、とりあえず書いて動かしながら気になるところを書き換えていくスタイルがお勧めです。