ASP.NET Core を使ってREST APIを作ってみる

ASP.NET Core を使ってREST APIを作ってみる

執筆者:みやじ

これは OUCC Advent Calendar 2022 の17日目の記事です。
ここではTodoアプリのバックエンドを想定して簡単な REST APIを作ってみようと思います

目次

プロジェクトを作る

Visual Studioで ASP.NET Core Web API のプロジェクトテンプレートを選択して作成します。
実行するとhttps://localhost:<port>/swagger/index.htmlが自動的に開き最初からある WeatherForecast API が表示されます。ここで<port>ランダムに選択されたポート番号です。よく見るSwaggerページだと思います。Try it out で試しに実行することもできます。

サンプルデータ用のサービスを作成する

まず次のようなTodoItemクラスを作成します。

public class TodoItem
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public string? Description { get; set; }
    public DateTime DueDate { get; set; }
}

つぎにそれを扱うITodoItemContainerを作ります。実装は少し長くなったので下の方に書いておきます。単純にList<TodoItem>を用意してそれに足したり抜いたりしているだけです。

public interface ITodoItemContainer
{
    IEnumerable<TodoItem> GetTodoItems();
    TodoItem? AddTodoItem(TodoItem item);
    bool RemoveTodoItem(int id);
    bool UpdateTodoItem(TodoItem item);
}

ASP.NET Core は依存性注入(DI)を採用しているのでITodoItemContainerを API Controller で使うためにはDIコンテナに登録する必要があります。 Program.cs でbuilder.Build();が呼び出されている箇所より上でbuilder.Services.AddSingletonを呼び出せば登録することができます。
今回は次のコードをを追加しました。

builder.Services.AddSingleton<ITodoItemContainer, TodoItemContainer>();

API コントローラーの作成

API コントローラーはControllerBaseを継承して作ります。これを継承することで応答に便利な関数を利用できます。

[ApiController]
public class TodoItemsController : ControllerBase

ApiCotroller属性をつけると以下のようなAPIの動作を有効化できます。

  • 属性ルーティング要件
  • 自動的な HTTP 400 応答
  • バインディング ソース パラメーター推論
  • マルチパート/フォーム データ要求の推論
  • エラー状態コードに関する問題の詳細

以下のように API コントローラーにメソッドを定義することでアクションを設定できます。ApiController属性を付けているとHttpGet属性なしでもメソッド名にGET POST PUT DELETEが含まれていると自動的にHTTPメソッドとして登録されますが、Swaggerが認識できないのでHttpGet属性をつけるほうが良いです。

[HttpGet]
public ActionResult<TodoItem> GetTodo() { }

ルーティング

ルーティングはRoute属性を用いてコントローラーまたはアクションに設定できます。ASP.NET Core のルーティングは大文字小文字の区別がありません。api/todoでもApi/Todoでも同じものとして扱われます。
コントローラにRoute属性を使うとそのコントローラーでのデフォルトのルートが設定されます。[controller]はコントローラーの名前から自動的に Controller の部分を取り除いた文字列が入れられます。
アクション、つまりメソッドにRoute属性を使うとそのメソッドだけにルーティングを設定できます。このとき、/または~/で始めるとコントローラーのルートテンプレートと結合されません。

また HttpGet などの HTTP 動詞テンプレートでもルーティングは設定できます。 HTTP 動詞テンプレートを使うとその HTTP 動詞だけを受け付けるように制限されるので REST API を作るなら全て HTTP 動詞テンプレートを使うこと方が良いです。 HTTP 動詞テンプレートは以下の6つです。

  • [HttpGet]
  • [HttpPost]
  • [HttpPut]
  • [HttpDelete]
  • [HttpHead]
  • [HttpPatch]
[Route("api/[controller]")]
[ApiController]
public class TodoItemController : ControllerBase
{
    [HttpGet]          // api/TodoItem
    public ActionResult<TodoItem> GetTodo() { } 

    [HttpGet]
    [Route("todo")]    // api/TodoItem/todo
    public ActionResult<TodoItem> GetTodo() { }

    [HttpGet("todo")]  // api/TodoItem/todo
    public ActionResult<TodoItem> GetTodo() { }

    [HttpGet("/todo")] // todo
    public ActionResult<TodoItem> GetTodo() { }
}

メソッドの引数

メソッドの引数はURLパスから受け取ったりリクエスト本文から受け取ったりできます。

URLパスから情報を受け取る

Route属性やHttpGet属性などで{id}のように書くことで、引数idにその値を受け取ることができます。また、{id?}とすることで省略することができます。省略された場合引数にはデフォルトの値が入ります。

[Route("api/[controller]")]
[ApiController]
public class TodoItemController : ControllerBase
{
    // api/TodoItem/1 => id = 1
    // api/TodoItem   => id = 0
    [HttpGet("{id?}")]
    public ActionResult<TodoItem> GetTodo(int id) { }
}

URLクエリパラメータやリクエスト本文から受け取る

それぞれ以下の属性を引数につけることでリクエストから受け取ることができます

  • FromQuery リクエストのクエリパラメーター
  • FromBody リクエスト本文
  • FromForm リクエスト本文内のフォームデータ
  • FromHeader リクエストヘッダー

FromBodyはSystem.Text.Jsonを使って自動的にパースしてくれます。

[Route("api/[controller]")]
[ApiController]
public class TodoItemController : ControllerBase
{
    [HttpPost]
    public ActionResult<TodoItem> CreateTodo([FromQuery] int id, [FromQuery] string title) { }

    [HttpPost]
    public ActionResult<TodoItem> CreateTodo([FromBody] TodoItem todo) { }
}

メソッドの返り値

メソッドの返り値にはActionResult<T>を指定しておけば暗黙的なT=>ActionResult<T>のキャストが実装されているのでとりあえずは困らにと思います。型が指定できないとき、例えば匿名型を使用するときなどはActionResultを使えばよいです。
そのまま値を返しても問題ありませんが、ControllerBaseに実装されているメソッドを利用することで一緒にStatusCodeを指定することもできます。よく使われそうなものをいかに列挙しておきます。

  • Ok(Object)
    HTTP 200 OK を返すオブジェクトを生成する。
    引数にはアクションの返り値にしたいオブジェクトを指定する。
  • Created(String, Object)
    HTTP 201 Created を返すオブジェクトを生成する。
    第1引数にはコンテンツが作成された URI を、第2引数にはアクションの返り値にしたいオブジェクトを指定する。
  • CreatedAtRoute(String, Object, Object)
    HTTP 201 Created を返すオブジェクトを生成する。
    第1引数には作成されたコンテンツを返すアクションの名前を、第2引数にはそのアクションのURLの生成に使用するルートデータ。第3引数にはアクションの返り値にしたいオブジェクトを指定する。
  • NotFound()
    HTTP 404 Not Found を返すオブジェクトを生成する。

