- 投稿日:2020-03-15T23:41:27+09:00
.Net標準のChartコントロールでPythonでよくある散布図行列を作成してみた
はじめに
最近Chartコントロールにハマっている。どうせ標準のものだし、融通の利かないコントロールだろうと当初は高をくくっていたが、使ってみると全くそんなことはなく、かゆいところに手が届く、噛めば噛むほど味が出る奥が深いコントロールなのだ。
さて、このChartコントロールの素晴らしさをお伝えするため、今回ChartコントロールおよびMath.Netを使い、Pythonの機械学習本等によく出てくる"あの"散布図行列を作成してみたい。
作りたいもののイメージ
- N個のパラメータとデータを与えると、合計 N x N 個のグラフを並べて表示する。
- 対角線上には各パラメータのヒストグラムを表示する。
- 対角線以外には、対応する2つのパラメータの散布図を表示
- N x N のグラフが、どのパラメータか分かるよう、パラメータ名のラベルを表示する。
- サイズは固定とし、パラメータが多くなってグラフが大きくなってもスクロールバーでスクロールできるようにする。
工夫した点
- スクロールできるようPanelの中にChartコントロールを配置した。
- 並べて表示するといっても、Chartコントロールは1つとし、ChartAreaを自前で N x N 個配置した。
- 大量のグラフを表示するため、軸の表示はしないようにし、グラフを隙間なく並べて、俯瞰しやすいようにした。
- そのままでは、グラフとグラフの間に境界が表示されないので、Chartコントロールの背景を黒にし、少し隙間がでるように配置することで境界っぽく表示されるようにした。
- パラメータ名のラベルは、ChartAreaを並べる場所を少し開け、グラフィックスオブジェクトで直接背景色およびラベル名を描画した。
ソース
さて、ソースは以下の通りだ。データを準備するところはバッサリカットしているのでそのままでは動かないが、上の工夫を見れば大体やっていることは分かるだろう。
using MathNet.Numerics.Statistics; using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; using System.Windows.Forms.DataVisualization.Charting; namespace Kimisyo.Forms { public partial class ScatterPlotForm : Form { public int width = 1500; public int height = 1500; public int margin = 20; int nBuckets = 20; // パラメータIDのリスト List<int> parameterIds = new List<int>(); // パラメータ名のリスト List<String> parameterNames = new List<String>(); // パラメータID毎にデータが格納されたディクショナリ Dictionary<int, Dictionary<int, Double>> parameterId2ValueMap = new Dictionary<int, Dictionary<int, Double>>(); public ScatterPlotForm() { InitializeComponent(); } private void ScatterPlotForm_Load(object sender, EventArgs e) { chart1.BackColor = Color.Black; chart1.Width = this.width; chart1.Height = this.height; chart1.Series.Clear(); chart1.Legends.Clear(); chart1.ChartAreas.Clear(); float margin_rate = ((float)margin / (float)chart1.Width) * 100; Random random = new System.Random(); for (int i = 0; i < parameterIds.Count; i++) { for (int j = 0; j < parameterIds.Count; j++) { String name = Convert.ToString(i) + "_" + Convert.ToString(j); ChartArea ca = new ChartArea(name); ca.AxisX.Enabled = AxisEnabled.False; ca.AxisY.Enabled = AxisEnabled.False; ca.Position.X = margin_rate + ((float)j / (float)parameterIds.Count) * (100 - margin_rate); ca.Position.Y = margin_rate + ((float)i / (float)parameterIds.Count) * (100 - margin_rate); ca.Position.Width = ((float)1/(float)parameterIds.Count) * (100 - margin_rate) - (float)0.2; ca.Position.Height = ((float)1/(float)parameterIds.Count) * (100 - margin_rate) - (float)0.2; this.chart1.ChartAreas.Add(ca); Series series = new Series(); series.ChartArea = name; // ScatterPlotを作成 if (i != j) { series.ChartType = SeriesChartType.Point; series.MarkerColor = Color.Blue; series.MarkerStyle = MarkerStyle.Circle; series.MarkerSize = 2; // データ int parameterIdX = parameterIds[j]; int parameterIdY = parameterIds[i]; Dictionary<int, double> valueMapX = parameterId2ValueMap[parameterIdX]; Dictionary<int, double> valueMapY = parameterId2ValueMap[parameterIdY]; foreach (int sampleId in valueMapX.Keys) { DataPoint dp = new DataPoint(valueMapX[sampleId], valueMapY[sampleId]); series.Points.Add(dp); } } // ヒストグラムを作成 else { series.ChartType = SeriesChartType.Column; series.Color = Color.Blue; int parameterId = parameterIds[i]; Dictionary<int, double> valueMap = parameterId2ValueMap[parameterId]; Histogram hist = new Histogram(valueMap.Values, nBuckets, -50, 50); for (int k = 0; k < nBuckets; k++) { double mid = Math.Round((hist[k].UpperBound + hist[k].LowerBound) / 2, 1); series.Points.Add(new DataPoint(mid, hist[k].Count)); } } chart1.Series.Add(series); } } } /// ラベルを表示する(PostPaintイベント) private void chart1_PostPaint(object sender, ChartPaintEventArgs e) { ChartGraphics cg = e.ChartGraphics; Graphics g = cg.Graphics; SolidBrush myBrushWhite = new SolidBrush(Color.White); g.FillRectangle(myBrushWhite, 0, 0, margin-3, this.height); g.FillRectangle(myBrushWhite, 0, 0, this.width, margin-3); for(int i=0; i<parameterIds.Count; i++) { float margin_rate = ((float)margin / (float)chart1.Width) * 100; float x_rate = margin_rate + ((float)i / (float)parameterIds.Count) * (100 - margin_rate); int x = Convert.ToInt32(this.width * x_rate/100); SolidBrush myBrushBlack = new SolidBrush(Color.Black); Font font = new Font("MS UI Gothic", 6); g.DrawString(parameterNames[i], font, myBrushBlack, new Point(x, 3)); } // 原点を移動 g.TranslateTransform(0, this.height); // -90度回転 g.RotateTransform(-90f); // 文字列を描画 for(int i=0; i<parameterIds.Count; i++) { float margin_rate = ((float)margin / (float)chart1.Width) * 100; float x_rate = ((float)i / (float)parameterIds.Count) * (100 - margin_rate); int x = Convert.ToInt32(this.width * x_rate/100); SolidBrush myBrushBlack = new SolidBrush(Color.Black); Font font = new Font("MS UI Gothic", 6); g.DrawString(parameterNames[parameterIds.Count-i-1], font, myBrushBlack, new Point(x, 3)); } // 元に戻す g.ResetTransform(); } } }出来上がり図
以下は1500x1500のサイズに20個のパラメータを並べてみた図である。表示に数秒またされるが、表示後のスクロールは快適である。
図は小さいもが、気になるグラフをクリックすると、詳細なグラフを表示する等の処理を組み込めば全く問題ないだろう。課題
課題としては、グラフの境界線の上にプロットされているように見えてしまい見ずらい。また、X軸、Y軸の範囲が統一されていないなどの問題がある。まだ、Chartコントロールの使い方の理解が不十分なため、今後様々なカスタマイズに挑戦してみたい。
参考
- 投稿日:2020-03-15T23:41:27+09:00
.Net標準のChartコントロールでPythonライブラリにあるような散布図行列を作成してみた
はじめに
最近Chartコントロールにハマっている。どうせ標準のものだし、融通の利かないコントロールだろうと当初は高をくくっていたが、使ってみると全くそんなことはなく、かゆいところに手が届く、噛めば噛むほど味が出る奥が深いコントロールなのだ。
さて、このChartコントロールの素晴らしさをお伝えするため、今回ChartコントロールおよびMath.Netを使い、Pythonの機械学習本等によく出てくる"あの"散布図行列を作成してみたい。
作りたいもののイメージ
- N個のパラメータとデータを与えると、合計 N x N 個のグラフを並べて表示する。
- 対角線上には各パラメータのヒストグラムを表示する。
- 対角線以外には、対応する2つのパラメータの散布図を表示
- N x N のグラフが、どのパラメータか分かるよう、パラメータ名のラベルを表示する。
- サイズは固定とし、パラメータが多くなってグラフが大きくなってもスクロールバーでスクロールできるようにする。
工夫した点
- スクロールできるようPanelの中にChartコントロールを配置した。
- 並べて表示するといっても、Chartコントロールは1つとし、ChartAreaを自前で N x N 個配置した。
- 大量のグラフを表示するため、軸の表示はしないようにし、グラフを隙間なく並べて、俯瞰しやすいようにした。
- そのままでは、グラフとグラフの間に境界が表示されないので、Chartコントロールの背景を黒にし、少し隙間がでるように配置することで境界っぽく表示されるようにした。
- パラメータ名のラベルは、ChartAreaを並べる場所を少し開け、グラフィックスオブジェクトで直接背景色およびラベル名を描画した。
ソース
さて、ソースは以下の通りだ。データを準備するところはバッサリカットしているのでそのままでは動かないが、上の工夫を見れば大体やっていることは分かるだろう。
using MathNet.Numerics.Statistics; using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; using System.Windows.Forms.DataVisualization.Charting; namespace Kimisyo.Forms { public partial class ScatterPlotForm : Form { public int width = 1500; public int height = 1500; public int margin = 20; int nBuckets = 20; // パラメータIDのリスト List<int> parameterIds = new List<int>(); // パラメータ名のリスト List<String> parameterNames = new List<String>(); // パラメータID毎にデータが格納されたディクショナリ Dictionary<int, Dictionary<int, Double>> parameterId2ValueMap = new Dictionary<int, Dictionary<int, Double>>(); public ScatterPlotForm() { InitializeComponent(); } private void ScatterPlotForm_Load(object sender, EventArgs e) { chart1.BackColor = Color.Black; chart1.Width = this.width; chart1.Height = this.height; chart1.Series.Clear(); chart1.Legends.Clear(); chart1.ChartAreas.Clear(); float margin_rate = ((float)margin / (float)chart1.Width) * 100; Random random = new System.Random(); for (int i = 0; i < parameterIds.Count; i++) { for (int j = 0; j < parameterIds.Count; j++) { String name = Convert.ToString(i) + "_" + Convert.ToString(j); ChartArea ca = new ChartArea(name); ca.AxisX.Enabled = AxisEnabled.False; ca.AxisY.Enabled = AxisEnabled.False; ca.Position.X = margin_rate + ((float)j / (float)parameterIds.Count) * (100 - margin_rate); ca.Position.Y = margin_rate + ((float)i / (float)parameterIds.Count) * (100 - margin_rate); ca.Position.Width = ((float)1/(float)parameterIds.Count) * (100 - margin_rate) - (float)0.2; ca.Position.Height = ((float)1/(float)parameterIds.Count) * (100 - margin_rate) - (float)0.2; this.chart1.ChartAreas.Add(ca); Series series = new Series(); series.ChartArea = name; // ScatterPlotを作成 if (i != j) { series.ChartType = SeriesChartType.Point; series.MarkerColor = Color.Blue; series.MarkerStyle = MarkerStyle.Circle; series.MarkerSize = 2; // データ int parameterIdX = parameterIds[j]; int parameterIdY = parameterIds[i]; Dictionary<int, double> valueMapX = parameterId2ValueMap[parameterIdX]; Dictionary<int, double> valueMapY = parameterId2ValueMap[parameterIdY]; foreach (int sampleId in valueMapX.Keys) { DataPoint dp = new DataPoint(valueMapX[sampleId], valueMapY[sampleId]); series.Points.Add(dp); } } // ヒストグラムを作成 else { series.ChartType = SeriesChartType.Column; series.Color = Color.Blue; int parameterId = parameterIds[i]; Dictionary<int, double> valueMap = parameterId2ValueMap[parameterId]; Histogram hist = new Histogram(valueMap.Values, nBuckets, -50, 50); for (int k = 0; k < nBuckets; k++) { double mid = Math.Round((hist[k].UpperBound + hist[k].LowerBound) / 2, 1); series.Points.Add(new DataPoint(mid, hist[k].Count)); } } chart1.Series.Add(series); } } } /// ラベルを表示する(PostPaintイベント) private void chart1_PostPaint(object sender, ChartPaintEventArgs e) { ChartGraphics cg = e.ChartGraphics; Graphics g = cg.Graphics; SolidBrush myBrushWhite = new SolidBrush(Color.White); g.FillRectangle(myBrushWhite, 0, 0, margin-3, this.height); g.FillRectangle(myBrushWhite, 0, 0, this.width, margin-3); for(int i=0; i<parameterIds.Count; i++) { float margin_rate = ((float)margin / (float)chart1.Width) * 100; float x_rate = margin_rate + ((float)i / (float)parameterIds.Count) * (100 - margin_rate); int x = Convert.ToInt32(this.width * x_rate/100); SolidBrush myBrushBlack = new SolidBrush(Color.Black); Font font = new Font("MS UI Gothic", 6); g.DrawString(parameterNames[i], font, myBrushBlack, new Point(x, 3)); } // 原点を移動 g.TranslateTransform(0, this.height); // -90度回転 g.RotateTransform(-90f); // 文字列を描画 for(int i=0; i<parameterIds.Count; i++) { float margin_rate = ((float)margin / (float)chart1.Width) * 100; float x_rate = ((float)i / (float)parameterIds.Count) * (100 - margin_rate); int x = Convert.ToInt32(this.width * x_rate/100); SolidBrush myBrushBlack = new SolidBrush(Color.Black); Font font = new Font("MS UI Gothic", 6); g.DrawString(parameterNames[parameterIds.Count-i-1], font, myBrushBlack, new Point(x, 3)); } // 元に戻す g.ResetTransform(); } } }出来上がり図
以下は1500x1500のサイズに20個のパラメータを並べてみた図である。表示に数秒またされるが、表示後のスクロールは快適である。
図は小さいが、気になるグラフをクリックすると、詳細なグラフを表示する等の処理を組み込めば全く問題ないだろう。課題
課題としては、グラフの境界線の上にプロットされているように見えてしまい見ずらい。また、X軸、Y軸の範囲が統一されていないなどの問題がある。まだ、Chartコントロールの使い方の理解が不十分なため、今後様々なカスタマイズに挑戦してみたい。
参考
- 投稿日:2020-03-15T22:00:25+09:00
C# から Rust のインスタンスのメソッドを呼び出す
はじめに
この記事は次の記事の C# → Rust 版です。
普通に Rust の関数を extern で公開するだけ、という話ではなく構造体に対して impl キーワードで実装したメソッドを C# から呼び出します。 「ネイティブ (Rust) 側のリソース管理を呼び出し元の C# で行いたい」 を達成する事を目的とします。
基本的には C# 側は元記事とほぼ同じ (一部パラメーターを int から uint に変えましたがそれ以外は同じ) で、 Rust 側を C++ と同じ実装になるようにしています。元記事では "パターン 2" と呼んでいる比較的穏当な方になります。
環境は次の通りです。
- Windows 10
- Visual Studio 2019
- CLion 2019.3
- .NET Core 3.1
- rustc 1.41.0
リポジトリはこちらです。
Rust 側でインスタンスを生成する
Rust は他の言語と同様にメモリはスタックかヒープに確保されます。特に意識しない場合はスタックでヒープに確保したい場合は Box を使います。
Rust はメモリ安全性を強く意識されている言語で普通に使っている限りでは確保したメモリは不要になったら自動で解放されますが、今回の場合は C# 側が主体になっているので Rust の方で破棄されたりしたら困るわけです。
このような場合、 Box::into_raw, Box::from_raw を使用します。 into_raw で管理対象から外して raw pointer を取得します。 from_raw は逆で raw pointer から Box を生成します。
impl RustSample { fn new() -> RustSample { RustSample { number: 0, str: String::new() } } unsafe fn new_raw() -> *mut RustSample { Box::into_raw(Box::new(RustSample::new())) } fn destroy(&mut self) { unsafe { Box::from_raw(self as *mut RustSample); } } }new_raw では Box 化した RustSample を into_raw で raw pointer にしたものを返します。
destroy では from_raw で再度 Box 化し、そのままスコープを抜けることで解放をします。この流れでは drop trait の実行もされますので、インスタンスの管理以外については通常の Rust とほぼ同じように扱えています。
当たり前の話ですが、 into_raw で raw pointer 化したものは from_pointer で Rust に戻さないとリークします。要・自己責任です。
malloc / free を使う
実は当初 into_raw, from_raw の存在に気づいていなくて malloc / free でやっていました。このやり方は面倒な上にコンストラクタ (new) と drop trait の呼び出しを記述しない事が十分考えられます。のでやらない方が無難でしょう。
関数テーブルを用意する
C++ 版では関数テーブルを自前で生成してそれを配列で返すというやり方をしました。今回もそれを踏襲します。
Rust の関数ポインタは次のようにキャストすると raw pointer (= 外部に出せる) として取得できます。一回 Rust の関数ポインタにキャストし、その次に普通のポインタにキャストするのがポイントです。
fn foo() { println!("foo"); } let p = foo as fn() as *const ();Rust の関数は C++ のインスタンスメソッドと異なりどのような時でも C 言語関数と互換性があります。ので C++ の時のような細工は不要で普通に配列に並べることができます。
let table = &[ RustSample::destroy as fn (&mut RustSample) as *const(), RustSample::get_current_value as fn (&RustSample) -> i32 as *const(), ];create_rust_sample_instance では渡されたバッファに対して RustSample のインスタンスと関数テーブルの内容をコピーします。
関数テーブルは必要ない?
C++ の時は上記の通りそれをやる強い理由がありました (C++ のインスタンスメソッドの関数ポインタは通常ポインタと互換性がなかった) が、 Rust にはそれがなく
impl RustSample { fn add(&mut self, num: i32) { self.number += num; } } #[no_mangle] extern fn RustSample_add(s: &mut RustSample, num: i32) { s.number += num; }この二つは ABI 的には等価であるので、 RustSample_add など各メソッドを DllImport で入力するようにしても問題ないですし C# 側を含めた記述量的にも結果的に少なくなるかもしれません。割と趣味的なところかと思いますが、
- 型に対するメソッドは impl を使って実装するのが一般的と思います
- extern で定義した関数を踏み台にして impl のメソッドを呼ぶのは記述量が増えて本末転倒になりそうです (マクロでなんとかなりそうではあります)
- DllImport ではなくコードで DLL の動的ロードをしようとした時、結局手間になりそうです
また、大量に export している関数があるのが個人的にはあんまり好きではないので、そういった観点からでも私は関数テーブルを作るやり方をとるかと思います。
C# から呼び出す
こちらは C++ 版と同じです。
[DllImport("cs_rust_invoke", EntryPoint = "create_rust_sample_instance")] static extern uint CreateRustSampleInstance(IntPtr[] buffer, uint bufferSize);Rust は命名規則が snake case なので DllImport 時に調整するのがよいと思います。
public RustSample() { uint bufferSize = CreateRustSampleInstance(null, 0); var buffer = new IntPtr[bufferSize]; if (CreateRustSampleInstance(buffer, bufferSize) != bufferSize) { throw new Exception(); } _self = buffer[0]; _fnDestroy = Marshal.GetDelegateForFunctionPointer<FnAction>(buffer[1]);テーブルの 0 番に Rust 側のインスタンスポインタ、 1 番以降が関数ポインタなので delegate 化して C# から使います。
public void Add(int value) => _fnAdd(_self, value);第一引数が渡された struct のインスタンスへのポインタなのでそれを指定します。
public void Dispose() { _fnDestroy?.Invoke(_self); _fnDestroy = null; _self = IntPtr.Zero; }Dispose に RustSample::destroy を呼び出す記述を実装し、ここから Rust 側のインスタンスを破棄するようにします。
文字列の扱い
delegate uint FnAppendChars(IntPtr self, [MarshalAs(UnmanagedType.LPUTF8Str)] string s);Rust の文字列は UTF-8 なので UnmanagedType.LPUTF8Str を指定するのがよいと思いますが、この列挙値は .NET Framework 4.7 / .NET Core 1.1 以降にあるもののようなので環境によっては使えない場合もあるので注意が必要です (.NET Standard 2.0 でもない) 。
実行
C++ 版と同じになりました。また、 new, print_chars, drop でそれぞれポインタ値を合わせて出力するようにしていますが、それぞれ適切に呼ばれていることが確認できます。
trait object を使う
struct 自体ではなく trait object をインスタンスへの参照として利用しようとした場合、 trait object はこれまで扱ってきた普通のポインタ (raw pointer) と互換性がありません。
によると trait object は
pub struct TraitObject { pub data: *mut (), pub vtable: *mut (), }という表現になっていて実際に通常のポインタ 2 個分のサイズであることを確認しています。
よって trait object を使用する場合、 C# 側もそれを扱えるような構造にする必要があります。具体的には第一引数に渡すものを trait object と互換性がある形式にする必要があります。が、実際のところ FFI で trait object を必要とするケースはないと思いますのでとりあえず気にする必要はないかと思います。一応そういった注意点があるということだけ記載しておきます。
呼び出し規約について
今回 Windows x64 しか試していないので確認していませんが、実際のところは呼び出し規約に配慮する必要があります。が、呼び出し規約が面倒くさいのは Windows x86 くらいだと思うのでとりあえず見なかったことにしています・・・
おわりに
.NET 用のネイティブコードのリソース管理を C# 主体で行いたいというケースはよくありますので、 Rust でのやり方を明確にしておくことは Rust を使っていくにあたって個人的には重要なことです。
Rust はまだ始めたばかりなので結構試行錯誤をしましたが、最終的にはかなりシンプルなコードになりました。これをベースに作業していけるかなと思います。
- 投稿日:2020-03-15T21:03:13+09:00
[paiza レベルアップ問題集] 日付セット 連休を伸ばす2 (paizaランク A 相当)
paizaのレベルアップ問題集に挑戦。
考え方
連休の初日を1日ずつずらしていく。
前のループにおける①連休の初日が休日か営業日か、②連休の最終日がいつか、③何連休であったかを利用する。class Program { static int howManyHolidays(int firstDay,int N, int L, int[] list) { int counter = 0; for(int i = firstDay; i < N; i++) { if(list[i] == 1) { counter++; } else if(L > 0) { counter++; L--; } else { break; } } return counter; } static void Main(string[] args) { string[] input = Console.ReadLine().Trim().Split(' '); int N = int.Parse(input[0]), L = int.Parse(input[1]); input = Console.ReadLine().Trim().Split(' '); int[] list = new int[N]; //1 -> 休業日、0 -> 営業日 for(int i = 0; i < N; i++) { list[i] = int.Parse(input[i]); } //連休の初日が1日目の場合求める int firstDay = 0; int temp = howManyHolidays(firstDay, N, L, list); int max = temp; int lastDay = firstDay + temp - 1; //連休の初日がfirstDayの場合の連休の最終日 //連休の初日を一日ずつずらす for(int i = 1; i < N; i++) { temp--; //連休の初日の前日営業日の場合、(lastDay+1)日目から後1回年休が使える int paidDay; if(list[i - 1] == 0) { paidDay = 1; } else { paidDay = 0; } for(int j = lastDay + 1; j < N; j++) { if(list[j] == 1) { temp++; }else if(paidDay == 1) { temp++; paidDay--; } else { lastDay = j - 1; break; } } if(temp > max) { max = temp; } } Console.WriteLine(max); } }youtubeの解説動画を視聴後に書いたコード
class Program { static int howManyHolidays(int firstDay,int N,int L,int[] list) { //L日は休める -> 0の日を1に書き換えられる × L回 int one = 0; //休業日を数える変数 int zero = 0; //年休を使った日数を数える変数 while(firstDay + one + zero < N) { if(list[firstDay] == 1) { one++; } else { if(zero == L) { break; } zero++; } } return one + zero; } static void Main(string[] args) { string[] input = Console.ReadLine().Trim().Split(' '); int N = int.Parse(input[0]), L = int.Parse(input[1]); input = Console.ReadLine().Trim().Split(' '); int[] list = new int[N]; //1 -> 休業日、0 -> 営業日 for (int i = 0; i < N; i++) { list[i] = int.Parse(input[i]); } int result = 0; int one = 0; int zero = 0; for (int left = 0, right = 0; right < N; right++) { if(list[right] == 1) { one++; } else { zero++; while(zero > L) { //leftを増やす、oneとzeroを調整 if(list[left] == 1) { one--; } else { zero--; } left++; } } if(one + zero > result) { result = one + zero; } } Console.WriteLine(result); } }
- 投稿日:2020-03-15T14:25:41+09:00
Unity入門:今週悩んだことと。単純なバグに長時間時間を取られてしまった。
はじめに
今週勉強したことをまとめています。
初めまして、HITOMI(男)です。現在Unityを勉強しています。今週勉強したこと、悩んだことをここに書き残します。先週悩んでいた、UIのアニメーションをうまく実装することができました。うまくいかなかった原因は私の操作ミスだと考えています。できるだけこういったミスを少なくしていきたいなと考えています。
さて、私はUnityの勉強のため3D横スクロールゲームを開発しています。そこで今週悩んだことや、詰まったとこをここに書き残そうと思います。
今週(3月8〜3/14日)詰まったところ。
・読み込みクラスの単純なバグの解決に1日以上ほどかかった。
・GUIのアニメーション遷移でのバグ。
・イベント読み込みクラスについて単純なバグの解決に1日以上かかった。
データ読み込みクラス(前回紹介したの部分)でうまく読み込めないバグが発生しました。結論から言うとバグは解決したのだが、バグの場所の特的と修正に1日以上時間かかってしまった。そのバグの原因はfor文の Length-1の −1をつけ忘れていたと言う単純なミスでした。こんな単純なミスで時間を取られた時は、「うわーーーーーー」ってなります。今後こんな単純なミスはできるだけ無くしていきたいと思います。何か対策を取らなくては。
//ここが抜けていた。 for(var i; I<m_List.Count -1; i++) { }GUIのアニメーション遷移でバグが発生した。
漫画などによく使われている、吹き出しのアニメーションをプログラムで実装しています。内容は吹き出しがフェードインしたとに、テキストが一定間隔で表示され、テキストが終了するとフェードアウトする。といったものです。今回発生したバグは2周目のフェードインの時にフェードインが終わった際に吹き出しが消えるといったものだ。Unity上で再生を一時中断してCanvasを確認したところ、ImageのRectTransformのscaleが0,0,0になっていた。アニメーション部分がうまく動いていることを考えると、コルーチンとsetActiveの部分に問題がある思っている。実際setActiveを常にtrueにすると問題なく動く。色々悩んだ末、画面から非表示にする処理をsetActiveを使わずにscale0を用いることにした。こうするとうまく動くことができた。この部分はまた、全体が完成してからもりもり作り込んでいこうと考えている。
イベント読み込みクラスに改良
外部のテキストから以下のようなファイルを読み込み
イベント等を実装するクラスを作っています。
(ゲームプログラマになる前に覚えておきたい技術参考)<EventData name=“event01”,type = “count”,trigger=“”,nextEvent=“event02”> <CountDown position = "hairScreen,hairScreen,0.0f"> </EventData> <EventData name=“event02”,type = “speak”,trigger=“”,nextEvent=“event03”> <TriggerObject type = “box” , event=“event”, position = "12.4,7.0,0.0”, size="1.0,1.0,1.0"></TriggerBox> //行動制限 <SpeechBubble position="12.4,7.0,0.0"> <Speech speech="左右の矢印を押すと移動できるよ” , type=“timer”></Speak> <Speech speech="上ボタンを押すとジャンプできるよ” , type=“wait”></Speak> <Speech speech="下ボタンを押しても何もないよ。” , type=“wait”></Speak> </SpeechBubble> </Event> <EventData name=“event03”,type = “clear”,trigger=“”,nextEvent=“none”> 略 </Event>前回考えていたテキストの内容をプログラムに反映しました。その結果、カウントダウンからスタートクリアまで外部テキストで調節できるようになりました。と言っても、実際はカウントダウンとクリアは一枚絵を貼り付けただけで実際にアニメーションまでは実装できていません。来週はこれにカメラの制御とプレイヤーの動きを制御できるよう機能を追加したいと考えています。
さいごに
今週は思っていたよりも進捗が遅れています。多分3月中の完成はちょっと難しいのかなと考えています。来週はスタートからクリアまで一通り遊べるようにすることと、メモリリークをなくすところまで進めたいと考えています。
- 投稿日:2020-03-15T13:05:42+09:00
【中級者向けUnity講座】RPG編 part4『キャラにカメラを追従させる』
はじめに
皆様こんにちは。個人でゲーム制作をしております、kenと申します。
この講座では、実際にゲームを作りながら「こういう事をするなら、こういう風にすると良いと思うよ。詳しくは自分で調べてね。」という感じで、ゲームの作り方、Unityの機能やアセット等を軽く紹介して行きます。今回は、カメラをオブジェクトに追従させる方法をいくつか紹介します。
StandardAssetsを使う
多分これが1番簡単だと思います。
StandardAssetsに簡単なカメラのスクリプトがあるので、アセットストアからStandardAssetsをインポートして、それを使いましょう。使い方は調べなくても見れば分かるくらい簡単ですが、一応使い方を解説している記事を貼っておきます。
Unityでカメラの設定とキャラクターの追従をさせるCinemachineを使う
Cinemachineを使うと高機能なカメラがノンコーディングで作れちゃいます。めちゃくちゃ多機能で、3Dも2Dもあらゆるタイプのカメラが作れます。
どんな事ができるの?
まずは、どんな事が出来るのか、公式のサンプルの中からゲームでよく使いそうなものを紹介します。
ちなみに、このサンプルで使われているキャラ移動のスクリプトですが、キー入力の判定がFixedUpdate内に書かれているので、ジャンプキーの反応が悪いです。part2でも言いましたが、キーの入力判定をFixedUpdate内に書かないようにしましょう。
2DConfiner
2Dでキャラをスムーズに追従しつつ、壁の向こう側を移さないようにするカメラです。
2DTargetGroup
2Dで複数の対象がカメラに収まるようにズームするカメラです。
Free Look character
Free Look collider
Noise
StateDrivenCamera
走っている間だけカメラをゆれるカメラに切り替えるサンプルです。もう少し詳しく説明すると、Cinemachineには仮想カメラというものを複数用意し、それらをスムーズに切り替えられる機能があるので、キャラクターの状態ごとに別のカメラに切り替えるサンプルですね。三人称視点と一人称視点の切り替えなんかにも応用できそうです。
他にもいろんなことが出来ますし、カメラの挙動はかなり細かく設定することが出来ます。
使い方
Cinemachineの基本的な使い方は以下の記事が分かりやすいです。
【Unity】Cinemachine入門!スクリプトなしでカッコいいカメラワークを作るサンプルの種類がめちゃくちゃ豊富なので、Cinemachineを詳しく知りたい方は、自分でサンプルをインポートして動かしてみるのがオススメです。
オブジェクトを動かしてみよう!
『実際にゲームを作りながら紹介していく』と言いながら、いまだに何も作ってませんでしたが、そろそろ動くものを作りたいと思います。
今まで紹介して来た機能の中から以下の機能を使ってカプセルを動かしてみます。
・移動/NavMesh
・入力の取得/新しいInputSystem
・カメラ/CinemachineNavMeshの設定
まずは、適当に作ったTerrainにNavMeshをBake
適当にカプセルを置いてNavMeshAgentをアタッチ(設定はデフォルトのまま)
InputSystemの設定
InputSystemのサンプルに付いているSimpleControlsをそのまま使います。
Cinemachineの設定
仮想カメラを作成。
メインのカメラにCinemachineBrainをアタッチします。Priorityの高い仮想カメラが自動で選択されるので、特に何も設定しなくても使えます。
スクリプト
移動する方向を受け取って移動させるクラスと、入力の方向とカメラの向きから移動する方向を算出して移動させるクラスに渡すクラスに分離しました。これをカプセルにアタッチします。
IMoveComponent.csusing UnityEngine; public interface IMoveComponent { void Move(Vector3 velocity); }MoveAgent.csusing UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(NavMeshAgent))] public class MoveAgent : MonoBehaviour, IMoveComponent { [SerializeField] float _speed = 3f; NavMeshAgent _agent; void Start() { _agent = GetComponent<NavMeshAgent>(); } public void Move(Vector3 velocity) { _agent.Move(velocity * _speed); } }PlayerController.csusing UnityEngine; [RequireComponent(typeof(IMoveComponent))] public class PlayerController : MonoBehaviour { Camera _mainCamera; SimpleControls _controls; IMoveComponent _moveComponent; void Awake() { _controls = new SimpleControls(); } void Start() { _mainCamera = Camera.main; _moveComponent = GetComponent<IMoveComponent>(); } void OnEnable() { _controls.Enable(); } void OnDisable() { _controls.Disable(); } void OnDestroy() { _controls.Dispose(); } void Update() { var inputVector = _controls.gameplay.move.ReadValue<Vector2>(); var cameraVector = _mainCamera.transform.forward; var velocity =Quaternion.LookRotation(cameraVector, Vector3.up)* new Vector3(inputVector.x, 0, inputVector.y); velocity *= Time.deltaTime; _moveComponent.Move(velocity); } }完成品
まとめ
やっとオブジェクトを動かせるようになりましたが、カプセルを動かしても面白く無いですね。
という訳で次回は、キャラクターのセットアップの仕方を紹介します。
次回⇨4/5(日)投稿予定(他に書きたい記事があるので2週間お休みします)
- 投稿日:2020-03-15T10:35:32+09:00
paizaレベルアップ問題集 連休を伸ばす1 (paizaランク B 相当)
自力で解いたコード
class Program { static void Main(string[] args) { string[] input = Console.ReadLine().Trim().Split(' '); int N = int.Parse(input[0]), L = int.Parse(input[1]); //N日のリスト、L日まで連休が使える input = Console.ReadLine().Split(' '); //1なら休業日、0なら営業日 int max = 0; //連休の初日を1日ずつずらしながら、取れる休みを求める for(int i = 0; i < N; i++) { int temp = 0; int paidHolidays = L; for(int j = i; j < N; j++) { if(input[j] == "1") { temp++; }else if(paidHolidays > 0) { //有休が残っている場合 temp++; paidHolidays--; } else { break; } } if(temp > max) { max = temp; } } Console.WriteLine(max); } }youtubeの解説動画を視て書いたコード
class Program { static int howManyHolidays(int firstDay,int N,int L, int[] list) { //元々休みの日と有休をとった日の合計を求める int one = 0; int zero = 0; while(firstDay + one + zero < N) { if(list[firstDay + one + zero] == 1) { one++; } else { if(zero == L) { break; } zero++; } } return one + zero; } static void Main(string[] args) { string[] input = Console.ReadLine().Trim().Split(' '); int N = int.Parse(input[0]), L = int.Parse(input[1]); //N日のリスト、L日まで連休が使える input = Console.ReadLine().Split(' '); //1なら休業日、0なら営業日 int[] list = new int[N]; for(int i = 0; i < N; i++) { list[i] = int.Parse(input[i]); } int max = 0; for(int firstDay = 0; firstDay < N; firstDay++) { int temp = howManyHolidays(firstDay, N,L,list); if(temp > max) { max = temp; } } Console.WriteLine(max); } }
- 投稿日:2020-03-15T05:15:23+09:00
【Unity】Raycastで簡単に弾道に沿った当たり判定を実装する方法
はじめに
銃を扱うゲームにおいて当たり判定の実装は不可欠ですが、リアルな弾速で弾を飛ばすと簡単にすり抜けてしまいます。
銃口からRayを飛ばせば弾速は出せますが、リアルさを求めているのに弾道が直線というのも残念ですよね。
今回はRaycastの速さと、リアルな弾道の放物線を両立する方法を紹介したいと思います。
(Unity歴が浅いので、もしかしたらポピュラーな方法かもしれません。また、もっと簡単でいい方法(CollisionDetectionModeとか)があるのかもですがご容赦ください。)方法
考え方は至って単純で、弾の前フレームでの位置から今のフレームでの位置にRaycastを飛ばして当たりを判定します。
コード
Vector3 prepos; void Start(){ prepos = transform.position; //前フレームでの位置 } void Update(){ Vector3 pos = transform.position; //今フレームでの位置 Ray ray = new Ray(prepos, (pos - prepos).normalized); //前フレームの位置から今の位置の向きにRayを飛ばす RaycastHit hit; //これがあると当たったオブジェクトの情報が扱える if (Physics.Raycast(ray, out hit, (pos - prepos).magnitude, 1 << 8)){ //判定ししたいオブジェクトはレイヤーを分けておく(この場合はレイヤー8) Debug.Log("Hit!"); //当たったらコンソールにて報告 } prepos = pos; //今のフレームの位置を次のフレームにおける前のフレームの位置として保存 }注意
この方法ではRay(すごくほそい)を使っているため、オブジェクトのpositionの座標が通過しないと当たりが検出されません。
そのため弾丸など小さな物体には有効ですが、大きさのあるものに使うのはおすすめしません。まとめ
需要があるかわからない当たり判定の紹介でした。問題等あれば教えて下さい。