C/C++から始めた人の、C#のref/out/in修飾子の用途
概要
ref: 引数を参照で渡し、値の書き換えと、関数内で参照先の書き換えの両方を許可したい時に使う
out: 関数の引数に計算結果を返したい時に使う
in: 引数を参照で渡し、値の書き換えのみ許可したい時に使う
前提知識:C#における値型と参照型の違いについて
C/C++からプログラムを始めた私にとって、C#の最初のつまづきポイントがここだった。C#プログラミングにおいて、値型と参照型を意識することは非常に重要である。
値型と参照型の違いは単純で、「その型の値を直接保持するか否か」である。以下に簡単な例を示す。
// C#ではstruct(構造体)は値型である public struct T { public int a, b, c; public T(int _a, int _b, int _c) { a = _a; b = _b; c = _c; } public override string ToString() { return $"{a}, {b}, {c}"; } } // C#ではclassは参照型である public class U { public int a, b, c; public U(int _a, int _b, int _c) { a = _a; b = _b; c = _c; } public override string ToString() { return $"{a}, {b}, {c}"; } } // 値型の変数を=で代入した時の挙動を見る T a1 = new T(1, 1, 1); T a2 = a1; T a3 = a2; a1.a = 2; Console.WriteLine($"a1: {a1}"); Console.WriteLine($"a2: {a2}"); Console.WriteLine($"a3: {a3}"); Console.WriteLine("======================="); // 参照型の変数を=で代入した時の挙動を見る U b1 = new U(1, 1, 1); U b2 = b1; U b3 = b2; b1.a = 2; Console.WriteLine($"b1: {b1}"); Console.WriteLine($"b2: {b2}"); Console.WriteLine($"b3: {b3}");
出力結果
a1: 2, 1, 1 a2: 1, 1, 1 a3: 1, 1, 1 ======================= b1: 2, 1, 1 b2: 2, 1, 1 b3: 2, 1, 1
値型の変数は、値そのもの(今回の例でいうと int a, b, cの3つのint型の値
)を保持する。そのため、 =
を使って代入した時、値すべてをコピーして代入先に格納するという挙動になる。
一方参照型の変数は、値の実体がどこにあるかを示す参照情報だけを持ち、実体は別の場所(ヒープ)に用意する。そのため、 =
を使って代入すると参照情報だけをコピーするので、上記コードでの変数 b1, b2, b3
は同じ値の実体を指していることになる。
値型と参照型について、詳しくは以下のWebサイトを見てほしい。
関数の引数は値渡し
C#で引数付きの関数を呼び出す時、関数には値そのものが渡される。これを一般に値渡しと呼ぶ。
では、値型と参照型で「値渡し」した時の挙動がどう変わってくるか見てみる。
void ModifyValueStruct(T t) { t.a = 10; // 値型引数の値を関数内で書き換える } void ModifyValueClass(U u) { u.a = 10; // 参照型引数の値を関数内で書き換える } T t = new T(1, 1, 1); U u = new U(1, 1, 1); ModifyValueStruct(t); ModifyValueClass(u); Console.WriteLine($"T t: {t}"); Console.WriteLine($"U u: {u}");
出力結果
T t: 1, 1, 1 U u: 10, 1, 1
値型の変数を関数に値渡しすると、「値そのもの」がコピーされ関数に引き渡される。つまり、関数呼び出し側の変数 t
と、関数内の変数 t
は別モノ。関数内でいくら値型引数の値を書き換えても、関数呼び出し元には、その変更は伝わらない。
一方参照型の変数を関数に値渡しすると、「値の参照情報」だけがコピーされ、関数に引き渡される。つまり、関数呼び出し側の変数 u
と 関数内の変数 u
は同じ値を指している。したがって、関数内で参照型引数の値を書き換えると、関数呼び出し元の変数が指す値も書き換わる。
ref/out/inを使った参照渡し
ref/out/in修飾子は、「関数の引数を参照渡し」したい時に使われる。
ref/out/inを使いたいときは、関数宣言側と、引数引数の型名の前にrefなどの修飾子をつければよい。
void RefFunc(ref int i) { /* ... */ } // 型名の前にref修飾子をつける // ... int num; RefFunc(ref num); // 呼び出すときにもref修飾子をつける(注:inはつけなくてもよい)
ref/out/in修飾子はすべて「引数の参照渡し」を実現するためのキーワードだが、それぞれ細かく挙動が変わってくる。以下にその違いをまとめた表を載せた。
キーワード | 引数として渡す前に初期化が必要か | メソッド内で参照先の値の書き換えが可能か | 呼び出し時の修飾子付与 |
---|---|---|---|
ref | 必須 | 可能 | 必須 |
out | 不要 | 必須 | 必須 |
in | 必須 | 不可 | 任意 |
値型を参照渡しする例
void Func(T t) { t.a = 2; // 値渡しされた値型を関数内で書き換えても呼び出し元には伝わらない } void Func2(T t) { t = new T(2, 2, 2); // 値渡しされた値型のインスタンス差し替えは呼び出し元に伝わらない } void RefFunc(ref T t) { t.a = 2; // ref引数の値を書き換えると呼び出し元にも伝わる } void RefFunc2(ref T t) { t = new T(2, 2, 2); // 引数が指すインスタンスを書き換えることも出来る } void OutFunc(out T t) { t = new T(3, 3, 3); // out引数は関数内で必ず初期化しなければならない } void InFunc(in T t) { // t.a = 2; // in引数の値を書き換えることはできない } T t; t = new T(1, 1, 1); Func(t); Console.WriteLine($"Func() -> {t}"); t = new T(1, 1, 1); Func2(t); Console.WriteLine($"Func2() -> {t}"); t = new T(1, 1, 1); RefFunc(ref t); Console.WriteLine($"RefFunc() -> {t}"); t = new T(1, 1, 1); RefFunc2(ref t); Console.WriteLine($"RefFunc2() -> {t}"); t = new T(1, 1, 1); OutFunc(out t); Console.WriteLine($"OutFunc() -> {t}"); t = new T(1, 1, 1); InFunc(in t); Console.WriteLine($"InFunc() -> {t}");
出力結果
Func() -> 1, 1, 1 Func2() -> 1, 1, 1 RefFunc() -> 2, 1, 1 RefFunc2() -> 2, 2, 2 OutFunc() -> 3, 3, 3 InFunc() -> 1, 1, 1
参照型を参照渡しする例
void Func(U u) { u.a = 2; // 値渡しされた参照型引数の値を書き換えると、呼び出し元に伝わる } void Func2(U u) { u = new U(2, 2, 2); // 値渡しされた参照型引数の指す先を変えても、呼び出し元には伝わらない } void RefFunc(ref U u) { u.a = 2; // 値を書き換えると呼び出し元にも伝わる } void RefFunc2(ref U u) { u = new U(2, 2, 2); // 引数が指すインスタンスを書き換えることも出来る } void OutFunc(out U u) { u = new U(3, 3, 3); // ouu引数は関数内で必ず初期化しなければならない } void InFunc(in U u) { // u.a = 2; // in引数の値を書き換えることはできない } U u; u = new U(1, 1, 1); Func(u); Console.WriteLine($"Func() -> {u}"); u = new U(1, 1, 1); Func2(u); Console.WriteLine($"Func2() -> {u}"); u = new U(1, 1, 1); RefFunc(ref u); Console.WriteLine($"RefFunc() -> {u}"); u = new U(1, 1, 1); RefFunc2(ref u); Console.WriteLine($"RefFunc2() -> {u}"); u = new U(1, 1, 1); OutFunc(out u); Console.WriteLine($"OutFunc() -> {u}"); u = new U(1, 1, 1); InFunc(in u); Console.WriteLine($"InFunc() -> {u}");
出力結果
Func() -> 2, 1, 1 Func2() -> 1, 1, 1 RefFunc() -> 2, 1, 1 RefFunc2() -> 2, 2, 2 OutFunc() -> 3, 3, 3 InFunc() -> 1, 1, 1
ref/out/inの使い道
refは、既に値が入っている変数を書き換える時に使う。
気をつけたいのは、参照型の引数を書き換えたいからという理由でrefをつける必要はないこと。参照型の値渡しで値の書き換えは可能なので、その用途でrefをつける必要はない。
参照型引数にrefをつけるのは、「引数が指す先を変えたい(新しいインスタンスを指すようにしたい)」ときのみ。.NET APIでは、 Array.Resize()
で参照型引数にref修飾子を使っている。
outは、関数の返り値を複数持たせたいような時に使うことが多い印象。C++では「非constポインタ渡し」で実現する処理。ただしrefとは異なり、関数内で新たに初期化されるので、まだ宣言しかしていない変数を渡すべき。
ちなみに、下のように変数宣言をoutによる参照渡しと同時にこなすこともできる(outを使うときはこの書き方が安全だと思う)。
bool isValid = Calclate(out int num);
inは、inを使わければならない特別なプログラムがあるわけではない。
inの用途はズバリ「値型引数のコピーコスト削減」。
値型を値渡しする時、その値のコピーが作られる。この値が4Byteのint程度ならコピーにかかるコストは無視できるほど小さい(だろう)。しかし、もし巨大な値型(struct)を値渡ししたら巨大なコピーが作られてしまい、その分コピーに必要な時間もメモリも増えてしまう。こういう時にinは使われる。C++でいうところの「&を使ったconst参照渡し」に相当すると考えて良い。
が、コピーに無視できないほどのコストがかかる巨大な値型(struct)を作るべきではないということは忘れてはならない(そうしないとならない特別な事情って何かあるんでしょうか。知ってる方がいたら教えていください)。
まとめ
C/C++からプログラムを始めた人にとって、C#の値型と参照型の違いは必ず抑えておきたい。それを理解した上で、ref/out/inを使いたい。
UnityのHDRPでSNNフィルタを実装する
最近UnityのHDRPを使って色々と実験をしている。今回はその実験中に作ったSNNフィルタポストプロセスの実装メモ。今回実装にはGtihubで公開されている「HDRP-Custom-Passes」に付属するシーンを使用した。
環境
Unity 2020.3.1f1 Personal
High Definition RP ver.10.3.2
SNNフィルタとは
SNNフィルタ(Symmetric Nearest Neighbor Filter)は、画像を油彩画のような見た目に変化させるフィルタである。中のアルゴリズムについては、既にわかりやすく説明されているサイトがあるので、ここでは省略する。
実装
PostProcessを管理するC#クラスを作成する
Create > Rendering > C# Post Process Volume からPostProcessを管理するC#クラスを作成する。ここでは「SNNFilter.cs」と名付けた。
SNNFilterクラスの中身は以下の通り。
using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.HighDefinition; using System; [Serializable, VolumeComponentMenu("Post-processing/Custom/SNNFilter")] public sealed class SNNFilter : CustomPostProcessVolumeComponent, IPostProcessComponent { [Tooltip("Controls the intensity of the effect.")] public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f); public FloatParameter sampleCountFactor = new FloatParameter(2f); Material m_Material; public bool IsActive() => m_Material != null && intensity.value > 0f; // Do not forget to add this post process in the Custom Post Process Orders list (Project Settings > HDRP Default Settings). public override CustomPostProcessInjectionPoint injectionPoint => CustomPostProcessInjectionPoint.AfterPostProcess; const string kShaderName = "Hidden/Shader/SNNFilter"; public override void Setup() { if (Shader.Find(kShaderName) != null) m_Material = new Material(Shader.Find(kShaderName)); else Debug.LogError($"Unable to find shader '{kShaderName}'. Post Process Volume SNNFilter is unable to load."); } public override void Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination) { if (m_Material == null) return; m_Material.SetFloat("_Intensity", intensity.value); m_Material.SetFloat("_SampleCountFactor", sampleCountFactor.value); m_Material.SetTexture("_InputTexture", source); HDUtils.DrawFullScreen(cmd, m_Material, destination); } public override void Cleanup() { CoreUtils.Destroy(m_Material); } }
SNNFilterシェーダを作成する
Create > Shader > HDRP > PostProcess でポストプロセス用のシェーダを作成できる。ファイル名はSNNFilterとする。この時シェーダ名は先程作成したC#クラス中の kShaderName
と完全に一致させる必要がある。
シェーダの中身は以下の通り。
Shader "Hidden/Shader/SNNFilter" { HLSLINCLUDE #pragma target 4.5 #pragma only_renderers d3d11 playstation xboxone vulkan metal switch #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/PostProcessing/Shaders/FXAA.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/PostProcessing/Shaders/RTUpscale.hlsl" struct Attributes { uint vertexID : SV_VertexID; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float2 texcoord : TEXCOORD0; UNITY_VERTEX_OUTPUT_STEREO }; Varyings Vert(Attributes input) { Varyings output; UNITY_SETUP_INSTANCE_ID(input); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID); output.texcoord = GetFullScreenTriangleTexCoord(input.vertexID); return output; } // List of properties to control your post process effect float _Intensity; float _SampleCountFactor; TEXTURE2D_X(_InputTexture); float4 GetOffsetPixel(float2 offset, float2 coord) { float2 invScreenSize = 1.0 / _ScreenSize; float4 result = LOAD_TEXTURE2D_X(_InputTexture, (coord + invScreenSize * offset) * _ScreenSize.xy); return result; } float4 GetSNN(float4 centerColor, Varyings input, float2 offset) { float4 col0 = GetOffsetPixel(offset, input.texcoord); float4 col1 = GetOffsetPixel(-offset, input.texcoord); float3 d0 = col0.rgb - centerColor.rgb; float3 d1 = col1.rgb - centerColor.rgb; return dot(d0, d0) < dot(d1, d1) ? col0 : col1; } float4 CustomPostProcess(Varyings input) : SV_Target { UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); float4 centerColor = LOAD_TEXTURE2D_X(_InputTexture, input.texcoord * _ScreenSize.xy); int count = 0; float4 resultColor = (0, 0, 0, 1); for (int x = -_SampleCountFactor; x <= _SampleCountFactor; ++x) { for (int y = -_SampleCountFactor; y <= _SampleCountFactor; ++y) { if (x == 0 && y <= 0) { continue; } resultColor += GetSNN(centerColor, input, float2(x, y)); count++; } } resultColor /= count; return resultColor; } ENDHLSL SubShader { Pass { Name "SNNFIlter" ZWrite Off ZTest Always Blend Off Cull Off HLSLPROGRAM #pragma fragment CustomPostProcess #pragma vertex Vert ENDHLSL } } Fallback Off }
作成したポストプロセスをHDRPに登録する
Edit > ProjectSettings > HDRP Default Settings からHDRPの詳細な設定を決められる。今回は、 After Post Process に先程作成したSNNFilterを登録する。
ポストプロセスをシーンに追加する
Hierarchyから、 Volume > Global Volume と選択して、シーンにGlobal Volumeを追加する。Global Volumeはポストプロセスを設定すると、シーン全体にそのポストプロセスを適用することができる。
Global Volume中のVolumeコンポーネントのProfileのNewを押して、Profileを新規作成する。
続けて Add Override を選択する。ここまで適切に設定していれば、ここで「SNNFilter」を追加できる。Intensityを0より大きい値に設定するとポストプロセスが適用される。
SNNFilterの例
Sample Count Factor = 2の時
Sample Count Factor = 10の時
Sample Count Factor = 20の時
参考資料
Custom Post Process | High Definition RP | 10.2.2
alelievr/HDRP-Custom-Passes: A bunch of custom passes made for HDRP
Re: 大学生がゲーム会社にプログラマーとして内定が出るまでやってたことを晒す(新卒一年目を終えて)
前置き
2年前の3月、ブログで大学生がゲーム会社にプログラマーとして内定が出るまでやってたことを晒すという記事を書いたのですが、これが存外好評で、色々な人に見ていただいた。
2020年4月から、この時内定を頂いたコンシューマゲーム系の開発会社に無事就職し、今もゲームプログラマとして働いている。COVID-19の影響で、入社以来ずっとリモートで就業を続けているという特殊な環境にはいるが、当時ブログを書いた学生の頃には見えていなかったものが見えるようになった。そこで、改めて当時(2020卒)の自分の就活事情について振り返りつつ、ゲーム業界への就職を考え出した大学生の自分が知りたかった情報を整理してみた。
就活時の筆者のスペック
- 学歴
- 地方国立大学のシステム系工学部生(学部卒)
- 開発経験
- 大学入学時にプログラミングを始めた
- 就活時に使えた言語・ゲームエンジンは以下の通り
- C / C++ / Python / Unreal Engine ... それなりには使った
- C# / Java / Unity ... 触り程度で使ったことがある
- 学生時代にUnreal Engineを使って小規模のゲームを2本作った
当時の就活戦略
インターン
今どきの就活、特にIT系はどうしてもインターンありきなところがある。私も例に漏れず、5社のインターンに応募して、内2社のインターンに参加した。インターンに参加した2社の内1社は、本選考まで進み、内定を頂いている(諸事情で辞退したが)。
インターンに参加することのメリットは、「インターン参加者限定の早期選考ルートに案内されることがある」「現場の人間のナマの声が聞ける」「同年代の凄く強い奴らに出会える」というところだと思う。インターンには絶対参加すべきとは言わないが、もっと頑張ろうというモチベに繋がるので、都合が合うのなら是非参加してみるべきだと思う。
逆求人イベント
サポーターズや、ジースタイラスの逆求人系のイベントにそれぞれ1回参加している。これは、当時の就活を振り返って一番といってもいい経験だった。
逆求人イベントとは、事前選考を通過した学生がイベントに参加する企業と面談ができる、というイベントだ。選考があるため、参加する学生の数は必然的に限られてくる。そのため、通常の合同会社説明会と比べて企業の採用担当者と1対1で話す時間が長く取れる。加えて、大体のイベントで交通費を支給される。これはお金のない学生の身分には非常に助かった。
今は世の中がこの調子なので、リアルスペースでのイベント開催ができていない(だろう)が、調べたところ、変わらずオンラインで頻繁に開催しているらしい。事前選考も、ある程度のアウトプットがあればだいたい通ると思うので、就職を考えている学生は挑戦してみるといいと思う。
面接対策
面接対策といっても大したことはやっていない。上述の逆求人イベントを通じて、企業の採用担当者との会話にはある程度慣れていたので。こればかりは、本人の資質と場数がものを言う。
あ、ただし面接の最後に聞かれる「なにかそちらから質問はございますか?」の回答は事前に準備しておいたほうがいい。
ブログ・Twitter
正直に白状すると、このブログは就活対策として始めた。結果としては大成功だったと思う。聞いた限りでは、ブログで積極的にアウトプットしている姿勢は高評価を頂いた。アウトプットの一つの形として、ブログはおすすめしたい。
Twitterは就活に関係なく高校時代から使っていたが、結果として就活に一番役にたったツールだと思う。今働いている会社もTwitter経由で会社説明会の存在を知り、そのまま説明会参加→書類提出→面接→内定までつながった。
2年前の記事で、自分はTwitterについて以下のように書いていた。
Twitterには、沢山のつよいひとがいる。開発過程・進捗を、ハッシュタグをつけて(UE4だったら #UE4Study)ツイートすると、RT・ふぁぼしてもらえたり、アドバイスと言う名のマサカリが飛んでくる。それをモチベーションにしてまた新たな進捗を生み、また拡散・アドバイスしてもらえる。これを「正のSNSループ」と言う。今名付けた。
一人で孤独に開発し続ける、ってのも大切ではあるが、それを続けてると、大体挫折する。
正のSNSループ」に乗ることがモチベーションを枯らさず開発を続けられるし、時折アドバイスが貰えたりする。
また、Twitterで著名な開発者をフォローすると、TLに開発にまつわる色んな最新情報が得られたりする。私は最近、ニュースサイトをいちいち巡回するよりも、Twitter見てたほうが良いんじゃないかと思っている。
大筋は今もこの考えに変わりはない。だがTwitterというところは、悪い面も沢山ある。自分が向いてないと思ったら無理せず辞めればいい。それも踏まえてTwitterを続けるという人は、ハードルの低い細かいアウトプットの場所として使うといいと思う。
(ちなみに自分のアカウントはこちら↓)
一般の採用情報サイト
マ○ナビとかリ○ナビとかのこと。私はこれらのサイトはあまり利用しなかった。この地域にはこれだけのゲーム会社があることを確認するのには使えるが、会社それぞれの個性やこれまで作ったタイトル等は、やはり個々の企業のサイトを見に行かないとわからない。なので、○イナビ等はざっくりいろんな企業の名前を知りたい時だけ使って、後は知っているゲームのクレジットだったり、SNSで知った企業に、企業HPから直接応募していた。
なお、自分は遭遇しなかったが、稀にマ○ナビ経由の応募しか受け付けていない企業もあるらしい。
結局就職のために何をしたらいいのか
そんなの人次第……ってのが本音だが、就職できる確率を上げる方法なら多少上げられる。なので、あくまで以下は「あくまで私個人の意見」という前提で読んでほしい。
技術トレンドを追うアンテナを張る
面接でよく「最近気になっている最新技術・技術ニュースはありますか?」と尋ねられる。そういった質問に答えられるよう、Twitterなりブログなり、はてなブックマークなり、なんでもいいので、技術トレンドを追うアンテナは張っておくべき。ただしあくまで面接対策で……と調べるのはよくない。仕事では、わからないことを調べることが日常茶飯事。いざそうなったときに、苦しまずに済むような自分なりの「調べる技術」「知識のインデックス」を身に着けておくべきだと思っている。
面接で普通に話せる程度の経験を積む
就職となると、面接だけはどうあっても避けて通れない。バイトするなり、大学等で開かれる面接練習に参加するなり、先述の逆求人イベントに参加するなどして、本番の面接で大失敗しないよう経験を積んでおきたい。
ゲームのプレイ・プログラミングを楽しむ
2年前に記事を書いた時と同じく、これが個人的に一番大切だと思うこと。
ゲームのプレイや、プログラミングが楽しめないと、ゲーム会社に就職しても幸せになれないと思う。はっきりいって、ゲームプログラミングを学ぶのにかかる労力を、Web開発やアプリ開発、ネットワークインフラの学習に費やした方が、普通にいけば生涯賃金は高くなる。それがわかっていてなおゲーム業界に進みたいというのなら、相応の理由が必要になる。その理由というのはやはり「ゲームが好き」「ゲーム開発が楽しい」といったものになると思う。
捕捉
2年前の記事では、ゲーム業界に進むためにやるべきことの一つに「学生のうちにゲームを一本作る」というのを上げていた。これは重要ではあるし、作ったゲームを就活時のポートフォリオに載せられれば、書類選考を通過する確率を上げられると思う。
ただ、当時ほど「ゲームを一本作る」ことに重要さは感じていない。というのも、何人か学生時代にゲームを作ったことがないのに、今ゲーム会社で働いているという方を知ったからだ。
ではなぜ採用されたのか、と考えると、大学での研究が評価されたからだと思う。
大学生、特に情報・システム工学系の学部で研究できることは、最新のゲーム作りに繋げられるものもある。また、ゲーム開発に直接活かせる活かせる研究をしていなかったとしても、研究で一定以上の成果を上げていれば、この人は数学・物理・英語etc...のスキルを持っているとみなしてもらいやすい。
ゲーム開発の専門学校からの就職を狙うのであれば、やはりゲームを1本以上作ることは必須になるだろう。しかし、もしあなたが大学からゲーム業界への就職を狙うのであれば、別にゲームを一本作り上げることに固執する必要はなく、ひたすらに大学での研究活動に打ち込み、それを成果として就職活動に持ち込むのは大いにアリな戦略だと思う。
高校・大学時代には知らなかった疑問とその答え
Q. 学歴って本当に必要なの?
- 部分的にYES。やはり一部の企業や、R&D方面に進みたいなら一定の学歴や研究成果が必要になる。ただ、別に東大京大に行けなかったからあの会社には絶対に行けないなんてことはない。
Q. ゲーム業界の給与水準は他のIT系業界と比べてどうなの?
- やっぱり低いと思う。他の業界に進んだ方の情報を聞くとやっぱり他IT系業界と比べると低いと感じることが多いし、昇給ペースも決して早いとは言えない。まぁここ数年の働き方改革で、よほど駄目な企業以外は、食うのには困らない程度のお金はもらえるようになったとは思う(昔のことは聞きかじった程度の話しか知らないので話半分で聞いてください……)。
Q. ブラックじゃないの?
- ここ何年かで相当マシになったらしい。とは言えまだ残業続きが解消されない……って話はたまに聞く。これはもう会社次第としか言えない……
Q. ゲーム開発ってどうやって始めればいいんですか?
- ひとまずUE4なりUnityなりの汎用ゲームエンジンを触ってみればいいと思う。C++/C#だけで開発することも出来なくはないが、情報の多さやすぐに絵を出せる楽しさを考えると、私ならゲームエンジンから触ってみることをおすすめする。
Q. ゲームエンジンだけじゃなくてDirectX/OpenGL使えないと就職できないってホント?
- うーん。もしグラフィックエンジニアとして採用されたいなら、やっぱDirectX/OpenGL等のグラフィックAPIは触れた方がいいとは思う。ただ、私は就活当時、グラフィックAPIを触ったことがなかった。結局グラフィックAPIを触れなくても、Unityで相当に目を引くゲームを作った、大学の研究で大きな成果を上げた、面接での弁が立つ等、他の人と差別化できる点を持っているなら、それでいいと思う。
Q. ゲーム専門学校ってどうなの?
- ネットで言われているほど悪いところではないと思うよ。専門学校で、十把一絡げの大学生が束になっても敵わないような成果を上げている学生はいるし、フツーに講義を受けていれば、ゲーム制作の一連の流れは学べると思う。
ただ、専門学校は大学と違い、お金を払えば誰でも入れてしまうため、できる人とできない人での格差が非常ーーーに大きいらしい。なぜここに来たの?と言いたくなるような何もやらない・出来ない学生に引っ張られて、自分も駄目になってしまうような人は向いてないと思う。それに専門学校に通っても、ゲーム業界に必ず就職できるわけではない。周りに流されないという自信が持てなら、将来潰しが効く大学に進んだ方がいいのだろう。
Q. あの有名プロデューサやプログラマと一緒に働きたいから○○社に志望します!
マジでやめとけ。
プロデューサクラスでも、会社員である以上は、会社を辞めるときはあっさり辞める。なので、憧れのあの人と一緒に働きたい!で会社を選ぶと、入社したときにその人は辞めていたなんてことはザラにある。人事情報なんか、学生がどう調べたってわかるわけないのだから、憧れのあの人と!という理由で会社を選ぶのは全くおすすめしない。
ただ、その会社の「XXX」というタイトルのゲームが作りたい!というのはやっていいと思う。場合によりますが、そういった所属するプロジェクトの志望は通りやすいので。
まとめ
重ねてにはなるが、これはあくまでn=1の個人の意見。この記事が多少なり参考になれば嬉しいが、基本ここに書いたことは無視してもらっていい。結局実際に手を動かすのはあなたなので。あなたのやりたいように、精一杯頑張ってください。
WindowsでMacのようにキーボードショートカット一発で呼び出せる辞書アプリ「RapidDict」を買った
学生時代、研究室で支給されたmacを使っていた時期がある。その時よく使っていたmacの機能で、Windowsにもほしいなあと思ったのが「spotlightで起動できる辞書検索」機能だ。
spotlightとはmacに標準に搭載されているランチャーアプリのことで、これを使ってショートカット一つで起動し、アプリの起動やファイル検索などを容易に呼び出すことができる。このspotlightには辞書.appが組み込まれており、調べたい単語を入れると、その辞書ページに一発で飛ぶことができる。この機能のお陰で、大学時代海外の論文を読むときに簡単に英単語を調べられて、助かったのを覚えている。
Windowsにも「Wox」などmacのSpotlightに相当するサードパーティ製アプリは存在する。私の場合、Microsoftが開発しているユーティリティアプリ「PowerToys」にバンドルされている「PowerToys Run」を使っている。
ただ、これらのランチャーアプリには辞書が組み込まれていない。また、ショートカット一つで起動できるような出来の良いWindows用辞書アプリもないため、macを研究室に返還して自宅のWindowsで英単語を調べるときは、仕方なくブラウザを立ち上げてWeblio辞書を使っていた。
で、最近になってショートカット一つで起動できるWindows向け辞書アプリを見つけた。それが、RapidDictだ。
RapidDictはアプリ自体は無料だが、辞書データ(英辞郎)を入手するのに980円支払う必要がある。一応無料の辞書データもあるようだが、かなり貧弱なため、基本RapidDictを使うなら980円を払うべき。
RapidDictを使ってみて、正直アプリ自体の出来はあまり良くないと感じた。購入した辞書データをインストールする際何度かアプリがダウンすることもあったし、検索速度も決して早いとはいえない。
ただショートカット一つで起動できる英語辞書というだけで、その出来の悪さに目をつぶって使い続けたくなる魅力がある。Windowsで英単語を検索するためだけにわざわざブラウザを起動するのに嫌気が差している人は、是非RapidDictを試してみてほしい。
……PowerToys Runに辞書機能入らないかなあ……
(遅すぎる)今年の目標 2021年版
遅すぎますが今年の目標を簡単に書いておこうと思う。
ブログ毎月更新継続
こんな急ぎ書いたようなのが丸見えな記事でも、3年以上続いているブログの毎月更新は、今年も継続したい。その中で、1、2記事は、10数時間かけた大作の記事が書きたい
今年こそ一人Advent Calender完走
すっかりなかったことになっている去年の一人アドベントカレンダー。あれは直前に思いつきでカレンダーを作って、即興で記事を書いていたところがあるので仕方ないところがある。
今年は、まだまだ時間があるので、記事のストックをためて、12月に一人アドベントカレンダーが完走できるようにしたい。(そしてその最終日に、この記事を踏まえた今年の振り返り記事を書きたい)
就活記事のリメイク
昔書いた就活記事のリメイクを書く。これは今絶賛書いているところで、来月中に公開する目処が経っているので、乞うご期待ということでお待ち下さい。
月2冊は技術書を読む
去年は本を4冊買ったら1冊買うみたいなスタイルで、完読したという本があまり多くなかった。最近本棚を買い替えたこともあり読書のモチベが高いので、まずは積んでいる本の消化から優先して、最低月2冊ペースで本を読みたい。
二ヶ月に1個はGithubに何かしらの小さいプロジェクトを公開する
最近OpenSiv3Dを使って2048ゲームのクローンを作成した(これについては後でまた詳しくブログに詳細を書くと思います)。この規模でいいので、小さいプロジェクトを最低でも二ヶ月に一個は開発して、それをGithub及びブログで公開していきたい。
新しい言語を一つ覚える
今の所考えているのはjavascript + HTML5。web系の知識が空っぽなので、Github pagesにポートフォリオとなるサイトが作れる程度にはなにか作りたいなーと。
まとめ
以上。今年もフルリモート勤務が続くと思われるので、この目標が達成できるだけの時間は取れると思う。今年は無理に大きいことをやろうとせず、将来の肥やしになるような、小さい事をコツコツと、をテーマにエンジニアライフを送りたいと思います。
windowsのリモートデスクトップで複数画面を使う&画面上部の接続バーを消す方法
本記事はai_9684_dctソロ Advent Calendar 2020 10日目の記事です。
「Windowsリモートデスクトップ接続」に関する機能の紹介。
コロナ禍の中、4月の入社以来、数回の出社を除いてリモートワークが続いている。弊社の場合、自宅にある会社から貸与されたPCから会社のネットワークにVPN接続し、windows標準の「リモートデスクトップ接続」か、「Chromeリモートデスクトップ」で会社のPCにつないでリモートで作業するというスタイルを取っている。
私は学生時代もたまに使っていて、使い慣れていたということもあり、半年以上「Chromeリモートデスクトップ」を使用していた。しかしながら「Chromeリモートデスクトップ」は1画面でしか作業できない事が非常に不満だった。
そんなことをMTG中にぼやいたら、先輩から「windowsのリモートデスクトップなら複数画面使えるよ!」というアドバイスを貰った。マジか、と調べてみたら確かにあった。
リモートデスクトップ接続の接続前の設定画面で、「リモートセッションで全てのモニターを使用する」というオプションを有効にすると、例えばリモート接続元のPCが2画面使っていれば、2画面まるごと使ってリモートデスクトップの画面を使う事が出来た。なぜこの機能にもっと早く気づかなかったんだ……
更に、windows標準のリモートデスクトップ接続の大きな不満の一つだった、画面上部に常に表示される↓この接続バーも消せることが分かった。
同じ設定画面で、「全画面表示の使用時に接続バーを表示する」という設定のチェックを外せば、接続バーが最初以外一切表示されなくなる。なお、接続バーを使わなくても、「[Ctrl] + [Alt] + [Pause]」キーを同時押しすれば、全画面表示が解除される。
というわけで、リモートワークの不満の一つだった「画面の狭さ」という課題がクリアされ、リモート作業中にかかるストレスを大きく減らすことができましたとさ。まあ、夕方6時頃になると回線が重くなって、入力の遅延が我慢できないほど目立つようになるのは変わらないのだが。10Gビット回線引こうかなあ……
WindowsのC++でウィンドウアプリケーションを書く際にWinMain()を隠匿する方法の一例
序文
OpenSiv3Dなど、ユーザーが使いやすいように考えて作られたC++製Windowsアプリケーション開発用フレームワークは、複雑なシグネチャを持つ WinMain()
を隠してくれる。
# include <Siv3D.hpp> // WinMain()を書く必要がない // int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, // LPSTR lpCmdLine, int nCmdShow) void Main() { Print << U"Hello, Siv3D!"; while (System::Update()) { } }
私もこうしたWinMainの隠匿をしてみたいなーと思い、実際に実装してみた。
最も簡単な実装
FrameworkMain.cpp
#include <Windows.h> void Main(); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { OutputDebugStringA("winmain() start!\n"); Main(); return 0; }
UserMain.cpp
#include <Windows.h> void Main() { MessageBoxA(nullptr, "Demo", "Demo Message", MB_OK); }
上記のコードのように、フレームワーク側(.lib)で、宣言だけ用意して未実装な void Main()
関数を用意し、それをフレームワーク側に記述した WinMain()
で呼び出す。これにより、ユーザーは WinMain()
の存在に気を払うことなく、Main()
関数さえ実装することができる。
以上で目的は達成できたのだが、これではあまりに薄いので、もう少し凝った実装をしてみた。
クラスのメンバ変数をエントリポイントのように扱う
ここでは以下の要件を満たすものを実装する。
- フレームワークはAppBaseという名のクラスを用意する
- AppBaseは
virtual int Init()
という純粋仮想関数をもつ。 - ユーザーはAppBaseをpublic継承したアプリケーション用クラスを作成できる
- ユーザーは継承先で
virtual int Init()
関数をエントリポイントのように扱って実装できる - ユーザーは、これらの機能を最小限の労力で使用できる。
AppBase.h(フレームワーク実装)
#pragma once #include <windows.h> class AppBase* GetWinPtr(); class AppBase { private: friend AppBase* GetWinPtr(); static AppBase* m_App; public: AppBase(); virtual int Init() = 0; };
AppBase.cpp(フレームワーク実装)
#include "AppBase.h" AppBase::AppBase() { m_App = this; } AppBase* AppBase::m_App = nullptr; AppBase* GetWinPtr() { return AppBase::m_App; } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { OutputDebugStringA("winmain() start!\n"); AppBase* g_App = nullptr; g_App = GetWinPtr(); if (g_App == nullptr) { return -1; } return g_App->Init(); }
MyApp.cpp(ユーザー実装)
#include "AppBase.h" class MyApp : public AppBase { public: virtual int Init() { MessageBoxA(nullptr, "Demo", "Demo Message", MB_OK); return 0; } }; MyApp app;
解説
ユーザーは、グローバルに自身がAppBaseクラスを継承して作成したクラスのインスタンスを作成するだけで、 Init()
関数をエントリポイントのように扱って実装できる。仕組みとしては以下の通り。
AppBaseはstatic変数として、自身の型のポインタ変数を持つ。このポインタは、AppBaseのfriend関数である GetWinPtr()
から取得できる。
フレームワーク上では、 WinMain()
において GetWinPtr()
経由でAppBase型のポインタから Init()
メンバ関数を呼び出す。しかし、この時点ではそのポインタの実体がどの型を表しているか分からない。
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { OutputDebugStringA("winmain() start!\n"); AppBase* g_App = nullptr; g_App = GetWinPtr(); if (g_App == nullptr) { return -1; } return g_App->Init(); }
そこでユーザーは、AppBase型を継承したクラスであるMyApp型のインスタンスを、グローバルで作成する。グローバルで定義した変数は、エントリポイント、つまり WinMain()
より早く用意される。
これにより、ユーザーは、AppBase継承クラスの Init()
関数を、エントリポイントのように扱って実装することができる。
補足
この実装は、WindowsのGUIフレームワークとしてMicrosoftが提供する、MFC(Microsoft Foundation Class)の実装とほとんど同じらしい。
参考
In a typical MFC application we never write WinMain() function so how is it possible to compile and link a windows program with out WinMain()?(http://www.equestionanswers.com/vcpp/mfc-application-winmain().php)