もっと見る場合は ControllerBase クラスのドキュメントを参照してください。

[Route("api/[controller]")]
[ApiController]
public class TodoItemController : ControllerBase
{
    private ITodoItemContainer _todoItemContainer;

    public TodoItemController(ITodoItemContainer todoItemContainer) {
        _todoItemContainer = todoItemContainer;
    }

    [HttpGet("{id}")]
    public ActionResult<TodoItem> GetTodo(int id) {
        var todo = _todoItemContainer.GetTodoItems().FirstOrDefault(t => t.Id == id);
        return todo is null ? NotFound() : Ok(todo);
    }

    [HttpPost]
    public ActionResult<TodoItem> AddTodo([FromBody] TodoItem item) {
        var todo = _todoItemContainer.AddTodoItem(item);
        return todo is null ? BadRequest() : CreatedAtAction(nameof(GetTodo), new { Id = todo.Id }, todo);
    }
}

完成

他にPUT DELETEメソッドを追加して最終的に次のようになりました。

TodoItemControllerの実装

[Route("api/[controller]")]
[ApiController]
public class TodoItemController : ControllerBase
{
    private ITodoItemContainer _todoItemContainer;

    public TodoItemController(ITodoItemContainer todoItemContainer) {
        _todoItemContainer = todoItemContainer;
    }

    [HttpGet]
    public ActionResult<TodoItem[]> GetTodoList() {
        return Ok(_todoItemContainer.GetTodoItems().ToArray());
    }

    [HttpGet("{id}")]
    public ActionResult<TodoItem> GetTodo(int id) {
        var todo = _todoItemContainer.GetTodoItems().FirstOrDefault(t => t.Id == id);
        return todo is null ? NotFound() : Ok(todo);
    }

    [HttpPost]
    public ActionResult<TodoItem> AddTodo([FromBody] TodoItem item) {
        var todo = _todoItemContainer.AddTodoItem(item);
        return todo is null ? BadRequest() : CreatedAtAction(nameof(GetTodo), new { Id = todo.Id }, todo);
    }

    [HttpPut]
    public ActionResult<TodoItem> UpdateTodo([FromBody] TodoItem item) {
        var result = _todoItemContainer.UpdateTodoItem(item);
        return result ? Ok(item) : BadRequest();
    }

    [HttpDelete("{id}")]
    public ActionResult DeleteTodo(int id) {
        var result = _todoItemContainer.RemoveTodoItem(id);
        return result ? NoContent() : BadRequest();
    }
}

ITodoItemContainerの実装

public class TodoItemContainer : ITodoItemContainer
{
    private List<TodoItem> _items;

    public TodoItemContainer() {
        _items = Enumerable.Range(1, 10)
            .Select(i => new TodoItem {
                Id = i,
                Title = $"todo item id:{i}",
                DueDate = DateTime.Today.AddDays(1)
            }).ToList();
    }

    public IEnumerable<TodoItem> GetTodoItems() => _items;

    public TodoItem? AddTodoItem(TodoItem item) {
        if (item.Id >= 1 && _items.Any(t => t.Id == item.Id))
            return null;

        if (item.Id < 1)
            item.Id = _items.Max(t => t.Id) + 1;
        _items.Add(item);
        return item;
    }

    public bool RemoveTodoItem(int id) {
        var todo = _items.FirstOrDefault(t => t.Id == id);
        return todo is not null && _items.Remove(todo);
    }

    public bool UpdateTodoItem(TodoItem item) {
        var todo = _items.FirstOrDefault(t => t.Id == item.Id);
        if (todo is null)
            return false;

        _items[_items.IndexOf(todo)] = item;
        return true;
    }
}

あとがき

HTTP Status Code がどれを使えばいいのか全然わからなかったです。加えてうまくSwaggerに反映されてくれなくてよくわからなかったです...

参考

初学者のためのC#

C#とは

 C#はC++からJavaになる際に追加されたガーベージコレクション(GC)や中間言語を介して共通の実行環境で実行することなどを参考にMicrosoftが開発した言語です。C#はIL(中間言語)にコンパイル後.Net Framework上で実行されます。この.Net Framewokというのは標準でWindowsにインストールされており、そのためC#とその統合開発環境のVisual Studioを使うことで簡単にWindowsアプリケーションが開発・公開ができます。また、OUCCで主に使用しているUnityというゲーム作成に使われるフレームワークで使用する言語にも採用されています。

基本的な書き方

using System; // ファイル内でほかの名前空間のものを完全修飾型名を使わずに使用できるようにする

namespace Hoge // 名前空間はこうやって宣言する。
{
    public class Program // 処理は必ずクラスの中に関数を置いて書く
    { // 波カッコで範囲を示す

        public int HogeHoge { get { return _hogeHoge; } set { _hogeHoge = value; } } // プロパティの宣言方法
        private int _hogeHoge; // フィールドの宣言方法

        public static void Main()
        {
            Console.WriteLine("Hello World"); // 1文の最後は ; (セミコロン)で終わる
            var odds = Enumerable.Range(0, 100)
                .Where(i => i % 3 == 0)
                .ToArray(); // こんな感じで複数行にわたって書くことも可能。
            /*
            複数行コメントはこうやる
            */
        }
    }
}

StructとClass

 大きく分けてStructとClassの2種類があり、したのようにいろいろ違いはあります。StructはClassよりも動作が早いのですが参照型でないためにおこる様々な問題があるためオブジェクトを自分で定義する際は何か重要な問題がない限りClassで作ることをお勧めします。

structとclassの違い

struct class
オブジェクトの型 値型 参照型
データが保存されるメモリ スタック ヒープ
継承 interfaceの実装のみ class,interfaceともに可能
コンストラクタの定義 引数ありのものだけ可能 なんでも可能
nullableか nullを入れられない nullを入れられる

structの問題点

  • スタックに保存されるのでメモリを多く占有する大きなオブジェクトを入れると逆に遅くなる。
  • 参照型でないので関数やプロパティでとってきたものを変更してももとのオブジェクトを変更できない。(classでは可能)
  • 継承ができない
  • 引数なしのコンストラクタを作れない
  • nullを入れられない

