- 投稿日:2019-02-15T23:41:54+09:00
[Unity] 重力を変更する
1.ゲーム全体の重力を変更する方法
ゲーム全体の重力を変更するには、
Edit→Project Settings→Physicsを開いて、InspectorのPhysicsManagerのGravityのVector3の値を変更するだけです!
デフォルトではy軸に-9.81 m/s^2 の力がかかっていますね。地球と同じです。2.特定のオブジェクトのみ重力を変更する方法
特定のオブジェクトに違う重力をかけたい場合は、
以下のコード(ChangeGravity.cs)をオブジェクトにアタッチして、using System.Collections; using System.Collections.Generic; using UnityEngine; public class ChangeGravity : MonoBehaviour { [SerializeField] private Vector3 localGravity; private Rigidbody rBody; // Use this for initialization private void Start () { rBody = this.GetComponent<Rigidbody>(); rBody.useGravity = false; //最初にrigidBodyの重力を使わなくする } private void FixedUpdate () { setLocalGravity (); //重力をAddForceでかけるメソッドを呼ぶ。FixedUpdateが好ましい。 } private void setLocalGravity() { rBody.AddForce (localGravity, ForceMode.Acceleration); } }Inspectorから好きなVector3を入力してあげればOKです。
以上、重力を変更する方法でした。
- 投稿日:2019-02-15T23:37:51+09:00
ニート翔ぶ~C#でドローンを飛ばす~
昨今、退職エントリーが流行っているので、昨年、勢いで会社を辞めてニートになった記念に何か書こうと思います。
おっさんは明るい未来に羽ばたくことはできませんでしたので、せめて、ドローンぐらいは明るい未来を羽ばたかせてみせよう、そう思ってこの記事を書いてみました。Telloとは
Telloは小型のドローンでカメラもついており、Android,iPadといった携帯端末で操作が可能です。
https://www.ryzerobotics.com/jp/telloこの度は無収入のくせに以下のセットを購入しました。
https://www.amazon.co.jp/gp/product/B07979Q4YS注意事項:
・充電用のUSBはついてこないので自前でそろえる必要があります。
機体にささないと充電できません。ただし別売りのバッテリーケースを購入すれば機体にささずに充電が可能のようですが、おっさんは無職なので購入してまでの検証はしてません。・ハードウェアの性能としてはカメラがついているので動画撮影が可能です。つまり、住宅地で飛ばすと覗きとまちがわれるので気をつけましょう。おっさんは無職なのでポリス沙汰になると無職で全国デビューになるので細心の注意しないといけません。
・wifiでつなげて機体の操作をする必要があるため、操作側のリモコンはLANカードが2枚差しでないとインターネットにつなげながらの操作は行えません。おっさんはノートPCを10年ぶりくらいに有線のLANにつなげて作業しました。
・羽に指が当たると、そこそこ痛いので、慣れない間は軍手をして操作したほうがいいです。たぶん、大型のドローンの羽だったら、ドローンのかわりに指が飛んでいたと思います。
・可能なら外の広いところで運転した方が安全です。おっさんは引きこもりなので家でやりましたが、5回ほど壁にあたり墜落しました。
Tello SDK
TelloはSDKが提供されており、UDP経由で以下のことが行えます。
・機体の操作。
・機体の情報取得(傾きとか温度とかバッテリー情報)
・カメラからの撮影情報の取得UDPなので基本的に無線LANがつながればどんなプラットフォームでも動作させることができますが、検索してでてくるMacのPythonかC/C++でやった方が絶対にいいです。
ジャイアントロボのように音声で操作しようと思って、音声認識が簡単にできる.NETで始めたら、えらい苦労しました。また、SDKではなくて、バイナリデータを送信してSDKに書かれていない操作もできるようですが、ここでは割愛します。
Tello SDK 1.3.0.0
以下はTelloSDK1.3.0.0をそれっぽく翻訳したものです。
1. 概要
Tello SDKはWi-Fi UDPポートを介して航空機に接続し、ユーザーはテキストコマンドでドローンを制御することができます。 Tello3.pyファイルをダウンロードするにはここをクリックしてください。
2. アーキテクチャ
Wi-Fiを使用してTelloとPC、Mac、またはモバイルデバイスとの間の通信を確立します。
コマンドの送信と応答の受信
Tello IP: 192.168.10.1 UDP PORT:8889 <<-->> PC/Mac/Mobile
注意1:同じポートを介してTelloとメッセージを送受信するように、PC、Mac、またはモバイルデバイスでUDPクライアントを設定します。注意2:他のコマンドを送信する前に、"command"コマンドをUDP ポート8889を介してTelloに送信してTelloのSDKモードを開始します。
Telloステータスの受信
Tello IP: 192.168.10.1 ->> PC/Mac/Mobile UDP Server: 0.0.0.0 UDP PORT:8890
注意3:PC、Mac、またはモバイルデバイスにUDPサーバーをセットアップし、UDP PORT 8890を介してIP 0.0.0.0からのメッセージを聞きます。まだ行っていない場合は、注意2を実行して状態データの受信を開始してください。**
Telloビデオストリームの受信
Tello IP: 192.168.10.1 ->> PC/Mac/Mobile UDP Server:0.0.0.0 UDP PORT:11111
注意4:PC、Mac、またはモバイルデバイスにUDPサーバーをセットアップし、UDP PORT 11111を介してIP 0.0.0.0からメッセージを受信します。
注意5:もし実行していないなら注意2を実行してください。その後、UDP PORT 8889を介して「streamon」コマンドをTelloに送信してストリーミングを開始します。3. TELLOコマンドの種類と結果
このSDKには3つの基本的なコマンドタイプが含まれています。
コントロールコマンド(xxx)
・コマンドの実行が成功した場合、"ok"が戻ります
・成功しなかった場合、"error"または有益な結果コードが戻りますリードコマンド(xxx?)
・サブパラメータの現在値を戻します。設定コマンド(xxx a)は、新しいサブパラメータ値を設定しようとします
・コマンドの実行が成功した場合、"ok"が戻ります
・成功しなかった場合、"error"または有益な結果コードが戻ります4. Tello コマンド
コントロールコマンド
コマンド 説明 考えられる応答 command SDKモードに入る ok
errortakeoff Telloが自動で離陸する ok
errorland Telloが自動で着陸する ok
errorstreamon ビデオストリームをON ok
errorstreamoff ビデオストリームをOFF ok
erroremergency 全てのモータを停止 ok
errorup x Tello が x cm上昇
x:20-500ok
errordown x Tello が x cm下降
x:20-500ok
errorleft x Tello が x cm左へ
x:20-500ok
errorright x Tello が x cm右へ
x:20-500ok
errorforward x Tello が x cm前進
x:20-500ok
errorback x Tello が x cm後退
x:20-500ok
errorcw x Tello が 時計回りに x度 回転
x:1-3600ok
errorccw x Tello が 反時計回りに x度 回転
x:1-3600ok
errorflip x Tello が x 方向に宙返りをする
l:(left)
r(right)
f(foward)
b(back)ok
errorgo x y z speed Tello が x y z の方向へ speed(cm/s)の速度で飛ぶ x:20-500 ※訳注 前進する xで後退
y:20-500 ※訳注 左へ -y で右へ
z:20-500
speed:10-100curve x1 y1 z1 x2 y2 z2 speed Telloが現在位置とspeed(cm/s)とともに定義された2つの座標を曲線状に飛びます。もし円弧の半径が0.5-10meterの範囲外の場合、レスポンスはfalseとなります。
x1, x2: 20-500
y1, y2: 20-500
z1, z2: 20-500
speed: 10-60
x/y/z は同時に-20~20の間にはできませんok
error設定コマンド
コマンド 説明 考えられる応答 speed x 速度 x cm/sを設定する
x: 10-100ok
errorrc a b c d 4つのチャネルを通してRCコントロールを送信する
a: left/right (-100~100)
b: forward/backward (-100~100)
c: up/down (-100~100)
d: yaw (-100~100)ok
errorwifi ssid pass Wi-FiのSSIDとpasswordを設定する ok
errorリードコマンド
コマンド 説明 考えられる応答 speed? 現在の速度(cm/s)を取得 x: 1-100 battery? 現在のバッテリーのパーセンテージを取得 x: 1-100 time? 現在の飛行時間を取得 time height? 現在の高さ(cm)を取得 x: 0-3000 temp? 現在の温度(℃)を取得 x : 0-90 attitude? IMU(慣性計測装置) の姿勢情報を取得 pitch roll yaw baro? バロメータ(気圧計)の値(m)を取得 x acceleration? IMU角加速度データを取得する(0.001g) x y z tof? TOFからの距離(cm)を取得する x:30-1000 wifi? Wi-FiのSNRを取得する snr 訳者注
pitch roll yaw:参考:https://algorithm.joho.info/robotics/roll-pitch-yaw-matrix/
Tof:Time of Flightのことと思われる
SNR:信号対雑音比。SN比が高ければ伝送における雑音の影響が小さく、SN比が小さければ影響が大きい。5. Telloステータス
データ型:String
Example:
“pitch:%d;roll:%d;yaw:%d;vgx:%d;vgy%d;vgz:%d;templ:%d;temph:%d;tof:%d;h:%d;bat:%d;baro: %.2f; time:%d;agx:%.2f;agy:%.2f;agz:%.2f;\r\n”説明
o pitch: ピッチ角
o roll: ロール角
o yaw: ヨー角
o vgx: Speed x,
o vgy: Speed y,
o vgz: Speed z,
o templ: 最も低い温度, 摂氏℃
o temph: 最も高い温度、摂氏℃
o tof: TOF distance, cm
o h: Height, cm
o bat: 現在のバッテリーのパーセンテージ, %
o baro: バロメーター測定, cm
o time: モータの時間,
o agx: 加速度x,
o agy: 加速度y,
o agz: 加速度z,6 安全機能
もしTelloが15秒間なにもコマンドを受信しなければ、自動で着陸をします
7 TelloのWi-fiリセット
電源ON状態のTelloに5秒間の長押しをするとインジケータライトが消えて黄色に点滅します。 インジケータランプが黄色のライトを点滅させると、Wi-Fi SSIDとパスワードは工場出荷時の設定にリセットされ、デフォルトではパスワードは設定されません。
.NETで操作してみる。
おとなしく、Mac+Pythonで動かした方がいいです。やっている人がいっぱいいます。
それでもやるなら、以下を参考にしてみてください。事前準備
・ステータス取得のための8890とビデオストリーム取得のためのポート11111を開けておきます。
つながらない場合は、アプリケーション固有のファイアウォールの設定も確認してください。
Telloとのネットワークはパブリックのネットワークになっているはずなので、パブリックの設定もちゃんとみましょう(2敗)
・WireShark等でネットワークの電文をみれるようにしておきます。
https://www.wireshark.org/
問題の切り分けにやくに立ちます。・ffmpegを用意する。
Telloからのビデオ情報を表示するのに使用します。
また、自前でデコードする場合もffmpegのAPIを使用しないと厳しいです。
https://www.ffmpeg.org/簡単なTelloプログラミング
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Net;//for UDP using System.Net.Sockets; //for UDP using System.Threading;//for Interlocked using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; namespace TelloSample { public partial class Form1 : Form { private UdpClient udpForCmd; //コマンド結果受信用クライアント private UdpClient udpForStsRecv; //ステータスの結果受信用クライアント public Form1() { InitializeComponent(); } // コマンドの結果更新用 private delegate void DelegateUpdateCmdResult(String ret); // ステータスの更新用 private delegate void DelegateUpdateSts(String sts); // コマンド結果を更新。ワーカスレッドからの場合はメインスレッドで実行 private void UpdateCmdResult(String ret) { if (this.InvokeRequired) { Object[] param = new Object[1] { ret }; this.Invoke(new DelegateUpdateCmdResult(this.UpdateCmdResult), param); return; } this.txtRet.Text = ret; this.btnCmd.Enabled = true; } // ステータスを更新。ワーカスレッドからの場合はメインスレッドで実行 private void UpdateSts(String sts) { if (this.InvokeRequired) { Object[] param = new Object[1] { sts }; this.Invoke(new DelegateUpdateSts(this.UpdateSts), param); return; } this.txtSts.Text = sts; } // Telloとの通信を設定する private void SetupTello() { this.udpForCmd = new UdpClient(0); this.udpForStsRecv = new UdpClient(8890); // コマンド結果の受信処理 Task.Run(() => { IPEndPoint remoteEP = null;//任意の送信元からのデータを受信 while (true) { try { String rcvMsg = ""; byte[] rcvBytes = udpForCmd.Receive(ref remoteEP); Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes)); this.UpdateCmdResult(rcvMsg); } catch (Exception ex) { Debug.WriteLine(ex.Message); } } }); // ステータスの受信処理 Task.Run(() => { IPEndPoint remoteEP = null;//任意の送信元からのデータを受信 while (true) { try { String rcvMsg = ""; byte[] rcvBytes = udpForStsRecv.Receive(ref remoteEP); Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes)); rcvMsg = rcvMsg.Replace(";", "\r\n"); this.UpdateSts(rcvMsg); } catch (Exception ex) { Debug.WriteLine(ex.Message); } } }); // コマンド送信 private void sendCmd(string cmd) { byte[] data = Encoding.ASCII.GetBytes(cmd); this.udpForCmd.Send(data, data.Length, "192.168.10.1", 8889); } // 開始ボタン private void btnStart_Click(object sender, EventArgs e) { SetupTello(); this.txtRet.Text = ""; this.btnCmd.Enabled = false; sendCmd("command"); } // コマンド送信ボタン押下 private void btnCmd_Click(object sender, EventArgs e) { this.txtRet.Text = ""; this.btnCmd.Enabled = false; sendCmd(this.txtCmd.Text); } } }ビデオについて
streamon コマンドを送信するとポート11111にビデオの情報が受信できます。
これを表示するにはffmpegのffplayを使用するといいでしょう。ffplay -probesize 32 -sync ext udp://127.0.0.1:11111ウィンドウが起動して現在のカメラが表示されます。
よくある問題
・コマンドを受け付けない
無線LANでつながっているかを確認する。
充電されているか確認する。USBさして青ランプが点灯されたらフル充電である。
WireSharkでパケットの送受信がされているか確認する。
送受信のポートが開いているか確認。規定値だとパブリックネットワークなので注意。・カメラが受信できない。
11111ポートが開いているか見直す。
WireSharkでパケットが届いているか確認する。・ステータスが受信できない。
8890ポートが開いているか見直す。・たまにコマンドの応答結果がとれない。
UDPなので仕様だと思われます。okが必ずくるという前提は多分まずいかもです。.NETで自力でビデオデータを取り扱いたい。
OpenCVSharpを使えば簡単にできます・・・(震え)
・OpenCVSharp
OpenCvSharp3-AnyCPUとOpenCvSharp4.runtime.winをNuGetで取得していました。using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using OpenCvSharp; namespace OpenCVVideo { class Program { static void Main(string[] args) { var camera = VideoCapture.FromFile("udp://127.0.0.1:11111"); using (var normalWindow = new Window("normal")) { var normalFrame = new Mat(); var srFrame = new Mat(); while (true) { camera.Read(normalFrame); if (normalFrame.Empty()) break; normalWindow.ShowImage(normalFrame); int key = Cv2.WaitKey(100); if (key == 27) break; // ESC キーで閉じる } } } } }OpenCVのVideoCapture.FromFileはファイルといいつつ、UDPからのストリームもとれます。
まちがっても自分でUDPで11111ポートを監視してデコードしようとしてはいけません。
以下にその愚かな例をしめしますが、おとなしくOpenCVを使いましょう。愚かにも自前でUDPの11111ポートを監視した例
簡単な流れとしては以下の通りになります。
- streamonコマンドを送信
- ポート11111を監視
- 1460バイトとどいている間は後続のパケットがあるので受信しつづける。1460以外のデータ長がきたら、いままできたぶんとまとめてffmpegのAPIを使用してデコードする。
- デコード結果をffmpegの関数を使用してRGBに変換する
- OpenCVにかませるため、OpenCVの関数をつかってBGRに変換する
- OpenCVでビデオつくったりする。
この挙動は下記を参考にしました。
https://github.com/dji-sdk/Tello-Python/blob/master/Tello_Video/tello.py
https://github.com/dji-sdk/Tello-Python/tree/master/Tello_Video/h264decoder.NETでつらいのは3と4です。
これをおこなうにはネイティブのDLLでffmpegのAPIを使い実行した結果をC#に渡す必要があります。.NETでTelloのビデオを扱うために必要なライブラリ
・ffmpeg
https://ffmpeg.zeranoe.com/builds/
devにincludeファイルとlibファイル、sharedにdllがあるのでそれぞれダウンロードしました。
このライブラリはtelloから取得したh264形式をRGBに変換するために使用します。
ソースコードから自前でコンパイルもできますが、MSYS2をいれたりして結構、手間がかかるのでFormアプリでいいなら、おとなしく配布されているものを使った方がいいと思います。(一敗)h264decorderの移植
先に紹介したPythonでh264のデコードをするためのコードを.NETでやるために改造しました。
https://github.com/mima3/Tello/tree/master/h264decoderおそらく、メモリ解放処理がうまくできていない気がするので参考程度にしてください。
h264decoderの使用
.NETでネイティブのDLLを使用する場合は、32bitか64bitかは意識してください。
今回は64bitで動かすためにAnyCPUをx64に変更するか、32bitを優先するフラグをオフにする必要があります。
もし、今まで動いていたアプリケーションが起動すらしない場合、DLLが32bitと64bitで混在している可能性を疑ってみてください。以下に移植したh264decoderを使用して動画を作成する実装例を記載します。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Net;//for UDP using System.Net.Sockets; //for UDP using System.Threading;//for Interlocked using System.Diagnostics; using OpenCvSharp; using System.IO; using System.Runtime.InteropServices; namespace TelloSample { public partial class Form1 : Form { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct H264DecoderResult { public int w; public int h; public int size; public IntPtr buff; } [DllImport("h264decoder.dll", EntryPoint = "InitH264Decoder")] static extern void _InitH264Decoder(); [DllImport("h264decoder.dll", EntryPoint = "TermH264Decoder")] static extern void _TermH264Decoder(); [DllImport("h264decoder.dll", EntryPoint = "DecodeH264")] static extern bool _DecodeH264(IntPtr buff, int size, ref H264DecoderResult outbuff); [DllImport("h264decoder.dll", EntryPoint = "GetH264DecoderLastError")] public static extern IntPtr GetH264DecoderLastError(); [DllImport("h264decoder.dll", EntryPoint = "FreeData")] public static extern void FreeData(IntPtr data); [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); private UdpClient udpForCmd; //コマンド結果受信用クライアント private UdpClient udpForStsRecv; //ステータスの結果受信用クライアント private UdpClient udpForVideo; //ビデオストリームの受信用 public Form1() { InitializeComponent(); } // コマンドの結果更新用 private delegate void DelegateUpdateCmdResult(String ret); // ステータスの更新用 private delegate void DelegateUpdateSts(String sts); // コマンド結果を更新。ワーカスレッドからの場合はメインスレッドで実行 private void UpdateCmdResult(String ret) { if (this.InvokeRequired) { Object[] param = new Object[1] { ret }; this.Invoke(new DelegateUpdateCmdResult(this.UpdateCmdResult), param); return; } this.txtRet.Text = ret; this.btnCmd.Enabled = true; } // ステータスを更新。ワーカスレッドからの場合はメインスレッドで実行 private void UpdateSts(String sts) { if (this.InvokeRequired) { Object[] param = new Object[1] { sts }; this.Invoke(new DelegateUpdateSts(this.UpdateSts), param); return; } this.txtSts.Text = sts; } // Telloとの通信を設定する private void SetupTello() { this.udpForCmd = new UdpClient(0); this.udpForStsRecv = new UdpClient(8890); this.udpForVideo = new UdpClient(11111); // コマンド結果の受信処理 Task.Run(() => { IPEndPoint remoteEP = null;//任意の送信元からのデータを受信 while (true) { try { String rcvMsg = ""; byte[] rcvBytes = udpForCmd.Receive(ref remoteEP); Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes)); this.UpdateCmdResult(rcvMsg); } catch (Exception ex) { Debug.WriteLine(ex.Message); } } }); // ステータスの受信処理 Task.Run(() => { IPEndPoint remoteEP = null;//任意の送信元からのデータを受信 while (true) { try { String rcvMsg = ""; byte[] rcvBytes = udpForStsRecv.Receive(ref remoteEP); Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes)); rcvMsg = rcvMsg.Replace(";", "\r\n"); this.UpdateSts(rcvMsg); } catch (Exception ex) { Debug.WriteLine(ex.Message); } } }); // ビデオストリームの受信処理 Task.Run(() => { IPEndPoint remoteEP = null;//任意の送信元からのデータを受信 byte[] packetData = new byte[0]; int cnt = 0; _InitH264Decoder(); var fourcc = VideoWriter.FourCC('m', 'p', '4', 'v'); var video = new VideoWriter("test.mp4", fourcc, 20, new OpenCvSharp.Size(960, 720) ); while (true) { try { byte[] rcvBytes = udpForVideo.Receive(ref remoteEP); int l = packetData.Length; Array.Resize<byte>(ref packetData, l + rcvBytes.Length); Array.Copy(rcvBytes, 0, packetData, l, rcvBytes.Length); if (rcvBytes.Length != 1460) { int size = Marshal.SizeOf(packetData[0]) * packetData.Length; IntPtr inPtr = Marshal.AllocHGlobal(size); Marshal.Copy(packetData, 0, inPtr, packetData.Length); H264DecoderResult decret = new H264DecoderResult(); Debug.WriteLine("DO DECODE"); if (_DecodeH264(inPtr, packetData.Length, ref decret)) { Debug.WriteLine("DO DECODE,,,,ok"); var mat = new Mat(decret.h, decret.w, MatType.CV_8UC3); CopyMemory(mat.Data, decret.buff, (uint)decret.size); var matCv = new Mat(); Cv2.CvtColor(mat, matCv, ColorConversionCodes.RGB2BGR); video.Write(matCv); FreeData(decret.buff); } else { Debug.Write(Marshal.PtrToStringAnsi(GetH264DecoderLastError())); } Marshal.FreeHGlobal(inPtr); packetData = new byte[0]; ++cnt; } } catch (Exception ex) { Debug.WriteLine(ex.Message); } } // 後片付けの方法はあとで考える。(呼ばれない) _TermH264Decoder(); video.Release(); }); } // コマンド送信 private void sendCmd(string cmd) { byte[] data = Encoding.ASCII.GetBytes(cmd); this.udpForCmd.Send(data, data.Length, "192.168.10.1", 8889); } // 開始ボタン private void btnStart_Click(object sender, EventArgs e) { SetupTello(); this.txtRet.Text = ""; this.btnCmd.Enabled = false; sendCmd("command"); } // コマンド送信ボタン押下 private void btnCmd_Click(object sender, EventArgs e) { this.txtRet.Text = ""; this.btnCmd.Enabled = false; sendCmd(this.txtCmd.Text); } } }とりあえず64bitで動くソースは以下に置いておきます。
https://github.com/mima3/Telloメモリ解放関係がだいぶ怪しいので、とりあえず動かす用としてくださいというか、そもそもOpenCVでやった方がはるかに楽です・・・orz
最後に
ここではTelloの最低限の機能を.NETで実装した例をしめしました。
基本的に文字のコマンドを送信するだけで、制御できますが、h264のデコード処理はネィティブのDLLを作ってOpenCVがとれるようにする必要があります。
そこを超えてしまえば、あとは.NETのライブラリを色々と利用してにTelloを活用する道筋が見えるかと思います。(たとえば、音声認識で飛行させるとか・・・)
まぁ、.NETでTelloを使う道筋はみえても、おっさんの人生の道筋はみえないね。しかたないね。
- 投稿日:2019-02-15T23:37:51+09:00
ニート翔ぶ
昨今、退職エントリーが流行っているので、昨年、勢いで会社を辞めてニートになった記念に何か書こうと思います。
おっさんは明るい未来に羽ばたくことはできませんでしたので、せめて、ドローンぐらいは明るい未来を羽ばたかせてみせよう、そう思ってこの記事を書いてみました。Telloとは
Telloは小型のドローンでカメラもついており、Android,iPadといった携帯端末で操作が可能です。
https://www.ryzerobotics.com/jp/telloこの度は無収入のくせに以下のセットを購入しました。
https://www.amazon.co.jp/gp/product/B07979Q4YS注意事項:
・充電用のUSBはついてこないので自前でそろえる必要があります。
機体にささないと充電できません。ただし別売りのバッテリーケースを購入すれば機体にささずに充電が可能のようですが、おっさんは無職なので購入してまでの検証はしてません。・ハードウェアの性能としてはカメラがついているので動画撮影が可能です。つまり、住宅地で飛ばすと覗きとまちがわれるので気をつけましょう。おっさんは無職なのでポリス沙汰になると無職で全国デビューになるので細心の注意しないといけません。
・wifiでつなげて機体の操作をする必要があるため、操作側のリモコンはLANカードが2枚差しでないとインターネットにつなげながらの操作は行えません。おっさんはノートPCを10年ぶりくらいに有線のLANにつなげて作業しました。
・羽に指が当たると、そこそこ痛いので、慣れない間は軍手をして操作したほうがいいです。たぶん、大型のドローンの羽だったら、ドローンのかわりに指が飛んでいたと思います。
・可能なら外の広いところで運転した方が安全です。おっさんは引きこもりなので家でやりましたが、5回ほど壁にあたり墜落しました。
Tello SDK
TelloはSDKが提供されており、UDP経由で以下のことが行えます。
・機体の操作。
・機体の情報取得(傾きとか温度とかバッテリー情報)
・カメラからの撮影情報の取得UDPなので基本的に無線LANがつながればどんなプラットフォームでも動作させることができますが、検索してでてくるMacのPythonかC/C++でやった方が絶対にいいです。
ジャイアントロボのように音声で操作しようと思って、音声認識が簡単にできる.NETで始めたら、えらい苦労しました。また、SDKではなくて、バイナリデータを送信してSDKに書かれていない操作もできるようですが、ここでは割愛します。
Tello SDK 1.3.0.0
以下はTelloSDK1.3.0.0をそれっぽく翻訳したものです。
1. 概要
Tello SDKはWi-Fi UDPポートを介して航空機に接続し、ユーザーはテキストコマンドでドローンを制御することができます。 Tello3.pyファイルをダウンロードするにはここをクリックしてください。
2. アーキテクチャ
Wi-Fiを使用してTelloとPC、Mac、またはモバイルデバイスとの間の通信を確立します。
コマンドの送信と応答の受信
Tello IP: 192.168.10.1 UDP PORT:8889 <<-->> PC/Mac/Mobile
注意1:同じポートを介してTelloとメッセージを送受信するように、PC、Mac、またはモバイルデバイスでUDPクライアントを設定します。注意2:他のコマンドを送信する前に、"command"コマンドをUDP ポート8889を介してTelloに送信してTelloのSDKモードを開始します。
Telloステータスの受信
Tello IP: 192.168.10.1 ->> PC/Mac/Mobile UDP Server: 0.0.0.0 UDP PORT:8890
注意3:PC、Mac、またはモバイルデバイスにUDPサーバーをセットアップし、UDP PORT 8890を介してIP 0.0.0.0からのメッセージを聞きます。まだ行っていない場合は、注意2を実行して状態データの受信を開始してください。**
Telloビデオストリームの受信
Tello IP: 192.168.10.1 ->> PC/Mac/Mobile UDP Server:0.0.0.0 UDP PORT:11111
注意4:PC、Mac、またはモバイルデバイスにUDPサーバーをセットアップし、UDP PORT 11111を介してIP 0.0.0.0からメッセージを受信します。
注意5:もし実行していないなら注意2を実行してください。その後、UDP PORT 8889を介して「streamon」コマンドをTelloに送信してストリーミングを開始します。3. TELLOコマンドの種類と結果
このSDKには3つの基本的なコマンドタイプが含まれています。
コントロールコマンド(xxx)
・コマンドの実行が成功した場合、"ok"が戻ります
・成功しなかった場合、"error"または有益な結果コードが戻りますリードコマンド(xxx?)
・サブパラメータの現在値を戻します。設定コマンド(xxx a)は、新しいサブパラメータ値を設定しようとします
・コマンドの実行が成功した場合、"ok"が戻ります
・成功しなかった場合、"error"または有益な結果コードが戻ります4. Tello コマンド
コントロールコマンド
コマンド 説明 考えられる応答 command SDKモードに入る ok
errortakeoff Telloが自動で離陸する ok
errorland Telloが自動で着陸する ok
errorstreamon ビデオストリームをON ok
errorstreamoff ビデオストリームをOFF ok
erroremergency 全てのモータを停止 ok
errorup x Tello が x cm上昇
x:20-500ok
errordown x Tello が x cm下降
x:20-500ok
errorleft x Tello が x cm左へ
x:20-500ok
errorright x Tello が x cm右へ
x:20-500ok
errorforward x Tello が x cm前進
x:20-500ok
errorback x Tello が x cm後退
x:20-500ok
errorcw x Tello が 時計回りに x度 回転
x:1-3600ok
errorccw x Tello が 反時計回りに x度 回転
x:1-3600ok
errorflip x Tello が x 方向に宙返りをする
l:(left)
r(right)
f(foward)
b(back)ok
errorgo x y z speed Tello が x y z の方向へ speed(cm/s)の速度で飛ぶ x:20-500 ※訳注 前進する xで後退
y:20-500 ※訳注 左へ -y で右へ
z:20-500
speed:10-100curve x1 y1 z1 x2 y2 z2 speed Telloが現在位置とspeed(cm/s)とともに定義された2つの座標を曲線状に飛びます。もし円弧の半径が0.5-10meterの範囲外の場合、レスポンスはfalseとなります。
x1, x2: 20-500
y1, y2: 20-500
z1, z2: 20-500
speed: 10-60
x/y/z は同時に-20~20の間にはできませんok
error設定コマンド
コマンド 説明 考えられる応答 speed x 速度 x cm/sを設定する
x: 10-100ok
errorrc a b c d 4つのチャネルを通してRCコントロールを送信する
a: left/right (-100~100)
b: forward/backward (-100~100)
c: up/down (-100~100)
d: yaw (-100~100)ok
errorwifi ssid pass Wi-FiのSSIDとpasswordを設定する ok
errorリードコマンド
コマンド 説明 考えられる応答 speed? 現在の速度(cm/s)を取得 x: 1-100 battery? 現在のバッテリーのパーセンテージを取得 x: 1-100 time? 現在の飛行時間を取得 time height? 現在の高さ(cm)を取得 x: 0-3000 temp? 現在の温度(℃)を取得 x : 0-90 attitude? IMU(慣性計測装置) の姿勢情報を取得 pitch roll yaw baro? バロメータ(気圧計)の値(m)を取得 x acceleration? IMU角加速度データを取得する(0.001g) x y z tof? TOFからの距離(cm)を取得する x:30-1000 wifi? Wi-FiのSNRを取得する snr 訳者注
pitch roll yaw:参考:https://algorithm.joho.info/robotics/roll-pitch-yaw-matrix/
Tof:Time of Flightのことと思われる
SNR:信号対雑音比。SN比が高ければ伝送における雑音の影響が小さく、SN比が小さければ影響が大きい。5. Telloステータス
データ型:String
Example:
“pitch:%d;roll:%d;yaw:%d;vgx:%d;vgy%d;vgz:%d;templ:%d;temph:%d;tof:%d;h:%d;bat:%d;baro: %.2f; time:%d;agx:%.2f;agy:%.2f;agz:%.2f;\r\n”説明
o pitch: ピッチ角
o roll: ロール角
o yaw: ヨー角
o vgx: Speed x,
o vgy: Speed y,
o vgz: Speed z,
o templ: 最も低い温度, 摂氏℃
o temph: 最も高い温度、摂氏℃
o tof: TOF distance, cm
o h: Height, cm
o bat: 現在のバッテリーのパーセンテージ, %
o baro: バロメーター測定, cm
o time: モータの時間,
o agx: 加速度x,
o agy: 加速度y,
o agz: 加速度z,6 安全機能
もしTelloが15秒間なにもコマンドを受信しなければ、自動で着陸をします
7 TelloのWi-fiリセット
電源ON状態のTelloに5秒間の長押しをするとインジケータライトが消えて黄色に点滅します。 インジケータランプが黄色のライトを点滅させると、Wi-Fi SSIDとパスワードは工場出荷時の設定にリセットされ、デフォルトではパスワードは設定されません。
.NETで操作してみる。
おとなしく、Mac+Pythonで動かした方がいいです。やっている人がいっぱいいます。
それでもやるなら、以下を参考にしてみてください。事前準備
・ステータス取得のための8890とビデオストリーム取得のためのポート11111を開けておきます。
つながらない場合は、アプリケーション固有のファイアウォールの設定も確認してください。
Telloとのネットワークはパブリックのネットワークになっているはずなので、パブリックの設定もちゃんとみましょう(2敗)
・WireShark等でネットワークの電文をみれるようにしておきます。
https://www.wireshark.org/
問題の切り分けにやくに立ちます。・ffmpegを用意する。
Telloからのビデオ情報を表示するのに使用します。
また、自前でデコードする場合もffmpegのAPIを使用しないと厳しいです。
https://www.ffmpeg.org/簡単なTelloプログラミング
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Net;//for UDP using System.Net.Sockets; //for UDP using System.Threading;//for Interlocked using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; namespace TelloSample { public partial class Form1 : Form { private UdpClient udpForCmd; //コマンド結果受信用クライアント private UdpClient udpForStsRecv; //ステータスの結果受信用クライアント public Form1() { InitializeComponent(); } // コマンドの結果更新用 private delegate void DelegateUpdateCmdResult(String ret); // ステータスの更新用 private delegate void DelegateUpdateSts(String sts); // コマンド結果を更新。ワーカスレッドからの場合はメインスレッドで実行 private void UpdateCmdResult(String ret) { if (this.InvokeRequired) { Object[] param = new Object[1] { ret }; this.Invoke(new DelegateUpdateCmdResult(this.UpdateCmdResult), param); return; } this.txtRet.Text = ret; this.btnCmd.Enabled = true; } // ステータスを更新。ワーカスレッドからの場合はメインスレッドで実行 private void UpdateSts(String sts) { if (this.InvokeRequired) { Object[] param = new Object[1] { sts }; this.Invoke(new DelegateUpdateSts(this.UpdateSts), param); return; } this.txtSts.Text = sts; } // Telloとの通信を設定する private void SetupTello() { this.udpForCmd = new UdpClient(0); this.udpForStsRecv = new UdpClient(8890); // コマンド結果の受信処理 Task.Run(() => { IPEndPoint remoteEP = null;//任意の送信元からのデータを受信 while (true) { try { String rcvMsg = ""; byte[] rcvBytes = udpForCmd.Receive(ref remoteEP); Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes)); this.UpdateCmdResult(rcvMsg); } catch (Exception ex) { Debug.WriteLine(ex.Message); } } }); // ステータスの受信処理 Task.Run(() => { IPEndPoint remoteEP = null;//任意の送信元からのデータを受信 while (true) { try { String rcvMsg = ""; byte[] rcvBytes = udpForStsRecv.Receive(ref remoteEP); Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes)); rcvMsg = rcvMsg.Replace(";", "\r\n"); this.UpdateSts(rcvMsg); } catch (Exception ex) { Debug.WriteLine(ex.Message); } } }); // コマンド送信 private void sendCmd(string cmd) { byte[] data = Encoding.ASCII.GetBytes(cmd); this.udpForCmd.Send(data, data.Length, "192.168.10.1", 8889); } // 開始ボタン private void btnStart_Click(object sender, EventArgs e) { SetupTello(); this.txtRet.Text = ""; this.btnCmd.Enabled = false; sendCmd("command"); } // コマンド送信ボタン押下 private void btnCmd_Click(object sender, EventArgs e) { this.txtRet.Text = ""; this.btnCmd.Enabled = false; sendCmd(this.txtCmd.Text); } } }ビデオについて
streamon コマンドを送信するとポート11111にビデオの情報が受信できます。
これを表示するにはffmpegのffplayを使用するといいでしょう。ffplay -probesize 32 -sync ext udp://127.0.0.1:11111ウィンドウが起動して現在のカメラが表示されます。
よくある問題
・コマンドを受け付けない
無線LANでつながっているかを確認する。
充電されているか確認する。USBさして青ランプが点灯されたらフル充電である。
WireSharkでパケットの送受信がされているか確認する。
送受信のポートが開いているか確認。規定値だとパブリックネットワークなので注意。・カメラが受信できない。
11111ポートが開いているか見直す。
WireSharkでパケットが届いているか確認する。・ステータスが受信できない。
8890ポートが開いているか見直す。・たまにコマンドの応答結果がとれない。
UDPなので仕様だと思われます。okが必ずくるという前提は多分まずいかもです。.NETで自力でビデオデータを取り扱いたい。
OpenCVSharpを使えば簡単にできます・・・(震え)
・OpenCVSharp
OpenCvSharp3-AnyCPUとOpenCvSharp4.runtime.winをNuGetで取得していました。using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using OpenCvSharp; namespace OpenCVVideo { class Program { static void Main(string[] args) { var camera = VideoCapture.FromFile("udp://127.0.0.1:11111"); using (var normalWindow = new Window("normal")) { var normalFrame = new Mat(); var srFrame = new Mat(); while (true) { camera.Read(normalFrame); if (normalFrame.Empty()) break; normalWindow.ShowImage(normalFrame); int key = Cv2.WaitKey(100); if (key == 27) break; // ESC キーで閉じる } } } } }OpenCVのVideoCaptureはファイルといいつつ、UDPからのストリームもとれます。
まちがっても自分でUDPで11111ポートを監視してデコードしようとしてはいけません。
以下にその愚かな例をしめしますが、おとなしくOpenCVを使いましょう。自前でUDPの11111ポートを監視したパターン(失敗版)
簡単な流れとしては以下の通りになります。
- streamonコマンドを送信
- ポート11111を監視
- 1460バイトとどいている間は後続のパケットがあるので受信しつづける。1460以外のデータ長がきたら、いままできたぶんとまとめてffmpegのAPIを使用してデコードする。
- デコード結果をffmpegの関数を使用してRGBに変換する
- OpenCVにかませるため、OpenCVの関数をつかってBGRに変換する
- OpenCVでビデオつくったりする。
この挙動は下記を参考にしました。
https://github.com/dji-sdk/Tello-Python/blob/master/Tello_Video/tello.py
https://github.com/dji-sdk/Tello-Python/tree/master/Tello_Video/h264decoder.NETでつらいのは3と4です。
これをおこなうにはネイティブのDLLでffmpegのAPIを使い実行した結果をC#に渡す必要があります。.NETでTelloのビデオを扱うために必要なライブラリ
・ffmpeg
https://ffmpeg.zeranoe.com/builds/
devにincludeファイルとlibファイル、sharedにdllがあるのでそれぞれダウンロードしました。
このライブラリはtelloから取得したh264形式をRGBに変換するために使用します。
ソースコードから自前でコンパイルもできますが、MSYS2をいれたりして結構、手間がかかるのでFormアプリでいいなら、おとなしく配布されているものを使った方がいいと思います。(一敗)h264decorderの移植
先に紹介したPythonでh264のデコードをするためのコードを.NETでやるために改造しました。
https://github.com/mima3/Tello/tree/master/h264decoderおそらく、メモリ解放処理がうまくできていない気がするので参考程度にしてください。
h264decoderの使用
.NETでネイティブのDLLを使用する場合は、32bitか64bitかは意識してください。
今回は64bitで動かすためにAnyCPUをx64に変更するか、32bitを優先するフラグをオフにする必要があります。
もし、今まで動いていたアプリケーションが起動すらしない場合、DLLが32bitと64bitで混在している可能性を疑ってみてください。以下に移植したh264decoderを使用して動画を作成する実装例を記載します。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Net;//for UDP using System.Net.Sockets; //for UDP using System.Threading;//for Interlocked using System.Diagnostics; using OpenCvSharp; using System.IO; using System.Runtime.InteropServices; namespace TelloSample { public partial class Form1 : Form { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct H264DecoderResult { public int w; public int h; public int size; public IntPtr buff; } [DllImport("h264decoder.dll", EntryPoint = "InitH264Decoder")] static extern void _InitH264Decoder(); [DllImport("h264decoder.dll", EntryPoint = "TermH264Decoder")] static extern void _TermH264Decoder(); [DllImport("h264decoder.dll", EntryPoint = "DecodeH264")] static extern bool _DecodeH264(IntPtr buff, int size, ref H264DecoderResult outbuff); [DllImport("h264decoder.dll", EntryPoint = "GetH264DecoderLastError")] public static extern IntPtr GetH264DecoderLastError(); [DllImport("h264decoder.dll", EntryPoint = "FreeData")] public static extern void FreeData(IntPtr data); [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); private UdpClient udpForCmd; //コマンド結果受信用クライアント private UdpClient udpForStsRecv; //ステータスの結果受信用クライアント private UdpClient udpForVideo; //ビデオストリームの受信用 public Form1() { InitializeComponent(); } // コマンドの結果更新用 private delegate void DelegateUpdateCmdResult(String ret); // ステータスの更新用 private delegate void DelegateUpdateSts(String sts); // コマンド結果を更新。ワーカスレッドからの場合はメインスレッドで実行 private void UpdateCmdResult(String ret) { if (this.InvokeRequired) { Object[] param = new Object[1] { ret }; this.Invoke(new DelegateUpdateCmdResult(this.UpdateCmdResult), param); return; } this.txtRet.Text = ret; this.btnCmd.Enabled = true; } // ステータスを更新。ワーカスレッドからの場合はメインスレッドで実行 private void UpdateSts(String sts) { if (this.InvokeRequired) { Object[] param = new Object[1] { sts }; this.Invoke(new DelegateUpdateSts(this.UpdateSts), param); return; } this.txtSts.Text = sts; } // Telloとの通信を設定する private void SetupTello() { this.udpForCmd = new UdpClient(0); this.udpForStsRecv = new UdpClient(8890); this.udpForVideo = new UdpClient(11111); // コマンド結果の受信処理 Task.Run(() => { IPEndPoint remoteEP = null;//任意の送信元からのデータを受信 while (true) { try { String rcvMsg = ""; byte[] rcvBytes = udpForCmd.Receive(ref remoteEP); Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes)); this.UpdateCmdResult(rcvMsg); } catch (Exception ex) { Debug.WriteLine(ex.Message); } } }); // ステータスの受信処理 Task.Run(() => { IPEndPoint remoteEP = null;//任意の送信元からのデータを受信 while (true) { try { String rcvMsg = ""; byte[] rcvBytes = udpForStsRecv.Receive(ref remoteEP); Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes)); rcvMsg = rcvMsg.Replace(";", "\r\n"); this.UpdateSts(rcvMsg); } catch (Exception ex) { Debug.WriteLine(ex.Message); } } }); // ビデオストリームの受信処理 Task.Run(() => { IPEndPoint remoteEP = null;//任意の送信元からのデータを受信 byte[] packetData = new byte[0]; int cnt = 0; _InitH264Decoder(); var fourcc = VideoWriter.FourCC('m', 'p', '4', 'v'); var video = new VideoWriter("test.mp4", fourcc, 20, new OpenCvSharp.Size(960, 720) ); while (true) { try { byte[] rcvBytes = udpForVideo.Receive(ref remoteEP); int l = packetData.Length; Array.Resize<byte>(ref packetData, l + rcvBytes.Length); Array.Copy(rcvBytes, 0, packetData, l, rcvBytes.Length); if (rcvBytes.Length != 1460) { int size = Marshal.SizeOf(packetData[0]) * packetData.Length; IntPtr inPtr = Marshal.AllocHGlobal(size); Marshal.Copy(packetData, 0, inPtr, packetData.Length); H264DecoderResult decret = new H264DecoderResult(); Debug.WriteLine("DO DECODE"); if (_DecodeH264(inPtr, packetData.Length, ref decret)) { Debug.WriteLine("DO DECODE,,,,ok"); var mat = new Mat(decret.h, decret.w, MatType.CV_8UC3); CopyMemory(mat.Data, decret.buff, (uint)decret.size); var matCv = new Mat(); Cv2.CvtColor(mat, matCv, ColorConversionCodes.RGB2BGR); video.Write(matCv); FreeData(decret.buff); } else { Debug.Write(Marshal.PtrToStringAnsi(GetH264DecoderLastError())); } Marshal.FreeHGlobal(inPtr); packetData = new byte[0]; ++cnt; } } catch (Exception ex) { Debug.WriteLine(ex.Message); } } // 後片付けの方法はあとで考える。(呼ばれない) _TermH264Decoder(); video.Release(); }); } // コマンド送信 private void sendCmd(string cmd) { byte[] data = Encoding.ASCII.GetBytes(cmd); this.udpForCmd.Send(data, data.Length, "192.168.10.1", 8889); } // 開始ボタン private void btnStart_Click(object sender, EventArgs e) { SetupTello(); this.txtRet.Text = ""; this.btnCmd.Enabled = false; sendCmd("command"); } // コマンド送信ボタン押下 private void btnCmd_Click(object sender, EventArgs e) { this.txtRet.Text = ""; this.btnCmd.Enabled = false; sendCmd(this.txtCmd.Text); } } }とりあえず64bitで動くソースは以下に置いておきます。
https://github.com/mima3/Telloメモリ解放関係がだいぶ怪しいので、とりあえず動かす用としてくださいというか、そもそもOpenCVでやった方がはるかに楽です・・・orz
最後に
ここではTelloの最低限の機能を.NETで実装した例をしめしました。
基本的に文字のコマンドを送信するだけで、制御できますが、h264のデコード処理はネィティブのDLLを作ってOpenCVがとれるようにする必要があります。
そこを超えてしまえば、あとは.NETのライブラリを色々と利用してにTelloを活用する道筋が見えるかと思います。(たとえば、音声認識で飛行させるとか・・・)
まぁ、.NETでTelloを使う道筋はみえても、おっさんの人生の道筋はみえないね。しかたないね。
- 投稿日:2019-02-15T22:42:03+09:00
GetComponentの基本を理解する
はじめに
1から勉強すると必ず出るメソッドです。写経を行いテキストの指示通りにやればほぼうまくいきます。しかしこれらを繰り返していくといざ説明する時に、何も理解をしていない事に気が付きました。テストを繰り返していくうちに理解できるようになったので、記事にします。
Componentとは
Inspectorに表示されているTransform以下の各々の情報。AddComponentでGameObjectで使うコンポーネントを追加します。GetComponentはこれらの情報を変数に割り当てることで、自由に設定できます。
スクリプト
public class CylinderRotate : MonoBehaviour { float time; void Update() { time += Time.deltaTime; //timeを加算し続ける var ren = GetComponent<Renderer>(); //コンポーネントRendererを取得 var col = GetComponent<Collider>(); //コンポーネントColliderを取得 var tra = GetComponent<Transform>(); //コンポーネントTransformを取得 tra.Rotate(10f, 0, 0); //GameObjectoを回転させる。 if(time >= 0) //timeが0秒経過 { col.enabled = true; //Colliderを真 ren.enabled = true; //Rendererを真 } if(time >=2) //timeが2秒経過 { col.enabled = false; //Colliderを偽 ren.enabled = false; //Rendererを偽 if (time >=4) //timeが4秒経過 { time = 0; //timeを0にする。 } } } }上のスクリプトを打ちこんで、3Dオブジェクトにアタッチする。
結果
感想
今までコンポーネントの使い方は理解をほとんどしていなかったが、ロックマンのような足場を消す・現れるの繰り返しを作ってみたことで、理解が大分進んだと思う。
- 投稿日:2019-02-15T22:04:06+09:00
C#のリソースの仕組みを使って多言語対応させようぜ☆(^~^)
Visual Studio 2017 を使って C# のプログラムを多言語対応させようぜ
日本だけで使うプログラムなら、多言語対応の書き方は 一切 目にすることもないと思う。
見なくていいものは目に触れさせない、
そこらへん、Visual Studio / C#、 はたまた オブジェクト指向プログラミング というやつ は
よく出来てある……。状況設定
ここは東京。 フランス人と インド人と 日本人が 働いているとするぜ。
私の書いたプログラムが 運悪く ファイル書き込み時に ファイルのロックに引っかかって 何度リトライしてもダメ、
諦めて タイムアウトさせて 異常終了することにするとしよう。
SendMail の機能を使って フランスチーム、インドチーム、日本チームに 異常通知メールを飛ばすという状況を想像してくれだぜ。### 疑似コード ### 件名と本文。グーグル翻訳でいいだろ……、情報量の少ない日本語の直訳は、きっとおかしなことになっている。 SendMail("ツール障害", "ファイルのロックに引っかかった。") SendMail("Défaillance de l'outil", "J'ai attrapé un verrou de fichier.") SendMail("Tool failure", "I caught on a file lock.")こんな感じで ベタ書き すれば その場の用は足りるが、何と言ったらいいのだろう。
ソースコードが 1文字 でも増えるのは労力だ。
定時に帰りたいマンは こんなことはしない。
というのも、テキスト は人類にとって便利だが、 テキストを扱う のはきっと人類が得意なものではない……。そこそこ解決しよう
例えば、以下のようにする。
### 疑似コード foreach culture in ["ja-JP(日本)", "fr-FR(フランス)", "en-US(インド)"] Resources.Culture = culture SendMail(Resources.ToolFailureSubject, Resources.ToolFailureBody)文化を配列に入れておいてループで回し、
ループの中では 文化を切り替えて使う。翻訳の文章は、別のところに表を持っておく。# コーヒーブレイク - 東京弁←→大阪弁に切り替えできるか? # # en は英語、US はアメリカ方言で、合わせて en-US になる。英国方言なら en-EN。 # これは 言語コード という。 # 日本は ja-JP しかない。 大阪弁を追加しようと ja-OS(日本(大阪)) を作ろうとしたら # 言語コードは OS(コンピューター)への登録制になっていて、管理者権限 が求められたので止めた。 # だったら fr-FR(フランス) に大阪弁を詰め込めば 東京弁/大阪弁 切り替えも実現可能……だが、 # こういう本来意図しない使い方をすると 定時に誰も帰れないくそコード になってしまう。別の表は、以下のような形になっているとしよう。
Resources.ja-JP.resx
Key Value ToolFailureSubject ツール障害 ToolFailureBody ファイルのロックに引っかかった。 Resources.fr-FR.resx
Key Value ToolFailureSubject Défaillance de l'outil ToolFailureBody J'ai attrapé un verrou de fichier. Resources.resx
Key Value ToolFailureSubject Tool failure ToolFailureBody I caught on a file lock. ここで、
Resources.ja-JP.resx には日本向けの、
Resources.fr-FR.resx にはフランス向けの、
Resources.resx にはアメリカ向けの言葉が入っているとするぜ。インド人はどこに行ったのかというと、英語でいいらしい。
とりあえず、 英語(米国) を共通言語としておこう。 Key 列は 英語(米国) で書く。
あとは ja-JP を指定すれば 日本語テーブルの Value 列が、
fr-FR を指定すれば フランス語テーブルの Value 列が出てくる仕組みと考えれば すっきりしてるだろ。en-US 、これは英語(米国)だが、を指定すれば デフォルトのテーブルが出てくると思えだぜ。
で、あとから言語を増やしたり減らしたりしたければ、 Resources.zh-CN.resx ファイルを追加したり、
Resources.fr-FR.resx ファイルを消したりすれば、文化を書いていた配列を修正する程度の手間で
対応でき……るという夢を ひとまず見ておこう。エディターを見つけよう
Visual Studio 2017 という開発統合環境(IDE)があるが、機能拡張しなければ 便利ではない。
例えば ResXManager といった感じのフリーソフトをインストールするとしよう。すると、
Key en-US ja-JP fr-FR ToolFailureSubject Tool failure ツール障害 Défaillance de l'outil ToolFailureBody I caught on a file lock. ファイルのロックに引っかかった。 J'ai attrapé un verrou de fichier. 横一列に 文化 が並び、翻訳作業、あるいは 抜けチェック が楽になるかもしれない。
モデル/ビュー/コントローラー という言葉を聞いたことがあるなら、これは ビュー に当たる。
しかも、 [Translation] ボタンを押すと グーグル翻訳 と連携して 1列を埋めてくれるかもしれない。「だろう」とか「しれない」というのは サーバーからアクセス制限を食らったりといった 実践問題 が
いろいろ あるから 期待を下げているわけだが、「ベタ書き」よりは はるかにマシになるだろう。次に、ソースコードを改造している間に、ソースコードで使っていないリソース が出てきて、
不要だから Resources.resx ファイルから消したい、ということも出てくるかもしれない。
自動でやってくれれば……、と思うものの Visual Studio 2017 に期待しすぎてはいけない。
NuGet とか Marketplace とか、プラグインをダウンロードする仕組みを使って
欲しいものは自分で探して勝手に導入するのが 流行りだぜ。例えば RESX-Unused-Finder を使って、使っていないリソースは消してしまおう。
ぼーっとしているときに ファイルパスを間違えて どこかで何かが消えているヒューマンミスも起こりえるが、
人気が出れば 誰かが GUI を改善してくれるだろう。これで多言語対応は ばっちりだ……、と思うと そうでもない。フリーソフトは バグを避けながら使うものだぜ。
便利ではない Visual Studio 2017 にデフォルトで付いている「リソース」とかいうエディターを使う場面が出てくる。
それが Keyの変更 だぜ。Resources.resx ファイルを フリーソフトでいじっても、C# のソースコードの方はリファクタリング(プロパティ名の変更)してくれない。
理屈は分かる。
.resx とかいう XML ファイルをいじるのは簡単でも、C# のソースコードのリファクタリングまでやってくれるかというと、
その作業は 全く別物なのかもしれない。(あるいは 欲しければ自分で作らなければならない)ところで C# のソースを貼っておく。
using System; // ↓この角括弧が何やってるか分けわかんなければ消してくれだぜ。 [assembly: CLSCompliant(true)] namespace Grayscale.ResourcePracticeWithConsole { using System.Diagnostics; using System.Globalization; using System.Threading; // ↓リソースファイルを使おうとしている。 // リソースファイルの作り方は Qiita で別の人が書いてるだろ、勝手に調べろだぜ。 using Grayscale.ResourcePracticeWithConsole.Properties; /// <summary> /// プログラム。 /// </summary> public static class Program { /// <summary> /// エントリーポイント。 /// </summary> public static void Main() { // 使うリソースを 英語(米国)を使う。 Resources.Culture = new CultureInfo("en-US"); // ↓C#デフォルトのエラーメッセージや、画面に効いてくる。 Thread.CurrentThread.CurrentCulture = Resources.Culture; Thread.CurrentThread.CurrentUICulture = Resources.Culture; // ↓英語(米国)テーブルの HelloWorld キーに対応した値が表示される。 Trace.WriteLine(Resources.HelloWorld); // 使うリソースを 日本 に変える。 Resources.Culture = new CultureInfo("ja-JP"); // ↓日本テーブルの HelloWorld キーに対応した値が表示される。 Trace.WriteLine(Resources.HelloWorld); // 15秒待つ。 Thread.Sleep(15000); } } }だいたいの 基本 を紹介した。
変えたカルチャーを元に戻すために Old 変数に古いのを一時退避しておいて後で使う スワップ とか使いこなして、
ログは英語、通知メールは自分の言語に切り替えるとか、そういうのやり始めるのは 応用 なんで説明しない。データベースに 通知メール受け取りユーザーや、そのユーザーの言語を覚えさせといて
ユーザーの追加に対応するとか 発展 なんで、要件を満たした後で 勝手に遊べだぜ。じゃあ、
- 「リソース」エディターの中では [Shift]+[Enter] キーで改行できるが、他のエディターではどうやって改行を入れるのか?
- 「'」って何よ?
- 「&gt;」って何よ?
など、いろいろな 実践問題が出てくるので 1つ1つ 解決していって欲しい。
多分、フリーソフトの人気が上がって 利用者が儲かれば コミュニティの誰かが直してくれると思うが……。年月日時分
また、
12, FEB
と書いてあっても わたしには 何かわからないし、2019/02/12
と書いていても 何月か分からないし、
データベースに 年月日を 20190212 と 8桁で入れている日本企業があるとしても 何月か分からない。日付の 書式 をハードコーディングしていては 定時には帰れないだろう。
そんなときにも CultureInfo を使う。// 日付。 var dateValue = new DateTime(2019, 2, 16, 13, 57, 0); var cultures = new[] { new CultureInfo("ja-JP"), new CultureInfo("en-US"), new CultureInfo("fr-FR"), new CultureInfo("de-DE") }; foreach (var culture in cultures) { Trace.WriteLine(string.Format( "{0}: {1}", culture.Name, dateValue.ToString(culture))); }書式はハードコーディングせず、文化を指定すれば 勝手に変換されるように書いておく。
ja-JP: 2019/02/16 13:57:00 en-US: 2/16/2019 1:57:00 PM fr-FR: 16/02/2019 13:57:00 de-DE: 16.02.2019 13:57:00参考。
しかし、見た目だけ対応していても、日本はアメリカより9時間早い。
時差に対応する必要がある。 これも ハードコーディングしていては 定時には帰れないだろう。そこで、日本時間を いったん世界標準時に戻して、そこから タイムゾーンを指定して ずらす。
夏時間とか、 アメリカといっても東部と西部のどっちなのかとか、いっさい考えない。var universalDate = DateTimeOffset.Now.ToUniversalTime(); // System.ValueTuple (CultureInfo CultureInfo, string TimeZone)[] cultures = { (new CultureInfo("ja-JP"), "Tokyo Standard Time"), (new CultureInfo("en-US"), "GMT Standard Time"), (new CultureInfo("fr-FR"), "Romance Standard Time"), (new CultureInfo("de-DE"), "Romance Standard Time"), }; foreach (var culture in cultures) { Trace.WriteLine(string.Format( "{0}: {1} ({2})", culture.CultureInfo.Name, TimeZoneInfo.ConvertTime( universalDate, TimeZoneInfo.FindSystemTimeZoneById(culture.TimeZone)).ToString(culture.CultureInfo), culture.TimeZone)); }その結果、
ja-JP: 2019/02/16 14:48:15 +09:00 (Tokyo Standard Time) en-US: 2/16/2019 5:48:15 AM +00:00 (GMT Standard Time) fr-FR: 16/02/2019 06:48:15 +01:00 (Romance Standard Time) de-DE: 16.02.2019 06:48:15 +01:00 (Romance Standard Time)と時差も考慮してくれる。
アメリカの東なのか西なのかは 聞いておくといいのかもしれないが、めんどくさいんで イギリスのタイムゾーンにしている。+09:00 というのは、グリニッジ天文台の時計から+9時間ですよ、という意味なので、9を引けば グリニッジ標準時に戻せる。
日本で 朝8時にメールが届く、ということは グリニッジから見れば 昨日の23時 にメールが届いている。
フランスから見れば 0時に届いていると分かる。 これで時差は だいたい配慮できた。// ヒマがあれば もっと書く。
- 投稿日:2019-02-15T22:04:06+09:00
C#のリソースの仕組みを使おうぜ☆(^~^)
Visual Studio 2017 を使って C# のプログラムを多言語対応させようぜ
状況設定
ここは東京。 フランス人と インド人と 日本人が 働いているとするぜ。
私の書いたプログラムが 運悪く ファイル書き込み時に ファイルのロックに引っかかって 何度リトライしてもダメ、
諦めて タイムアウトさせて 異常終了することにするとしよう。
SendMail の機能を使って フランスチーム、インドチーム、日本チームに 異常通知メールを飛ばすという状況を想像してくれだぜ。### 疑似コード ### グーグル翻訳でいいだろ……、情報量の少ない日本語の直訳は、きっとおかしなことになっている。 SendMail("ツール障害", "ファイルのロックに引っかかった。") SendMail("Défaillance de l'outil", "J'ai attrapé un verrou de fichier.") SendMail("Tool failure", "I caught on a file lock.")こんな感じで ベタ書き すれば その場の用は足りるが、何と言ったらいいのだろう。
ソースコードが 1文字 でも増えるのは労力だ。
定時に帰りたいマンは こんなことはしない。
というのも、テキスト は人類にとって便利だが、 テキストを扱う のはきっと人類が得意なものではない……。そこそこ解決しよう
例えば、以下のようにする。
### 疑似コード foreach culture in ["ja-JP(日本)", "fr-FR(フランス)", "en-US(インド)"] Resources.Culture = culture SendMail(Resources.ToolFailureSubject, Resources.ToolFailureBody)文化を配列に入れておいてループで回し、
ループの中では 文化を切り替えて使う。翻訳の文章は、別のところに表を持っておく。# コーヒーブレイク - 東京弁←→大阪弁に切り替えできるか? # # en は英語、US はアメリカ方言で、合わせて en-US になる。英国方言なら en-EN。 # これは 言語コード という。 # 日本は ja-JP しかない。 大阪弁を追加しようと ja-OS(日本(大阪)) を作ろうとしたら # 言語コードは OS(コンピューター)への登録制になっていて、管理者権限 が求められたので止めた。 # だったら fr-FR(フランス) に大阪弁を詰め込めば 東京弁/大阪弁 切り替えも実現可能……だが、 # こういう本来意図しない使い方をすると 定時に誰も帰れないくそコード になってしまう。別の表は、以下のような形になっているとしよう。
Resources.ja-JP.resx
Key Value ToolFailureSubject ツール障害 ToolFailureBody ファイルのロックに引っかかった。 Resources.fr-FR.resx
Key Value ToolFailureSubject Défaillance de l'outil ToolFailureBody J'ai attrapé un verrou de fichier. Resources.resx
Key Value ToolFailureSubject Tool failure ToolFailureBody I caught on a file lock. ここで、
Resources.ja-JP.resx には日本向けの、
Resources.fr-FR.resx にはフランス向けの、
Resources.resx にはアメリカ向けの言葉が入っているとするぜ。インド人はどこに行ったのかというと、英語でいいらしい。
とりあえず、 英語(米国) を共通言語としておこう。 Key 列は 英語(米国) で書く。
あとは ja-JP を指定すれば 日本語テーブルの Value 列が、
fr-FR を指定すれば フランス語テーブルの Value 列が出てくる仕組みと考えれば すっきりしてるだろ。en-US 、これは英語(米国)だが、を指定すれば デフォルトのテーブルが出てくると思えだぜ。
で、あとから言語を増やしたり減らしたりしたければ、 Resources.zh-CN.resx ファイルを追加したり、
Resources.fr-FR.resx ファイルを消したりすれば、文化を書いていた配列を修正する程度の手間で
対応でき……るという夢を ひとまず見ておこう。エディターを見つけよう
Visual Studio 2017 という開発統合環境(IDE)があるが、機能拡張しなければ 便利ではない。
例えば ResXManager といった感じのフリーソフトをインストールするとしよう。すると、
Key en-US ja-JP fr-FR ToolFailureSubject Tool failure ツール障害 Défaillance de l'outil ToolFailureBody I caught on a file lock. ファイルのロックに引っかかった。 J'ai attrapé un verrou de fichier. 横一列に 文化 が並び、翻訳作業、あるいは 抜けチェック が楽になるかもしれない。
モデル/ビュー/コントローラー という言葉を聞いたことがあるなら、これは ビュー に当たる。
しかも、 [Translation] ボタンを押すと グーグル翻訳 と連携して 1列を埋めてくれるかもしれない。「だろう」とか「しれない」というのは サーバーからアクセス制限を食らったりといった 実践問題 が
いろいろ あるから 期待を下げているわけだが、「ベタ書き」よりは はるかにマシになるだろう。次に、ソースコードを改造している間に、ソースコードで使っていないリソース が出てきて、
不要だから Resources.resx ファイルから消したい、ということも出てくるかもしれない。
自動でやってくれれば……、と思うものの Visual Studio 2017 に期待しすぎてはいけない。
NuGet とか Marketplace とか、プラグインをダウンロードする仕組みを使って
欲しいものは自分で探して勝手に導入するのが 流行りだぜ。例えば RESX-Unused-Finder を使って、使っていないリソースは消してしまおう。
ぼーっとしているときに ファイルパスを間違えて どこかで何かが消えているヒューマンミスも起こりえるが、
人気が出れば 誰かが GUI を改善してくれるだろう。これで多言語対応は ばっちりだ……、と思うと そうでもない。フリーソフトは バグを避けながら使うものだぜ。
便利ではない Visual Studio 2017 にデフォルトで付いている「リソース」とかいうエディターを使う場面が出てくる。
それが Keyの変更 だぜ。Resources.resx ファイルを フリーソフトでいじっても、C# のソースコードの方はリファクタリング(プロパティ名の変更)してくれない。
理屈は分かる。
.resx とかいう XML ファイルをいじるのは簡単でも、C# のソースコードのリファクタリングまでやってくれるかというと、
その作業は 全く別物なのかもしれない。(あるいは 欲しければ自分で作らなければならない)ところで C# のソースを貼っておく。
using System; // ↓この角括弧が何やってるか分けわかんなければ消してくれだぜ。 [assembly: CLSCompliant(true)] namespace Grayscale.ResourcePracticeWithConsole { using System.Diagnostics; using System.Globalization; using System.Threading; // ↓リソースファイルを使おうとしている。 // リソースファイルの作り方は Qiita で別の人が書いてるだろ、勝手に調べろだぜ。 using Grayscale.ResourcePracticeWithConsole.Properties; /// <summary> /// プログラム。 /// </summary> public static class Program { /// <summary> /// エントリーポイント。 /// </summary> public static void Main() { // 使うリソースを 英語(米国)を使う。 Resources.Culture = new CultureInfo("en-US"); // ↓C#デフォルトのエラーメッセージや、画面に効いてくる。 Thread.CurrentThread.CurrentCulture = Resources.Culture; Thread.CurrentThread.CurrentUICulture = Resources.Culture; // ↓英語(米国)テーブルの HelloWorld キーに対応した値が表示される。 Trace.WriteLine(Resources.HelloWorld); // 使うリソースを 日本 に変える。 Resources.Culture = new CultureInfo("ja-JP"); // ↓日本テーブルの HelloWorld キーに対応した値が表示される。 Trace.WriteLine(Resources.HelloWorld); // 15秒待つ。 Thread.Sleep(15000); } } }だいたいの 基本 を紹介した。
変えたカルチャーを元に戻すために Old 変数に古いのを一時退避しておいて後で使う スワップ とか使いこなして、
ログは英語、通知メールは自分の言語に切り替えるとか、そういうのやり始めるのは 応用 なんで説明しない。データベースに 通知メール受け取りユーザーや、そのユーザーの言語を覚えさせといて
ユーザーの追加に対応するとか 発展 なんで、要件を満たした後で 勝手に遊べだぜ。じゃあ、
- 「リソース」エディターの中では [Shift]+[Enter] キーで改行できるが、他のエディターではどうやって改行を入れるのか?
- 「'」って何よ?
- 「&gt;」って何よ?
など、いろいろな 実践問題が出てくるので 1つ1つ 解決していって欲しい。
多分、フリーソフトの人気が上がって 利用者が儲かれば コミュニティの誰かが直してくれると思うが……。年月日時分
また、
12, FEB
と書いてあっても わたしには 何かわからないし、2019/02/12
と書いていても 何月か分からないし、
データベースに 年月日を 20190212 と 8桁で入れている日本企業があるとしても 何月か分からない。日付の 書式 をハードコーディングしていては 定時には帰れないだろう。
そんなときにも CultureInfo を使う。// 日付。 DateTime dateValue = new DateTime(2019, 2, 16, 13, 57, 0); CultureInfo[] cultures = { new CultureInfo("ja-JP"), new CultureInfo("en-US"), new CultureInfo("fr-FR"), new CultureInfo("de-DE") }; foreach (CultureInfo culture in cultures) { Trace.WriteLine(string.Format( "{0}: {1}", culture.Name, dateValue.ToString(culture))); }書式はハードコーディングせず、文化を指定すれば 勝手に変換されるように書いておく。
ja-JP: 2019/02/16 13:57:00 en-US: 2/16/2019 1:57:00 PM fr-FR: 16/02/2019 13:57:00 de-DE: 16.02.2019 13:57:00参考。
しかし、見た目だけ対応していても、日本はアメリカより9時間早い。
時差に対応する必要がある。 これも ハードコーディングしていては 定時には帰れないだろう。そこで、日本時間を いったん世界標準時に戻して、そこから タイムゾーンを指定して ずらす。
夏時間とか、 アメリカといっても東部と西部のどっちなのかとか、いっさい考えない。var universalDate = DateTimeOffset.Now.ToUniversalTime(); // System.ValueTuple (CultureInfo CultureInfo, string TimeZone)[] cultures = { (new CultureInfo("ja-JP"), "Tokyo Standard Time"), (new CultureInfo("en-US"), "GMT Standard Time"), (new CultureInfo("fr-FR"), "Romance Standard Time"), (new CultureInfo("de-DE"), "Romance Standard Time"), }; foreach (var culture in cultures) { Trace.WriteLine(string.Format( "{0}: {1} ({2})", culture.CultureInfo.Name, TimeZoneInfo.ConvertTime( universalDate, TimeZoneInfo.FindSystemTimeZoneById(culture.TimeZone)).ToString(culture.CultureInfo), culture.TimeZone)); }その結果、
ja-JP: 2019/02/16 14:48:15 +09:00 (Tokyo Standard Time) en-US: 2/16/2019 5:48:15 AM +00:00 (GMT Standard Time) fr-FR: 16/02/2019 06:48:15 +01:00 (Romance Standard Time) de-DE: 16.02.2019 06:48:15 +01:00 (Romance Standard Time)と時差も考慮してくれる。
アメリカの東なのか西なのかは 聞いておくといいのかもしれないが、めんどくさいんで イギリスのタイムゾーンにしている。+09:00 というのは、グリニッジ天文台の時計から+9時間ですよ、という意味なので、9を引けば グリニッジ標準時に戻せる。
日本で 朝8時にメールが届く、ということは グリニッジから見れば 昨日の23時 にメールが届いている。
フランスから見れば 0時に届いていると分かる。 これで時差は だいたい配慮できた。// ヒマがあれば もっと書く。
- 投稿日:2019-02-15T19:36:23+09:00
[Unity] GridLayoutGroup入門 図鑑的なもの作成 ScrollView
今回はUnityのUIにおいて超便利な機能、GridLayOutGroupについて解説します。
今回はScrollViewにGridLayoutGroupを使って、
よくゲームで見るこんな感じの図鑑のように、画像などを等間隔に表示することが簡単にできます!
UnityでScrollViewを作る
CanvasScalerのUIScaleModeをScale with screen sizeにして、
Create→UI→Scroll ViewからScroll Viewを作りましょう。
AnchorPresetsから右下のStretchを選び、RectTransformの全ての値を0にしてCanvasと同じ大きさにしましょう。
(Stretchは親オブジェクトから何ピクセル離れるか、というイメージです。今回は全て0にしているので親オブジェクトと同じ大きさですね)
これで画面いっぱいにScroll Viewが表示されました。今回は縦方向の移動だけで良いとするので、Scroll RectのHozirontalのチェックを外して、Scrollbar horizontalのオブジェクトも削除しちゃいましょう。
さて、ここから本題ですが、ScrollViewのViewPortの子にContentがあります。ここに画像やボタンを配置するのですが、ここで使える便利な機能がGridLayOutGroupです。
GridLayoutGroup
contentにAdd Componentから
・GridLayoutGroup
・Content size Flitter
の2つを追加しましょう。GridLayoutGroupについてそれぞれの機能は公式にわかりやすくのってるので以下参照。
Grid Layout Group - UnityマニュアルContentSizeFlitterについては、GridLayoutGroupで指定した子の要素の大きさに合わせて、オブジェクトの大きさを調整するという便利なものです。この2つはセットで使われることが多いです。
簡単に上から見ていくと、
・Gridの左右25ピクセル、上下10ピクセルは間をあける。
・子のサイズは縦横共に210ピクセル。
・子の要素同士は縦横50ピクセルづつ間隔をあける。
・列数は3で固定する。(Consraint column count)
という感じです。また、ContentSizeFlitterのFitをPreferred Sizeにするのを忘れずに。これをしないと、Sizeがずれてしまう可能性があります。
ではどんな感じで並ぶのか見てみましょう。今回は最初のGifのようにボタンを並べてそのボタンに数字を表示してみます。
以下のコードをオブジェクトにアタッチして、
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class Create : MonoBehaviour { public RectTransform contentRectTransform; public Button button; private void Start() { for(int i = 1; i <= 30; i++){ var obj = Instantiate(button,contentRectTransform); obj.GetComponentInChildren<Text>().text = i.ToString(); } } }contentRectTransformの部分にContentを、buttonの部分に作成したボタンをいれてあげれば、完了です!
自分でCellSizeやSpacingなどをいじって、間隔や要素の大きさなどを変更してみましょう!
以上です!!
- 投稿日:2019-02-15T18:21:51+09:00
Codewars で複数のプログラミング言語で全ての問題を解いていきたい時に便利な操作
クリックすると私が解いた問題が見れます。
# 今回は画像(スクショ)だけで手抜き~!# Codewars についてご存じない方は、先に『【Codewars】ブラウザでコーディングの基礎からトレーニングできるサイト (ブラウザでvimが使えて32種類のプログラミング言語に対応。4000個以上の問題が投稿されています!)』 をお読み頂くようお願い致します。
このような感じで、View Profile メニューから Kata タブを選択することで、他の言語で解き忘れがないか確認できます。すでにやった問題を自分の学習中の言語全てで解いてみたい方におススメです。
- 投稿日:2019-02-15T17:51:00+09:00
VisualStudioでデバッグ実行では動くのにビルドしたらexeが動作しない場合の対処
対処
実行しているexeは正しくbinフォルダ以下のexeかを確認する。
つまり、プロジェクト以下のbinフォルダにあるexeを使用すること。!!!間違っても、objフォルダ以下のexeを使ってはいけない。それは罠だ!!!
なにが起きたか?
タイトル通り。非常につまらないミスだが、binフォルダ以下のexeを実行すべきところを、objフォルダ以下のexeと取り違えると、実行しても何も起こらない。
(厳密にはイベントビューワで確認すると、DllNotFoundExceptionやSystem.NotImplementedExceptionが発生している。)原因
というか、仕様だが、VisualStudioのobjフォルダはビルド時の中間ファイルが置かれる場所。なので、dllやビルド時にコピーされるリソースファイル等は置かれていない。
どちらもReleaseフォルダができるので、取り違えやすいので注意しよう。
- 投稿日:2019-02-15T15:28:03+09:00
C#でクラスのプロパティ名と値を取得する
久々の投稿です.
後輩にクラスからプロパティ名と値ってどうやってとれるんですか?と聞かれたのでサンプルを書きました.例えばこんなクラスがあったとします.
public static class Orange { public const int Id = 1; public const string Name = "Orange"; public const int Price = 100; }このクラスからプロパティ名と値をリフレクションで取得します.
public class Program { static void Main(string[] args) { var t = typeof(Orange); foreach (var f in t.GetFields()) { var n = f.Name; var v = f.GetValue(t); Console.WriteLine(string.Format("プロパティ名: {0}, 値: {1}", n, v)); } Console.ReadLine(); } }Visual Studioで適当にコンソールアプリ作って実行するとこんな感じに表示されます.
プロパティ名: Id, 値: 1 プロパティ名: Name, 値: Orange プロパティ名: Price, 値: 100おしまい.
- 投稿日:2019-02-15T15:28:03+09:00
C#でクラスのフィールド名と値を取得する
久々の投稿です.
後輩にクラスからフィールド名と値ってどうやってとれるんですか?と聞かれたのでサンプルを書きました.例えばこんなクラスがあったとします.
public static class Orange { public const int Id = 1; public const string Name = "Orange"; public const int Price = 100; }このクラスからフィールド名と値をリフレクションで取得します.
public class Program { static void Main(string[] args) { var t = typeof(Orange); foreach (var f in t.GetFields()) { var n = f.Name; var v = f.GetValue(t); Console.WriteLine(string.Format("フィールド名: {0}, 値: {1}", n, v)); } Console.ReadLine(); } }Visual Studioで適当にコンソールアプリ作って実行するとこんな感じに表示されます.
フィールド名: Id, 値: 1 フィールド名: Name, 値: Orange フィールド名: Price, 値: 100おしまい.
- 投稿日:2019-02-15T14:16:53+09:00
CodinGame の TRON BATTLE で FloodFill アルゴリズムを実装してみた
# この記事は、『CodinGame はBOT(AIプログラム)でバトルするのが正しい楽しみ方かもしれません』 の続きです。
以下のようなデバッグ出力ができるように CodinGame の TRON BATTLE プログラムを改造してみた。
P=0
という出力から、自機のプレイヤー番号は0であることが分かる。Input{X0:2, Y0:2, X1:10, Y1:0}
という出力からプレイヤー0のスタート位置(tail)は、(2, 2) であることと、現在位置(head)の座標が (10, 0) であることが分かる。 *自機のプレイヤー(プレイヤー0)のヘッドから見て現時点で到達可能であるマスが+
記号で表示されている。ちなみにマイナス記号は何もない印である。P=0 Input{X0:2, Y0:2, X1:10, Y1:0} + + + + + + + + + + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1. 出来たソース
- コメントは全部削除しました。(読めるでしょ?w)
- 自己流でFloodFillアルゴリズムを実装してみました。
- FloodFillで到達可能範囲を計算しながら「一切」その結果を使ってないので「強くない」です。『CodinGame はBOT(AIプログラム)でバトルするのが正しい楽しみ方かもしれません』 で紹介したソースと強さは全く変わってませんw(誰か助けてw)
using System; using System.Linq; using System.IO; using System.Text; using System.Collections; using System.Collections.Generic; class Player { static void Main(string[] args) { Player control = new Player(); string[] numbers; Input[] inputs; // game loop while (true) { numbers = Console.ReadLine().Split(' '); int N = int.Parse(numbers[0]); int P = int.Parse(numbers[1]); Console.Error.WriteLine("P={0}", P); inputs = new Input[N]; for (int i = 0; i < N; i++) { numbers = Console.ReadLine().Split(' '); int X0 = int.Parse(numbers[0]); int Y0 = int.Parse(numbers[1]); int X1 = int.Parse(numbers[2]); int Y1 = int.Parse(numbers[3]); inputs[i] = new Input(i, X0, Y0, X1, Y1); } Input me = inputs[P]; string dir = control.HandleInputs(me, inputs); control.DumpMap(); Console.WriteLine(dir); } } Cell[,] map = new Cell[30, 20]; Queue<Cell> ffQueue = new Queue<Cell>(); private Player() { for(int y=0; y<20; y++) { for(int x=0; x<30; x++) { map[x, y] = new Cell(x, y, -1); } } } private string HandleInputs(Input me, Input[] inputs) { Console.Error.WriteLine(me); foreach(var input in inputs) { if (input.X1 < 0) DeleteIdsFromMap(input.Id); else AddInputToMap(input); } ExecuteFloodfill(me); if (CanMoveTo(me, -1, 0)) return "LEFT"; if (CanMoveTo(me, 1, 0)) return "RIGHT"; if (CanMoveTo(me, 0, -1)) return "UP"; if (CanMoveTo(me, 0, 1)) return "DOWN"; return "?"; } void AddInputToMap(Input input) { this.map[input.X1, input.Y1].V = input.Id; } void DeleteIdsFromMap(int id) { for(int y=0; y<20; y++) { for(int x=0; x<30; x++) { if (map[x, y].V == id) map[x, y].V = -1; } } } bool CanMoveTo(Input me, int xOffset, int yOffset) { int x = me.X1+xOffset; int y = me.Y1+yOffset; if (x < 0) return false; if (x > 29) return false; if (y < 0) return false; if (y > 19) return false; return map[x, y].V == -1 || map[x, y].V == 9; } bool IsEmptyCell(Cell center, int xOffset, int yOffset) { int x = center.X+xOffset; int y = center.Y+yOffset; if (x < 0) return false; if (x > 29) return false; if (y < 0) return false; if (y > 19) return false; return map[x, y].V == -1; } void DumpMap() { for(int y=0; y<20; y++) { for(int x=0; x<30; x++) { if (map[x, y].V == -1) Console.Error.Write("-"); else if (map[x, y].V == 9) Console.Error.Write("+"); else Console.Error.Write(map[x, y].V); Console.Error.Write(" "); } Console.Error.WriteLine(); } } void ExecuteFloodfill(Input me) { for(int y=0; y<20; y++) { for(int x=0; x<30; x++) { if (map[x, y].V == 9) map[x, y].V = -1; } } Cell c = map[me.X1, me.Y1]; if (IsEmptyCell(c, -1, 0)) ffQueue.Enqueue(map[c.X-1, c.Y]); if (IsEmptyCell(c, 1, 0)) ffQueue.Enqueue(map[c.X+1, c.Y]); if (IsEmptyCell(c, 0, -1)) ffQueue.Enqueue(map[c.X, c.Y-1]); if (IsEmptyCell(c, 0, 1)) ffQueue.Enqueue(map[c.X, c.Y+1]); FloodfillLoop(); } void FloodfillLoop() { while(ffQueue.Count > 0) { Cell c = ffQueue.Dequeue(); if(map[c.X, c.Y].V != -1) continue; c.V = 9; if (IsEmptyCell(c, -1, 0)) ffQueue.Enqueue(map[c.X-1, c.Y]); if (IsEmptyCell(c, 1, 0)) ffQueue.Enqueue(map[c.X+1, c.Y]); if (IsEmptyCell(c, 0, -1)) ffQueue.Enqueue(map[c.X, c.Y-1]); if (IsEmptyCell(c, 0, 1)) ffQueue.Enqueue(map[c.X, c.Y+1]); } } } class Input { public int Id; public int X0; public int Y0; public int X1; public int Y1; public Input(int id, int x0, int y0, int x1, int y1) { this.Id = id; this.X0 = x0; this.Y0 = y0; this.X1 = x1; this.Y1 = y1; } public override string ToString() { return String.Format("Input{{X0:{0}, Y0:{1}, X1:{2}, Y1:{3}}}", X0, Y0, X1, Y1); } } class Cell { public int X; public int Y; public int V; public Cell(int x, int y, int v) { this.X = x; this.Y = y; this.V = v; } public override string ToString() { return String.Format("Cell{{X:{0}, Y:{1}, V:{2}}}", X, Y, V); } }2. 最後に
TRON BATTLE については書く記事としてはこれで終わりになるかもしれません。
C# で挑戦される方の「手始め」の参考にでもなればと思い投稿してみました。# Wood 1 League を抜け出したいのだが…
- 投稿日:2019-02-15T10:20:31+09:00
[Unity] 確率判定
RPGとかなんやらで「何%の確率で何かが起きる」みたいな時に使える関数があると便利だと思って書いておきます。
ソース
/// <summary> /// 確率判定 /// </summary> /// <param name="fPercent">確率 (0~100)</param> /// <returns>当選結果 [true]当選</returns> public static bool Probability(float fPercent) { float fProbabilityRate = UnityEngine.Random.value * 100.0f; if (fProbabilityRate < fPercent) { return true; } else { return false; } }使い方
if(Probability(30)) { //30%の確率で起こるイベント }他にも方法はいろいろあると思うけど、自分はこれが使い勝手良かった。
- 投稿日:2019-02-15T01:59:55+09:00
CodinGame はBOT(AIプログラム)でバトルするのが正しい楽しみ方かもしれません
『【CodinGame】ブラウザでコーディングの基礎からトレーニングできるサイト (疑似ゲーム開発環境を使って学べます。解答は25種類のプログラミング言語から選択して記述可能!)』 という記事で、CodinGame に対してなにやら否定的なコメントを書いてしまいましたが、Twitter で「codingame」を検索してみると、「CodinGame はBOT(AIプログラム)でバトルするのが正しい楽しみ方」的な発言がみられたので、AIについては素人ながら挑戦してみました。
- まだ、挑戦し始めなのでログ(ブログ)っぽく、やったことをそのまま記述…
- 勝ち方の指南なんてできないので…「他の人が自分もやってみたい」と思えるような紹介風で…
という目標で書いてます。
長くなる(と思う)ので記事分けながら書いて、あとでマトメの記事が上手くできればいいなと考えています。
それでは、以下本文へ
1. https://www.codingame.com/start にアクセスします。
2. 「Sign up with Google」を選択します。
3. サインアップ完了後、https://www.codingame.com/home に自動的に遷移します。
4. https://www.codingame.com/multiplayer に遷移します。
5. https://www.codingame.com/multiplayer/bot-programming に遷移します。
6. TRON BATTLE のリプレイ動画
以下のツイート内の画像をクリックすると「TRON BATTLE のリプレイ動画」に飛びます。
この動画を見て分かるように、各プレイヤーの車は異なった色のリボンを残しながら進んでいきます。各プレイヤーの車は自分のリボンも他人のリボンも踏んではいけません(踏んだらアウト)。もちろん場外に出てもいけません。上記に違反した時点でそのプレイヤーはアウトとなり、そのプレイヤーのリボンがゲーム画面から消えます(ここもポイント!)。
最後までアウトにならずに生き残ったプレイヤーの勝ちです。(実際には1位、2位、…と順位がつきます)
Tron Battle - Replayhttps://t.co/0sMOpmueln
— JavaCommons (@javacommons) 2019年2月14日7. TRON BATTLE のメイン画面の「JOIN」を押してプログラム編集画面(IDE)を開きます
8. コードエディタに初期に表示される内容(C#の場合)
解答に使うプログラミング言語は、C#, C++, Java, Javascript, Python3, Bash, C, Clojure, Dart, F#, Go, Groovy, Haskell, Kotlin, Lua, ObjectiveC, OCaml, Pascal, Perl, PHP, Python2, Ruby, Rust, Scala, Swift, VB.NET の中から自由に選べます。
CodinGame のプログラミング問題はほとんど(全て?)、刻々と標準入力から情報を読み取り、刻々と標準出力に指示を書き出すというループから成り立っています。
- このおかげでプログラミング言語間の差異を吸収しやすくなっています。ユーザーが書くプログラムを取り巻く親プロセスのプログラムは共通の物が使えるからです。
static void Main(string[] args)
に合わせて、全プログラムを static メソッドで書こうとするとクラスを導入する際にハマることがありますのでご注意。コードエディタに初期に表示される内容(C#の場合)using System; using System.Linq; using System.IO; using System.Text; using System.Collections; using System.Collections.Generic; /** * Auto-generated code below aims at helping you parse * the standard input according to the problem statement. **/ class Player { static void Main(string[] args) { string[] inputs; // game loop while (true) { inputs = Console.ReadLine().Split(' '); int N = int.Parse(inputs[0]); // total number of players (2 to 4). int P = int.Parse(inputs[1]); // your player number (0 to 3). for (int i = 0; i < N; i++) { inputs = Console.ReadLine().Split(' '); int X0 = int.Parse(inputs[0]); // starting X coordinate of lightcycle (or -1) int Y0 = int.Parse(inputs[1]); // starting Y coordinate of lightcycle (or -1) int X1 = int.Parse(inputs[2]); // starting X coordinate of lightcycle (can be the same as X0 if you play before this player) int Y1 = int.Parse(inputs[3]); // starting Y coordinate of lightcycle (can be the same as Y0 if you play before this player) } // Write an action using Console.WriteLine() // To debug: Console.Error.WriteLine("Debug messages..."); Console.WriteLine("LEFT"); // A single line with UP, DOWN, LEFT or RIGHT } } }9. さて、TRON BATTLE の課題(問題)文は以下のような内容です
(長いので折りたたみ中。展開してご覧ください)
◎The Goal
◎目標In this game your are a program driving the legendary tron light cycle and fighting against other programs on the game grid.
このゲームであなたが目指すのは、ゲームグリッド上で、伝説のトロンライトサイクルを運転して他のプログラムと戦うことのできるプログラムです。
The light cycle moves in straight lines and only turn in 90° angles while leaving a solid light ribbon in its wake. Each cycle and associated ribbon features a different color.
Should a light cycle stop, hit a light ribbon or goes off the game grid it will be instantly deactivated.ライトサイクルは真っすぐに進むか90°の角度でしか曲がれず、起動時から固形分からなる光のリボンを残しながら進みます。それぞれのライトサイクルと関連付けられたリボンは異なる色を放ちます。
ライトサイクルが停止せざるを得ない、または光のリボンに衝突した、またはゲームグリッドの外に出た場合、そのライトサイクルは即座に非活性化されます。The last cycle in play wins the game. Your goal is to be the best program: once sent to the arena, programs will compete against each-others in battles gathering 2 to 4 cycles. The more battles you win, the better your rank will be.
最後まで残ったライトサイクルがゲームの勝者となります。あなたの目標はベストプログラムを目指すことです: アリーナに送られれば(訳注: SUBMITボタンを押せば)、プログラム達が、2~4台でのライトサイクルバトルでお互いに競争となります。より多くかつほどあなたのランクが上がります。
◎Rules
◎ルールEach battle is fought with 2 players. Each player plays in turn during a battle. When your turn comes, the following happens:
それぞれのバトルは2プレイヤーで戦います。それぞれのプレイヤーが順番にプレイします。あなたのターンが来たら、以下が発生します:
- Information about the location of players on the grid is sent on the standard input of your program. So your AI must read information on the standard input at the beginning of a turn.
- グリッド上のプレイヤーの位置情報があなたのプログラムの標準入力に送信されます。そのため、あなたのAIはターンの最初に標準入力上の情報を読み込まなければなりません。
- Once the inputs have been read for the current game turn, your AI must provide its next move information on the standard ouput. The output for a game turn must be a single line stating the next direction of the light cycle: either UP, DOWN, LEFT or RIGHT.
- 現在のゲームターンのための情報を読み込んだら、AIは次の移動のための情報を標準出力に提供しなければなりません。ゲームターン時の出力は、ライトサイクルの次の移動方向を宣言する一行の出力でなければなりません: UP, DOWN, LEFT, RIGHT のいずれかを出力します。
- Your light cycle will move in the direction your AI provided.
- あなたのライトサイクルはAIが出力した方向に動きます。
- At this point your AI should wait for your next game turn information and so on and so forth. In the mean time, the AI of the other players will receive information the same way you did.
- この時点で、あなたのAIは次のゲームターンの情報を待たなければなりません。後は、ここまでの繰り返しとなります。一方で、他のプレイヤーのAIもあなたと同様に情報を受け取ります。
If your AI does not provide output fast enough when your turn comes, or if you provide an invalid output or if your output would make the light cycle move into an obstacle, then your program loses.
もし、あなたのAIがあなたのターンが来た時に、十分高速に出力を提供できない場合、または妥当でない出力をした場合、または出力に従うとライトサイクルが障害物に衝突してしまう等の場合には、あなたのプログラムの負けとなります。
If another AI loses before yours, its light ribbon disappears and the game continues until there is only one player left.
もし他のAIがあなたより前に負けた場合は、その光のリボンは消滅し、一人のプレイヤーのみが残るまでゲームは継続します。
The game grid has a 30 by 20 cells width and height. Each player starts at a random location on the grid.
ゲームグリッドは、30x20 のセルで構成されます。それぞれのプレイヤーはグリッド上のランダムな位置からスタートします。
◎Victory Conditions
◎勝利条件Be the last remaining player
最後まで残るプレイヤーとなること。◎Game Input
◎ゲームの入力Input for one game turn
ゲームターン毎の入力Line 1: Two integers N and P. Where N is the total number of players and P is your player number for this game.
一行目: N と P の2つの整数。Nはプレイヤーの総人数で、Pはこのゲームでのプレイヤー番号です。
The N following lines: One line per player. First line is for player 0, next line for player 1, etc. Each line contains four values X0, Y0, X1 and Y1. (X0, Y0) are the coordinates of the initial position of the light ribbon (tail) and (X1, Y1) are the coordinates of the current position of the light ribbon (head) of the player. Once a player loses, his/her X0 Y0 X1 Y1 coordinates are all equal to -1 (no more light ribbon on the grid for this player).
続くN行: プレイヤー毎に一行。最初の行はプレイヤー0に対するもの、次の行はプレイヤー1、という形になります。それぞれの行は4つの値 X0, Y0, X1, Y1 を含みます。(X0, Y0) は光のリボンの初期位置(tail)で (X1, Y1) はプレイヤーの光のリボンの現在位置(head)です。あるプレイヤーの負けが決定すると、そのプレイヤーの X0 Y0 X1 Y1 の値hあ全て -1 となり、そのプレイヤーの光のリボンはグリッド上には存在しないことを意味します。
Output for one game turn
ゲームターン毎の出力A single line with UP, DOWN, LEFT or RIGHT
UP, DOWN, LEFT, RIGHT のいずれかを一行で出力。
Constraints
制約2 ≤ N ≤ 2
0 ≤ P < N
0 ≤ X0, X1 < 30
0 ≤ Y0, Y1 < 20Your AI must answer in less than 100ms for each game turn.
あなたのAIは各ゲームターンに対して100ms未満で応答しなければなりません。10. とりあえず、壁への激突、リボンへの激突を避ける目的で作ったプログラム
まったくAI的なことしてませんが、ゲームターン毎に隣(上下左右)のセルだけ見て、障害物がなければそちらに進む(判定順序: 左⇒右⇒上⇒下)。毎ターン、自キャラも含めて位置情報を二次元配列に格納(死んだキャラのリボン情報の消去も一応実装済み。初期は2キャラしかいないのでテストできませんw)。
- コメントに大体書いたので一点だけ補足すると、ライトサイクルが曲がるとき90°までしか曲がれない(来た方向に戻るようなことはできない)というのをどう表現しようかと迷っていたんですが、自キャラの光のリボンも配列(マップ)に記録して障害物と見做しているので、とりあえず障害物判定するだけでいける方向に進めば良いことだと気づきました。
using System; using System.Linq; using System.IO; using System.Text; using System.Collections; using System.Collections.Generic; class Player { static void Main(string[] args) { Player control = new Player(); string[] inputs; Position[] positions; // game loop while (true) { inputs = Console.ReadLine().Split(' '); int N = int.Parse(inputs[0]); // total number of players (2 to 4). int P = int.Parse(inputs[1]); // your player number (0 to 3). Console.Error.WriteLine("P={0}", P); positions = new Position[N]; for (int i = 0; i < N; i++) { inputs = Console.ReadLine().Split(' '); int X0 = int.Parse(inputs[0]); // starting X coordinate of lightcycle (or -1) int Y0 = int.Parse(inputs[1]); // starting Y coordinate of lightcycle (or -1) int X1 = int.Parse(inputs[2]); // starting X coordinate of lightcycle (can be the same as X0 if you play before this player) int Y1 = int.Parse(inputs[3]); // starting Y coordinate of lightcycle (can be the same as Y0 if you play before this player) positions[i] = new Position(i, X0, Y0, X1, Y1); } string dir = control.HandleVehicless(positions, P); control.DumpMap(); Console.WriteLine(dir); } } // 自分も含めて誰かが通った座標を記憶しておくために使う。 // 誰も通ってない場合は -1。通った、または現在いるマスに対してはプレイヤーのメンバーIDを格納する。 int[,] map = new int[30,20]; // メインコントロールクラスのコンストラクタ(map内の値を-1(=誰も通ってない)に初期化しておく。) private Player() { for(int y=0; y<20; y++) { for(int x=0; x<30; x++) { map[x, y] = -1; } } } private string HandleVehicless(Position[] positions, int myIndex) { Position me = positions[myIndex]; // me = 自分の座標情報 Console.Error.WriteLine(me); // me を標準エラーに出力(public override string ToString()の定義による) foreach(var p in positions) { // (me も含めて)全キャラの座標を通ってはいけない場所に登録。 AddToMap(p); // ただし、死にキャラの場合はそのキャラの座標情報を全消去する。 } // 上下左右のマスを判定し通ってはいけない場所でなければその方向を返す。 if (!FoundFromMap(me, -1, 0)) return "LEFT"; if (!FoundFromMap(me, 1, 0)) return "RIGHT"; if (!FoundFromMap(me, 0, -1)) return "UP"; if (!FoundFromMap(me, 0, 1)) return "DOWN"; return "LEFT"; // ここに来た時点でどの方向も通れないが一応正式な値の一つとして "LEFT" を返す。 } void AddToMap(Position p) { if (p.X1 < 0) { DeleteMemberIdsFromMap(p.Id); // 現在座標がマイナス値で来たら死にキャラなのでマップから消す。 return; } this.map[p.X1, p.Y1] = p.Id; // 配列にプレイヤーのメンバーIDを登録する。 } void DeleteMemberIdsFromMap(int id) { for(int y=0; y<20; y++) { for(int x=0; x<30; x++) { if (map[x, y] == id) map[x, y] = -1; } } } // map を検索して通れない場所の場合 true を返す。通れる場合は false。 // me(自機の座標)に xOffset と yOffset を加えた場所について判定(検索)する。 bool FoundFromMap(Position me, int xOffset, int yOffset) { int x = me.X1+xOffset; int y = me.Y1+yOffset; if (x < 0) return true; if (x > 29) return true; if (y < 0) return true; if (y > 19) return true; return map[x, y] != -1; } // デバッグ用に 30x20 のマップを表示(現在生きているメンバーのIDを表示。空のマスは '-' を出力) void DumpMap() { for(int y=0; y<20; y++) { for(int x=0; x<30; x++) { if (map[x, y] == -1) Console.Error.Write("-"); // -1の場合はマイナス記号を出力。 else Console.Error.Write(map[x, y]); // -1でなければプレイヤーID(0以上)を出力。 Console.Error.Write(" "); } Console.Error.WriteLine(); } } } // キャラクターの座標を登録・記憶しておくための入れ物。 // Main 関数が受け取る標準入力の情報を格納するための構造体のようなもの。 // Player インスタンスの各メソッドの引数は標準入力とのやり取りを意識せず、この構造体を期待できる。 class Position { public int Id; public int X0; public int Y0; public int X1; public int Y1; public Position(int id, int x0, int y0, int x1, int y1) { this.Id = id; this.X0 = x0; this.Y0 = y0; this.X1 = x1; this.Y1 = y1; } // デバッグなどで出力される際のフォーマットを制御する。 public override string ToString() { //return "{X0:" + X0 + ", Y0:" + Y0 + ", X1:" + X1 + ", Y1:" + Y1 + "}"; return String.Format("{{X0:{0}, Y0:{1}, X1:{2}, Y1:{3}}}", X0, Y0, X1, Y1); } }11. アリーナでリーグ戦をする前に「PLAY MY CODE」ボタンで確認
12. 対戦実行速度を上げてサクサクデバッグ
13. アリーナ(リーグ戦)に挑戦
14. リーグ戦でボスに勝ったら以下のような画面が表示されます
- 最初のリーグではプレイヤーの数は2ですが、リーグが上がっていくと増えていくみたいです。
15. リーグ戦で勝てず上位リーグに上がれなかった場合の対処法
他にも負けた相手がいる場合には、同様の手順でIDEに読み込んで対戦しながらプログラム(AI)を強くするとよいでしょう。
16. 最後に
AIを作るノウハウを持っていないことと、強いプログラムを記事で晒すのはいいアイディアではないかなと思ってますので、今回は TRON BATTLE を紹介しましたが、次はまた別のプログラムについて紹介したいと思っています。以下のツイートの画像をクリックしていただければ、そのゲームのリプレイ画面が表示されます。
もし、分かりにくいところなどありましたらコメント等をよろしくお願いいたします。それでは…Coders Strike Back - Replayhttps://t.co/DM0yyVfGSl
— JavaCommons (@javacommons) 2019年2月14日P.S.
https://www.codingame.com/multiplayer/bot-programming/tron-battle/leaderboard