他の問題を見たい方はC#に潜むstructの罠などを参考にしてみてください。

よく使う型

この中ではstringのみがclassでそのほかはstructです。

エイリアス 実装 リテラル 説明
byte System.Byte 0(収まる範囲内のみ) 符号なし整数(8bit) 主にバイナリデータのために使われます
short System.Int16 0(収まる範囲内のみ) 整数(16bit)
int System.Int32 0(収まる範囲内のみ) 整数(32bit)
long System.Int64 0(収まる範囲内のみ) 整数(64bit)
float System.Single 0f 小数型(32bit)
double System.Double 0.0又は0d 小数型(64bit)
decimal System.Decimal 0m 小数型(128bit)
bool System.Boolean true, false 真偽値(2bit)
char System.Char 'j' (シングルクォーテーションで囲む) UTF-16の文字(16bit)
string System.String "java" (ダブルクォーテーションで囲む) UTF-16の可変長文字列

使用例

int x = 1;
var x = 1; // varキーワードを使えば自動的に型を推測してくれる
System.Int32 x = 1; // こう書くこともできるが普通はしない

詳細(Microsoft公式ページ)

配列

C#で配列はclassであり、大きさは定義時に決定されます。書き方は以下の通りです。

int[] arr = new int[5]; //大きさ5の配列
string[] arr = new string[] { "a", "b", "c", "d", "e" }; //このように書いて初期化することも可能です。

var temp = arr[0]; //このように書くことで配列の要素にアクセスできます。

多次元配列

int[,] int[,,]のように書くことで多次元配列を作れます。たとえばint[3,4]とすると3×4多次元配列になります。

ジャグ配列

int[][]のように書くことでジャグ配列(ギザギザ配列)を作れます。これは配列の配列となっているので内側の配列の要素の数はそれぞれ違っても構いません。

演算子

定番の+ - * / %(余り)はありますが、pythonにはある累乗の**はありません。また、静的型付け言語なので以下のような整数同士の割り算は小数点以下が切り捨てられます。どちらかをdoubleにキャストしましょう。

int a = 10;
int b = 3;
int result = a / b; // 3
double result = (double) a / b // 3.333...

また、自分自身に+ - * / %を行う+= -= *= /= %=や、自分自身に1だけ足したりひいたりする++ -- などもあります。

int i = 0;
i += 1;    //これは
i = i + 1; //これと同等

i++;       //これは
i = i + 1; //これと同等

比較演算子として< <= > >= == !=があります。これらは前後のオブジェクトを比較して真偽値(bool)を返します。<= >=は数学における≦ ≧に相当し、== !=は数学における= ≠に相当します。また、&& ||はそれぞれ真偽値における論理積・論理和をあらわし、!は真偽値の直前につけることで真偽値を反転させることができます。

※論理積 二つともがtrueのときtrueを返す
※論理和 二つのうち一方がtrueのときtrueを返す。

ifとかforとか

条件分岐

C言語と違い条件式に指定できるのは真偽値だけです。ifステートメントとswitchステートメントが実装されており、詳しくはMicrosoft公式に書いてあります。

int x = 10;
if(x < 0)
{
  //ここに処理を書く
}
else if (x < 10) { /* else ifを使うことでいくつものパターンに対応できる */ }
else
  return 0; // 終わるときはelseで終わる。1文で終わるなら 波カッコ{} は不要

switch(x) //たくさんの条件で分岐したいときに使う
{
    case 10: // xが10の時の処理
        break;
    case 20: // xが20の時の処理
        break;
    default: // xが上のすべてと異なったときの処理
        break;
}

繰り返し

C#には繰り返しの処理としてforステートメント, foreachステートメント, whileステートメント, do-whileステートメントが実装されています。Microsoft公式を見るとここより詳しい情報を得られます。

for(var i = 0; i < 100; i++) // (初期化処理 ; 終了条件 ; ループが終わるごとに実行される)
{
    // 処理を書く
}

var arr = new int[]{ 0, 1, 2, 3, 4 };
foreach(var item in arr) //コレクションのループ
{
    item++;
}
foreach((var item, var i) in arr.Select((num, index) => (num, index))) //インデックス付きのコレクションのループ
{
    item += i;
}

while(true)
{
   // 条件を満たす間繰り返す。
}
do
{
  // 最初の一回実行され後条件を満たす間繰り返す。
}while(true)

デリゲート

C#では関数オブジェクトを格納するものとしてdelegateというもがあり、下のように定義し、使うことが可能です。

public delegate string Hoge(int x);//引数がint型 返り値がstring型の関数型Hoge

public class Program
{
  public static void Main()
  {
    Hoge func = new Hoge(Foo); //クラスをインスタンス化するときみたいにかける。
    Hoge func = Foo; //こうもかける。
  }

  public static string Foo(int x)
  {
    return x.ToString();
  }
}

このように定義できますが、実際はFuncActionを使うことが多いです。この2つはジェネリクスを用いて(厳密には違いますが)下のように定義されており、どんな型でもその場で指定して入れることができるので大変便利です。

public delegate void Action<T1>(T1 arg); //Actionは返り値がvoid
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2); //Funcは返り値がTResult(最後の型)

//使い方
public class Program
{
  public static void Main()
  {
    Func<int,int,int,string> func = Foo; //こんな感じで書けます。
  }

  public static string Foo(int x, int y, int z)
  {
    return x.ToString();
  }
}

コレクション

コレクションはデータをまとめて扱うためのクラスでジェネリクスなしのものはSystem.Collection名前空間に、ジェネリクスありのものはSystem.Collection.Generics名前空間にあります。代表的なコレクションはリストと辞書型があります。

リスト

リストは可変長配列のようなもので、配列とは違い検索メソッドやソートメソッドが実装されてたりします。

var arr = new List<int>();
arr.Add(3); //Addで追加
var temp = arr[0]; //これで中身を見る
arr.Remove(3); //引数と最初に一致したものを削除

詳細はMicrosoft公式で確認してください。

辞書型

辞書型はキーと値をセットで保持することができるコレクションです。

var dic = new Dictionary<string, string>();
dic.Add("key", "value"); //Addで追加
var temp = dic["key"]; //これで取得
dic.Remove("key"); //引数と一致したキーを持つ値を削除

詳細はMicrosoft公式で確認してください。

プロパティ

プロパティはclassやstructのメソッドをあたかもフィールドであるかのように扱う機能です。

public class Hoge
{
    public int Number
    {
        get
        {
            return _number;
        }
        private set // アクセス修飾子を片方だけ変えることが可能
        {
            if(value < 0) // 入ってきた値は valueキーワード に格納されている
                value = 0;
            _number = value;
        }
    }
    private _number;

    public int Id { get; private set; }  // getとsetだけ書いて中身は書かないとコンパイル時に
    public int Id { get{ return _id; } set{ _id = value; } }  // これみたいに展開される
    private int _id;

    public void HogeHoge()
    {
        Number = 10; //setプロパティが呼ばれる
    }
}

フィールドではなくプロパティで実装することのメリットはset時に制約をつけたりget時にnullの場合などで条件分岐したりできることがあります。また、interfaceにはフィールドは定義できないですがプロパティは実際は関数なので定義できます。

名前空間

C#ではコードを名前空間で分割することができす。下のようにクラスやメソッドと同じように宣言することができます。名前空間では同じ名前空間にあるもの、またはusingを使って宣言したものだけが制限なく使えます。別の名前空間のものを使用する際はA.Hogeのように名前空間から指定するかusing A;という文言を書いておく必要があります。

using System;
namespace A
{
   public class Hoge
   { }
}

yield return

yield returnで値を返すことで簡単にIEnumeratorを作ることができます。returnの代わりにyield returnを使うとIEnumeratorが自動的に生成され、foreachステートメントで使用できます。

public static void Main()
{
    foreach(var i in GetEnumerator())
    {
        // 処理
    }
}

private static IEnumerator GetEnumerator()
{
    int i = 0;
    while(true)
    {
        yield return i++;
    }
}

UnityではCoroutineという機能で使わています。

継承

C#はオブジェクト指向の言語なので継承が実装されています。継承とはあるクラスから性質を受け継いだ新しいクラスことで、派生とも呼ばれています。例としては以下のようなものがあります。

class Person
{
  public string name; // 名前
  public int    age;  // 年齢
}

class Student : Person
{
  public int    id;   // 学籍番号
}

継承 - C# によるプログラミング入門 | ++C++; // 未確認飛行 Cより

時間がなかったので継承はあとで書きます。

参考させていただいたサイト

合格おめでとうございます!

令和3年度大阪大学一般入試を突破された新入生の皆様、合格おめでとうございます!
今年度はコロナにより模試日程や授業形態が例年と異なることに加え、本年度から始まった新しい試験の共通テストが実施されたということもあり、受験生は例年よりも大変な思いをされたと思います。ちなみに、中の人は基礎工学部情報科学科の学部4年生ですが、今年の倍率は3.8倍と聞いて驚いております。(今年受けたら落ちてそう...汗)

OUCCでは春に新歓活動として、プログラミングやITスキルに関わる講習会を開催します。詳しい情報はTwitterの新歓アカウント公式サイトにて順次公開しますので、是非ご覧ください!

大学ではたくさんの出会いや学びの機会、今までの人生では味わえなかった経験をすることができます。激動の一年(もしくはそれ以上)を乗り越えた皆様と共に大学生活を送ることができるのを心待ちにしております。

Advent Calender 参加方法

前提

今回使用するサイトは、adventar.orgというものです。このサイトに登録するにあたり、

  • Google
  • GitHub
  • Twitter
  • Facebook

のいずれかのアカウントが必要になりますので、あらかじめご準備をお願いします。

参加登録

まず、指定されたURLを開きます。(2020年度のアドベントカレンダーのURLはこちら)

次に、赤円で囲んだマークをクリックして、アドベントカレンダーと連携させるアカウントを選択します。

そこからは、アカウントごとに連携させる手順があるので、各自で登録をお願いします。

無事にログインできたら、まだ枠が埋まっていない日の中から投稿したい日を選択します。

すると、選択した後に以下のようなウィンドウが現れるので、上の入力部分には投稿する記事の大まかな内容を記入し、下の入力部分には投稿する日になったときにそのサイトのURLを貼り付けます。

以上の手順によりアドベントカレンダーへの登録は完了となります。

院試終わりました

さて、お盆の前の8月上旬の今日この頃、筆者はとうとう院試が終了しました。

私が受けた院試は、大阪大学の大学院情報科学研究科というところで、8月1日と2日の二日に分けて行われました。

今年は今までの院試とは違って全体の問題量が少なくなっており、その分一問当たりの占める得点の重みがあったので、一問一問を大事にしながら問題を解いていきました。

他の大学の院試では、SQL( insert ~ from ...)のような対策をしないといけないのですが、阪大の院試は広く浅く、IPA主催の情報処理技術者試験にも似た雰囲気のものだったと思われます。

合格発表が待ち遠しいですね!()

OUCCの主な活動を振り返る

はじめに

この記事はOUCC Advent Calender 2020 の12日目の記事です。

新入生へ向けてのアドベントカレンダーということで、今回はここ数年でこの クラブ が行った活動を、私の知りうる範囲で振り返りたいと思います。

・4月

4月は新入生歓迎(新歓)の時期ということで、課外活動オリエンテーションで、OUCCがしていることや開発したものを展示し、毎週金曜日には部室で説明会を行いました。

・5月

5月にはいちょう祭があり、OUCCの模擬店では、唐揚げや焼き鳥を販売しました。また展示では、部員が作ったゲームを多くのお客様にプレイしていただきました。
この時期あたりで、部員やOBさんが新入生に向けて講習会を開き、講習会に興味のある新入生や部員が講習会に参加しました。(※注)春の講習会の画像は去年のものです

・6月

6月には確定新歓が行われ、新入生は晴れて正式な部員となります。
去年の確定新歓では、部室でたこ焼きやお菓子を食べながら、新部員と既部員がテレビゲームやボードゲーム、雑談等をして交流しました。
また、OUCCでは2D、3DCG班や競技プログラミング班、WEB班、等があり、各部員は興味のある班活に入って班での活動も行います。

・7月

7月は下旬にテストがあるので、上旬の部会で夏休みの各予定を決め、テスト休みとなります。

・夏休み

8月上旬にテストが終わったらいよいよ夏休みです!!
私たちの部室は冷房設備がなく、夏場はサウナと化してしまうため、部員は基本在宅での作業を行います。基本的には個人個人でやりたいことをやりますが、人によっては班で活動したり、似たようなことをしたい同士で共同開発や勉強会を行います。
昨年の夏休みの主な活動としましては、KC3と 夏合宿 がありました。
また、希望者を募って夏コミにも行きます

・KC3

KC3とは、関西情報系学生団体交流会の略称で、関西の大学の情報系団体に所属する学生たちが交流を深める会です。
参加団体は大阪大学コンピュータクラブのほかに、立命館コンピュータクラブ、立命館大学情報理工学部PJ団体 RiG++、関西大学電気通信工学研究会、関西学院大学機巧堂、近畿大学電子計算機研究会、京大マイコンクラブ等があります。
去年の9月に行われたKC3では、各クラブの紹介と、各 クラブ による講習会がありました。また、最後には懇親会があり、他のクラブ の部員との交流ができます。
KC3について詳しくはこちらから

・夏合宿(開発合宿)

去年は夏合宿で福井に行きました!!
この合宿は開発合宿ということで、2つのチームに分かれ、待ちかね祭に向けてゲームの開発を行い、最終日には各チーム制作物を発表しました。

・10,11月

夏休みが終わり10月になるとまちかね祭の準備に向けて動き出します。
まちかね祭が近づいてくると、模擬店の買い出しの確認をしたり、展示するゲームの開発によるデスマーチが発生したりします。
そして、まちかね祭当日は部員が一丸となって展示・模擬店を行います。

・12,1月

この時期になると部室は冷蔵庫並の寒さになるので、部室にこたつが導入されます。部員たちは、各々の開発を進めたり、課題やテストに追われたりと思い思いのことをします。

・春休み

テストが終わり冬休みになると、部員はバイトをしたり、旅行に行ったり、勉強・開発したりと、それぞれ自分のやりたいことをやります。また、4回生の追いコンも開催しました。(今年は新型コロナウイルスの影響で、追いコンは3,4回生のみでの開催となりました)

追いコン(画像はイメージです)

・まとめ

以上がOUCCのおおまかな活動内容となります。
主な活動曜日は金曜日ですが、部室にはいつでも入れるので、金曜日以外に来て作業をする人もいます。また、月に1回ある部会もオンラインでの参加が可能なので、部室に来れない人でも参加できます。
これまで見てもらった通り、このクラブは活動日を柔軟に決めることができ、他のサークルとの掛け持ちも可能です。
今回は大まかな紹介のみとなりましたが、興味のある人はOUCCのTwitter等を見れば今後の活動が分かると思います。

・最後に

このクラブに限らず、サークルや部活に入ることは、他学科の人との交流や大学生活を何かに打ち込むという点で大変良いものなので、自粛期間がおわり各サークルが新歓を始めたら、いろいろなところを見て回ることをおすすめします。

if文をすっきりさせる

ちょっとしたプログラム記述方法を紹介しようと思います。例えば、次のようなメソッドを記述したとします。

        private void Example0(int x)
        {
            if (x%2==0) {
                // 処理
            }
        }

このif文は必要でしょうか?こんなことを聞くからには、もちろんNoです。ただ、if文が必要ないというのは語弊があります。正確には、処理をif文で囲む必要はありません。次のメソッドは全く同じ動作をします。

        private void Example1(int x)
        {
            if (x%2==1) {
                return ;
            }

            // 処理

        }

xが奇数の時、つまり偶数でないとき、return命令によってメソッド処理を終了させます。すると// 処理 の位置に達したときはxが偶数であることが確定しているので、if文による確認を行わず処理を記述することができます。

この記述方法のうれしいところは、鍵かっこで処理を囲む必要がなくなるところです。例では処理は1行しかありませんが、これが何十行と続くと、最後にぽつんと}が残ることになり、どの鍵かっこと対応しているのかわかりにくくなってしまいます。鍵かっこを使わないpythonのような言語であっても、インデントが右にいきすぎてしまい、プログラムが見にくくなること必至です。

        private bool Example2()
        {

             if (ReturnBooleanMethod1()) {
                // 処理
                if (ReturnBooleanMethod2()) {
                    // 処理
                    if (ReturnBooleanMethod3()) {
                        // 処理
                        if (ReturnBooleanMethod4()) {
                            // 処理
                            if (ReturnBooleanMethod5()) {
                                // 処理
                                return true;
                            }
                        }
                    }
                }
            }
            return false;
        }

この記述方法の問題点は、if文の条件がわかりにくくなることです。xが偶数の時処理をしたいのに、if文の条件式は否定である「xが奇数であるか」の確認を行っています。人によっては処理の流れが追いづらいと感じたり、条件によっては否定の記述が難しい場合があります。1つ前の例では、順にメソッドの条件を満たした場合処理をし、次の確認事項を調べて・・・という流れがわかると思います。この記述方法で書いた場合は次のようになります。

       private bool Example3()
        {
            if (!ReturnBooleanMethod1()) {
                return false;
            }
            // 処理
            if (!ReturnBooleanMethod2()) {
                return false;
            }
            // 処理
            if (!ReturnBooleanMethod3()) {
                return false;
            }
            // 処理
            if (!ReturnBooleanMethod4()) {
                return false;
            }
            // 処理
            if (!ReturnBooleanMethod5()) {
                return false;
            }
            // 処理
            return true;
        }

インデントやかっこは見やすくなりましたが、何となくどういう場合にReturnBooleanMethod4()の確認を行うのかわかりにくいですよね?少なくとも僕にはわかりにくいです。また、メソッド内の最初と最後以外でreturnによって抜けると、処理していると思っていたコードが、実はその前にreturnしていて処理していない!ふざけんな!というような事態になったりするので注意です。

この記述方法は、メソッドだけでなくforループからの脱出(break,continue)でも使えます。また、if ... else break のような場合にも、breakする条件を記述することで、そのあとの処理をif文で囲む必要がなくなります。また、elseが消えてますよね。次のコードはbreak文の例を紹介するためだけに書いたどうでもいい動作をするプログラムです。

        private void Example4()
        {
            int sum = 0;
            for (int i=0;i<10;i++) {
                if (i<=5) {
                    sum += i;
                }
                else {
                    break;
                }
            }

            for (int i = 0; i < 10; i++) {
                if (i>5) {
                    break;
                }
                sum += i;
            }

        }

{}で1行とらないような記述方法をとるなら行数は変わりませんが、処理が複数ある場合は{}で1行とる人がほとんどだと思うので、そこそこ使えると思います。

プログラムの見やすい記述方法、書きやすい記述方法は人それぞれだと思います。自分に合った記述方法でデバッグをしやすい、ストレスのたまらないプログラムを書きましょう。

OpenCVでMONO消しゴムのデザインをイタリアの国旗にする

~あらすじ~
道のり① 消しゴムの青いところを取得
道のり② MONO消しゴムを判別
道のり③ MONO消しゴムをイタリア化
結果
コード
参考・その他

・~あらすじ~

 クリスマスではないけど、一応アドベントカレンダーの名目で二日目の記事書かせてもらいますOUCCの2DCG班長です。最初は適当にunityの音声認識の記事でも書いてやり過ごそうと思ってたんですが、一日目の先代の部長が結構頑張ってたんで、それを見て急遽書く内容変更して、一日で出来るけどそこそこ難易度のあるテーマを考えました。シートン学園を見ながら3時間くらい悩んだ結果、PCの隣にあったMONO消しゴムのデザインがエストニアの国旗に似ているな~とふと気が付いて、現在に至ります。


・道のり① 消しゴムの青いところを取得

 コードが300行あって説明が大変なので要点をかいつまんで書きます。読んでも幸せになれない可能性があるので、青い鳥をお探しの方は結果まで飛ばした後、AMAZONにお買い求めください。

※実行環境はwindows10、Python3.8で、使用ライブラリはOpenCVとNumPyです。

・MONO消しゴムが写っている画像を読込み

・読み込んだ画像をガウス平滑化

・cv2.inRangeで青周辺の色域選択&選択部分取得

・cv2.dilateで白い部分を膨張させ、erodeで収縮

↑(これは白い部分の中に散見される黒い所を出来れば除去したいなという希望でやってます。)

・cv2.findContoursで輪郭を取得

・各々の輪郭についてcv2.contourAreaで輪郭領域内の面積を求め、小さすぎる領域や、大きすぎる領域を除外。

・一面真っ黒の画像をnp.zerosで生成。

・各輪郭について黒一色の画像にcv2.drawContoursで輪郭を描画

・cv2.momentsで輪郭で囲まれた各領域の重心を求める。

・cv2.floodFillで先ほど輪郭を描画した画像について重心を含む領域を塗りつぶし。

↑領域の中の黒い点を塗りつぶしで除去

・塗りつぶし画像のnp.averageが大きければ塗りつぶし失敗なので、cv2.bitwise_notで反転。

・こうして作成された塗りつぶされた画像(輪郭の数だけ存在)と輪郭情報をペアでリスト(名前:blue_area)に格納。

・消しゴムの黒い部分に関しても同様にして、リスト(名前:black_area)に格納。

輪郭を描画した画像

・道のり② MONO消しゴムを判別

MONO消しゴムは青から少し離れて黒の領域があるという性質を利用して消しゴムの柄の場所を特定していきます。

・black_areaとblue_areaの要素の全ての組み合わせについて、
     
  1. ①2つの塗りつぶされた画像の膨張を前述のdilateで行う。
  2.   
  3. ②cv2.bitwise_andで重なった領域を取得する
  4.   
  5. ③2つの画像の重なった領域がnp.averageが0より大きいかを見て存在を確認し、存在すればその組み合わせを除外(一つの消しゴムの青い領域と黒い領域が接することは無いため。)
  6. ④np.concatenateで2つの輪郭領域情報のリストを結合する。

  7. ⑤cv2.minAreaRectで二つの領域に外接する最小の長方形を取得。
  8.   
  9. ⑥2つの塗りつぶされた領域の面積を前述の方法で求める。
  10.   
  11. ⑦長方形の面積を求める。
  12.   
  13. ⑧長方形の面積が塗りつぶされた面積に対し比較的大きかったり小さければその組み合わせを除外(青と黒の領域が離れている組み合わせを除外)
  14.   
  15. ⑨除外されずに残った組み合わせの先ほど作成した長方形を保持。
青い部分の抽出された画像(blue_areaの要素の一つ)と黒い部分の抽出された画像(black_areaの要素の一つ)をOR演算で合成し、外接矩形を表示した画像。心の清い人には外接しているように見えるはず。長方形の面積が白い部分の面積より比較的大きいため、この組み合わせは正しくないと判別する。

・保持された全ての長方形と保持された他の全ての長方形との組み合わせしていき、一方の長方形がもう一方の長方形にほとんど含まれているとき、大きい方の長方形を除外。

↑この判別法は、長方形の領域を塗りつぶし、AND演算を行って重なった部分の比率がどれくらいあるかで判別しています。

・残った領域一つ一つについて、その領域の部分だけをマスキングして切り取る。(読み込んだ画像と長方形の中身を塗りつぶした画像のAND演算)

↑なお、この過程で次元の低い長方形塗りつぶしの画像はcv2.COLOR_GRAY2BGRで3次元に変換。

画像にマスキングをかけて消しゴム一個だけ取得した画像

hitomatagiさんのコードにかけて特徴量マッチングし、コード中のgoodの個数がある一定水準を超えないものを除外。

特徴点を結んでいった画像

ここまでクリアしたものををMONO消しゴムと認定します。


・道のり③ MONO消しゴムをイタリア化

ここから色付けをしていきます。...とその前にもう3時近いので寝ます。おやすみなさい~

おはようございました。現在12時です。どうやら記事を書いていたのは夢落ちではないそうですね。さて...

・各長方形を塗りつぶした画像と読み込んだ画像の青い部分を取得した画像のANDを取る。

・AND演算して出来た画像の白い部分が存在するピクセルの座標と、同じ座標の読み込まれた画像の場所の色を緑に変更。(2重ループ文で一つ一つのピクセルを処理)

・同様にして黒の部分を赤に変更

完成!


・結果

イタリアと化したMONO消しゴム
他の画像での実行結果

 とても精度が悪いですね。影は仕方ないとしても、文字の部分や、ノイズは頑張ればどうにかなる部分です。でも作者は途中で力尽きました。許して下さい。


・コード

RTAして書いたコードなので絶対見ない方がいいですよ。呪われます。コード整理をしてなくてもOKで、呪い耐性がある方だけどうぞ。


import  cv2
import  numpy as np
import compare #引用したコードを記載した場所

def main():
    sample=cv2.imread("./data/monoEraser.jpg")
    testImg=cv2.imread("./data/test1.jpg")
    height, width, channels = testImg.shape
    image_size = height * width
    testImg_b= cv2.GaussianBlur(testImg, (9, 9), 2)
    hsv=cv2.cvtColor(testImg_b,cv2.COLOR_BGR2HSV)
    lower = np.array([110, 50, 50])
    upper = np.array([240, 255, 255])
    frame_mask = cv2.inRange(hsv, lower, upper)  

    img2,blueArea = getColorArea(frame_mask, testImg)
    lower = np.array([0, 0, 0])
    upper = np.array([180, 255, 50])
    frame_mask2=cv2.inRange(hsv,lower,upper)

    kernel = np.ones((5, 5), dtype=np.uint8)

    frame_mask2 = cv2.dilate(frame_mask2, kernel)
    frame_mask2 = cv2.erode(frame_mask2, kernel)


    testImg_g=cv2.cvtColor(testImg,cv2.COLOR_BGR2GRAY)

    img_canny=cv2.Canny(frame_mask2,300,400)


    img,blackArea=getColorArea(frame_mask2, testImg)
    rects=[]
    for i, blue_a in enumerate(blueArea) :
        for j,black_a in enumerate(blackArea):
            b,rect=checkAreaRatio(testImg,blue_a,black_a)
            if(b):
                rects.append(rect)
    deleteRects=[]
    for i, rect1 in enumerate(rects):
        for j,rect2 in enumerate(rects):
            if(i>j):
                k= checkContain(rect1,rect2,height,width)
                if(k==1):
                    deleteRects.append(rect2)
                elif(k==2):
                    deleteRects.append(rect1)
    for rect1 in deleteRects:
        rects.remove(rect1)
    deleteRects=[]
    for rect1 in rects:
        if(not compareTrait(sample,img,rect1)):
            deleteRects.append(rect1)
    for rect1 in deleteRects:
        rects.remove(rect1)
    for rect1 in rects:
        img=changeColorItaly(img,rect1)

    cv2.imshow("sample2", img)
    cv2.imwrite("./data/output6.png", img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
def changeColorItaly(img,rect):
    height, width, channels = img.shape
    mask = np.zeros((height, width), dtype=np.uint8)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    mask1 = np.copy(mask)
    mask1 = cv2.drawContours(mask1, [box], 0, 255, 1)
    mask1 = fillArea(mask1, int(rect[0][0] + 2), int(rect[0][1] + 2))
    mask2 = np.copy(mask)
    mask2 = cv2.drawContours(mask2, [box], 0, 255, 1)
    mask2 = fillArea(mask2, int(rect[0][0] + 2), int(rect[0][1] + 2))
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    lower = np.array([110, 50, 50])
    upper = np.array([240, 255, 255])
    frame_mask = cv2.inRange(hsv, lower, upper)
    lower = np.array([0, 0, 0])
    upper = np.array([180, 255, 50])
    frame_mask2 = cv2.inRange(hsv, lower, upper)
    mask1=cv2.bitwise_and(frame_mask,mask1)
    mask2=cv2.bitwise_and(frame_mask2,mask2)
    mask1=cv2.cvtColor(mask1,cv2.COLOR_GRAY2BGR)
    mask2=cv2.cvtColor(mask2,cv2.COLOR_GRAY2BGR)
    for x in range(height):
        for y in range(width):
            b, g, r = mask1[x, y]
            if (b, g, r) == (0, 0, 0):
                continue
            img[x, y] = 99, 135, 0
    for x in range(height):
        for y in range(width):
            b, g, r = mask2[x, y]
            if (b, g, r) == (0, 0, 0):
                continue
            img[x, y] = 57, 41, 206
    return img

def compareTrait(sample,img,rect):
    height, width, channels = img.shape
    mask = np.zeros((height, width), dtype=np.uint8)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    mask1 = np.copy(mask)
    mask1 = cv2.drawContours(mask1, [box], 0, 255, 1)
    mask1=fillArea(mask1,int(rect[0][0]+2),int(rect[0][1]+2))

    mask1=cv2.cvtColor(mask1,cv2.COLOR_GRAY2BGR)
    img2=cv2.bitwise_and(img,mask1)

    return compare.compare(sample,img2)

def checkContain(rect1,rect2,height,width):#領域の中に領域があるかチェック
    mask = np.zeros((height , width ), dtype=np.uint8)
    box=cv2.boxPoints(rect1)
    box = np.int0(box)
    mask1 = np.copy(mask)
    mask1 = cv2.drawContours(mask1, [box], 0, 255, 1)
    mask1=fillArea(mask1,int(rect1[0][0]+2),int(rect1[0][1]+2))
    box = cv2.boxPoints(rect2)
    box = np.int0(box)
    mask2 = np.copy(mask)
    mask2 = cv2.drawContours(mask2, [box], 0, 255, 1)
    mask2 = fillArea(mask2, int(rect2[0][0] + rect2[1][0] / 2),int( rect2[0][1] + rect2[1][1] / 2))
    mask3=cv2.bitwise_and(mask1,mask2)
    contours, hierarchy = cv2.findContours(mask3, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    if(len(contours)<=0):
        return  0
    area3 = cv2.contourArea(contours[0])

    area1=rect1[1][0]*rect1[1][1]
    area2 = rect2[1][0] * rect2[1][1]
    ratio=0.9
    if(area3>area1*ratio):
        return  1
    elif(area3>area2*ratio):
        return  2
    else:
        return 0

def checkAreaRatio(img,mask1,mask2):
    mask1,cnt_mask1=mask1
    mask2, cnt_mask2 = mask2
    kernel = np.ones((5, 5), dtype=np.uint8)
    mask4=cv2.dilate(mask1,kernel)
    mask5=cv2.dilate(mask2,kernel)
    mask6=cv2.bitwise_and(mask4,mask5)

    if(np.average(mask6)>0):#隣り合っている領域を除外
        return False,None
    area1=cv2.contourArea(cnt_mask1)
    area2=cv2.contourArea(cnt_mask2)
    cnt_mask3=np.concatenate([cnt_mask1,cnt_mask2])

    rect = cv2.minAreaRect(cnt_mask3)
    area3= rect[1][0]*rect[1][1]
    if (area3 > (area1 + area2) * 3 or area3<(area1 + area2) * 1.6):#比率で除外
        return False,None
    #box = cv2.boxPoints(rect)
    #box = np.int0(box)
    #img2=np.copy(img)
    #img2 = cv2.drawContours(img2, [box], 0, (0, 0, 255), 2)

    return  True,rect

def getColorArea(frame_mask, draw_img, draw=False):
    ret, testImg_g2 = cv2.threshold(frame_mask, 100, 255, cv2.THRESH_BINARY)

    contours, hierarchy = cv2.findContours(testImg_g2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    height, width, channels = draw_img.shape

    img=draw_img
    if(draw):
        img = cv2.drawContours(draw_img, contours, -1, (0, 0, 255, 255), 2, cv2.LINE_AA)
    cont_area=[]

    for i, contour in enumerate(contours):
        area = cv2.contourArea(contour)
        image_size=height*width
        if area < 500:
            continue
        if image_size * 0.99 < area:
            continue
        epsilon = 0.02 * cv2.arcLength(contour, True)
        approx = cv2.approxPolyDP(contour, epsilon, True)
        if(draw):
            cv2.drawContours(img,[approx], -1,  (255, 0, 255), 2)

        mask = np.zeros((height, width), dtype=np.uint8)
        cv2.drawContours(mask, [approx], -1, (255, 0, 255), 2)
        M = cv2.moments(contour)
        mask= fillArea(mask,int(M['m10']/M['m00']),int(M['m01']/M['m00']))#引数に重心を入れている


        cont_area.append((mask,contour))




    return  img,cont_area
def fillArea(img,startx,starty,color=(0,0,255)):
    channels=0
    height=0
    width=0
    if(img.ndim==2):
        channels=1
        height, width = img.shape
    elif(img.ndim==3):
        height, width, channels = img.shape
    mask = np.zeros((height+2, width+2), dtype=np.uint8)#+2しないとエラー
    if(channels==3):
        pass
    else:
        color=255
    retval, img2, mask, rect = cv2.floodFill(img, mask, seedPoint=(startx, starty), newVal=color)
    if(np.average(img2)>100):#もし塗りつぶしが多ければ
        img2=cv2.bitwise_not(img2)#反転
    return img2

if __name__ == '__main__':
    main()

・参考・その他

・OpenCV 3とPython 3で特徴量マッチング(A-KAZE, KNN):

https://qiita.com/hitomatagi/items/caac014b7ab246faf6b1

・OpenCV 2.2 C++ リファレンス:

http://opencv.jp/opencv-2svn/cpp/index.html

・OpenCV-Python チュートリアル :

http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/index.html

筆者:OUCC 2DCG・AI班長 上月

Unityでノベルゲームを制作中

はじめに

この記事はOUCC Advent Calender 2019の23日目の記事です(完成したのは12/31です)

パワポケから野球要素を抜いたノベルゲームみたいなのを作りたかったので、12月から土台作りを始めました。
土台作りといっても基礎となる部分は以下のサイトを参考に作成しました。

今回はこれをパワポケ風に近づけるために追加した機能を書きたいと思います。

1 キャラの立ち位置を二か所にする

パワポケでは左右にキャラが立って、会話が展開されるので、まずはキャラを左右に立たせるようにしました。このとき、3人目のキャラを追加するときは、既にいるどちらか二人のうちの一人を消して、3人目のキャラを追加するようにしました。

Gif画像をUpしたかったのですが、容量的に上げられませんでした...

2 背景画像を変える

#bg_imageで背景を変えられるようにしました。

3 真ん中に画像が出るようにする

背景と同じ要領で真ん中に画像が出るようにしました。背景との違いは#center_imageで真ん中に画像が出現して、#center_image_offでその画像を見えなくするといったところです。

4 選択肢の内容をメッセージボックスに表示する

選択肢のボタンを表示させて、ボタンに触れるとそのボタンのテキストの内容をメッセージボックスに表示するようにしました。

5 その他機能

そのほかに追加した機能としては、BGMを変える機能や、SEを鳴らす機能、コマンドで使用している'#'をメッセージボックスに表示させるためのエスケープシーケンスを実装しました。

6 まとめ

以上述べたような機能を実装しましたが、パワポケ風にするためには、パラメーターの追加やフラグ管理など、追加しなければならないことが沢山あるのでこれからも追加していきたいと思います。
今回は容量の都合上、gif画像を使えなかったので、次に投稿するときはgif画像を上げられるようなブログで書きたいなと思います。

お借りした素材

キャラクターの素材:いらすとや様
背景素材:あやえも研究所様
出演:部室に侵入した猫様

Unityで複数のスクリプトを同様に扱いたい

・あらすじ 

~ Unityで武器換装機能を実装したい~

 先週VRのシューティングゲームを作っている最中に武器の換装機能を徹夜で実装する機会がありました。

 その際プレイヤーやどこかのGameObjectに全ての武器の処理を書くのが煩雑で嫌だなと思ったので、武器のオブジェクトにその武器の動作を規定するスクリプトを張り付けることにしたんです。

 Oculus IntegrationアセットのOVRGrabberスクリプトのgrabbedObjectに現在つかんでいるオブジェクトが格納されるそうなので取得した武器オブジェクトからGetComponentでスクリプトを取得したいと思っていたんですが、unityの仕様上それぞれの武器につけるスクリプト名は同一の名前を使用出来ないので GetComponent で一様に処理できないことに気が付いて、AM2時を迎えた私は大変困りました。

 それで何分悩んだかわからないけど死にかけの頭で考えて何とか実装には成功したので、今後同じ事態にはまった時のためにメモとしてここに記述します。

・ GetComponents

 GetComponents<>()というメソッドがあって、ここでMonoBehaviourを指定すればMonoBehaviourを継承したスクリプト、つまり自作のスクリプト(の大半)を全て取得できるそうです。

public class TimerManager : MonoBehaviour 

  (↑実はこの:の後の部分が継承元だったんですね。私はC#を勉強せずにUnity書いてる人なので知りませんでした。) 

 しかし、MonoBehaviourを指定して取得したインスタンスは MonoBehaviour に存在するメソッドしか使用できないみたいです。

・解

 よって次のように MonoBehaviourを継承した、使用したいメソッドを記述したクラスを生成し、

public class guns : MonoBehaviour
 {
  public virtual void shoot(){}
}

 さらにそれを武器の動作を記述したクラスが継承すれば、 GetComponents<guns>() で取得し、shootメソッドを使用することが出来るようになります。やったね。

public class gun_makrov : guns//継承
{
   public virtual void shoot(){
   //内容
  } 
}

筆者:OUCC 2DCG班班長、AI班班長

ちなみに筆者は当日実装間に合ってません。