C++OpenSiv3D入門講座 Vol. 04 参照・クラスのポインタ

C++OpenSiv3D入門講座

オブジェクトから他のオブジェクトの情報を得るための参照・ポインタについて学び、OpenSiv3Dの実例ではEnemyからPlayerの情報を参照する。

bool型

C++には真偽を表すためだけの型、bool型が存在する。

#include <iostream>

int main(){

    bool b = true;

    if (b){
        std::cout << "条件文は真" << std::endl;
    }
    else{
        std::cout << "条件文は偽" << std::endl;
    }

    return 0;
}

以下のように、条件を満たすかどうかの真・偽を返す関数の返り値の型としてbool型を使うことが多い。

#include <iostream>

// 偶数だったらtrue,そうでなければfalseを返す関数
bool isEven(int num){
    return (num % 2) == 0;
}

int main(){

    if (isEven(10)){
        std::cout << "条件文は真" << std::endl;
    }
    else{
        std::cout << "条件文は偽" << std::endl;
    }

    return 0;
}   

スコープ

{} で囲まれた部分を抜けると、その{}内部で宣言された変数は破棄されるので、アクセスできなくなる。 変数や関数の「見える」範囲をスコープと言い、変数の寿命はスコープによって決まる。

#include <iostream>
void func(){
int a = 10;
std::cout << "aの値は " << a << std::endl;
}
int main() {
    func();
    //コメントを外すとコンパイルエラー
    //a = 100;  
    if (true) {
        int b = 20;
        std::cout << "bの値は " << b << std::endl;
    }
    //コメントを外すとコンパイルエラー
    //b = 100; 
    for (int i = 0; i < 3; i++) {
        std::cout << "iの値は " << i << std::endl;
    }
    //コメントを外すとコンパイルエラー
    //i = 100; 
    return 0;
}

{}だけでもスコープを作ることが出来る

#include <iostream>

int main(){
    {
        int b = 20;
        std::cout << "bの値は " << b << std::endl;
    }

    //コメントを外すとコンパイルエラー
    //b = 100; 

    return 0;
}

参照

C++ではポインタを扱いやすくした参照(リファレンス)というものが存在する。変数に別名を付けて参照することが可能となる機能である。

データ型& 名前
例:int& a

以下の例では、参照型のaliasでhogeを参照し、aliasからhogeの値を変えている。

#include <iostream>

int main(){

    int hoge = 2;

    //参照型のaliasで変数hogeを参照する(aliasを操作するとhogeの値が変わる)
    int& alias = hoge;

    std::cout << hoge << ", " << alias << std::endl;

    alias=4;
    std::cout << hoge << ", " << alias << std::endl;

    return 0;
}

ポインタ型と参照型

関数の中で引数で渡したものを変更するためにポインタを用いる事があると思う。 以下はその例である。

ポインタを用いたaとbを入れ替える関数

#include <iostream>
//aとbを入れ替える関数 
void swap(int *a, int *b){
    int tmp=*a;
    *a=*b;
    *b=tmp;
    return;
}

int main(){

    int a=3;
    int b=5;

    std::cout << a << "," << b << std::endl;

    swap(&a, &b);//aとbのポインタを渡さなければいけない

    std::cout << a << "," << b << std::endl;

    return 0;
}  


aとbを入れ替える関数のよくある間違い

#include <iostream>
//aとbを入れ替える関数 関数内でa, bを書き換えても元のa, bは書き換わらない
void swap(int a, int b){
    int tmp=a;
    a=b;
    b=tmp;
    return;
}

int main(){

    int a=3;
    int b=5;

    std::cout << a << "," << b << std::endl;

    swap(a, b);

    std::cout << a << "," << b << std::endl;

    return 0;
}


以下は参照を用いたaとbを入れ替える関数である。正しくaとbの値が入れ替わっており、かつポインタを用いた例より読みやすく書けていることがわかる。

#include <iostream>
//aとbを入れ替える関数
void swap(int& a, int& b) {
    int tmp = a;
    a = b;
    b = tmp;
    return;
}

int main() {

    int a = 3;
    int b = 5;

    std::cout << a << "," << b << std::endl;

    swap(a, b);

    std::cout << a << "," << b << std::endl;

    return 0;
}

ポインタを使っても同じことが出来るが、*&をつけるのに手間がかかり、ミスをしてバグを起こしやすいので、これからは参照を使おう。

値渡し・ポインタ渡し・参照渡し

void swap(int *a, int *b)のように、ポインタで引数を渡すことを「ポインタ渡し」という。
void swap(int& a, int& b)のように、参照で引数を渡すことを「参照渡し」という。
上記2つに対し、void swap(int a, int b)のように引数を渡すことを「値渡し」という。

値渡し

値渡しを行うと、コピーが行われる。例えば、int型が引数であれば、その数値がコピーされて関数の中で使われる。コピーされた引数を関数の中で変更しても元の変数は書き換えられない。

クラス等、サイズの大きな型は値渡しにするべきではない。コピーに時間がかかったり、オブジェクトの再構築で予期せぬ動作をする可能性がある。

ポインタ渡し・参照渡し

ポインタ渡しや参照渡しは関数の呼び出し元の実体を扱う。そのため、呼び出し元の変数を書き換えることができる。

また、オブジェクトのコピーが発生することを防ぐことができ、クラス等を渡す時に向いている。

const参照渡し

参照渡しを使うと、コピーが発生することを防ぐことができ、無駄がなくなる。ただし、参照渡しをすると呼び出し元の変数を書き換えることが出来てしまう。 呼び出し元の変数を書き換えてしまうのを防ぎたい時には、引数にconstを付けることで、関数内で値を変更できなくなる。 以下は、const参照渡しを使う関数の例である。本資料では以降クラスを引数に取る時、適宜参照渡し、const参照渡しを使っていく。

#include <iostream>

class Color {
public:
    int r, g, b;
    Color(int _r, int _g, int _b) :
        r{ _r },
        g{ _g },
        b{ _b }
    {
    }
};

void showColor(const Color& color) {
    std::cout << "r:" << color.r << " g:" << color.g << " b:" << color.b << std::endl;
    // コメントを外すと値を書き換えることになるのでコンパイルエラー
    // color.r = 0;
}

int main() {

    Color color{ 150, 100, 50 };

    showColor(color);

    return 0;
}

オブジェクトへのポインタ

オブジェクトを指すポインタからオブジェクトの要素にアクセスしたい時がある。下の例では、オブジェクトを指すポインタ、ptrから、objのxにアクセスしたい。(*ptr).xと書けばobjのxにアクセスできるが、少々書きづらいので、ptr->xと書くことができるようになっている。

(*ptr).xptr->x は同じ意味である。-> をアロー演算子という。

#include <iostream>

class Vector2{
public:
    int x, y;

    Vector2(int _x, int _y)
    {
        x = _x;
        y = _y;
    }
};

int main(){

    Vector2 obj{ 12, 34 };
    Vector2 *ptr = &obj;

    //普通に表示
    std::cout << obj.x << " " << obj.y << std::endl;

    std::cout << (*ptr).x << " " << (*ptr).y << std::endl;

    std::cout << ptr->x << " " << ptr->y << std::endl;


    //↓このようには書けない。 *(ptr.x)と解釈されるから
    //std::cout << *ptr.x << " " << *ptr.y << std::endl;


    return 0;
}

ちなみに、*ptr.xと書くと*(ptr.x)と解釈され、ptrのx(存在しない)が指すものを表す。

まとめ

  • bool型は真偽(true, false)を表す型である。
  • 変数や関数の見える範囲({}で囲まれた範囲)をスコープと言い、変数の寿命はスコープによって決まる
  • C++ではポインタを扱いやすくした参照(リファレンス)というものが存在する
  • クラス等を関数の引数にする時は参照渡しをしよう
  • クラス等を参照渡しする際、値を変更してほしくない時はconst参照渡しをしよう
  • オブジェクトを指すポインタからオブジェクトの要素にアクセスするときはアロー演算子->を利用できる

演習問題(コンソール)

  1. 参照でint型を受け取り2倍にする関数を作れ。

演習問題(OpenSiv3D)

今回は、これまでコードを書いてきたプロジェクトとは別に、プロジェクトを作ることを推奨します。

  1. 以下の様なPlayerクラスとEnemyクラスを用意した。PlayerクラスとEnemyクラスのインスタンスを作り、動作を確認せよ。
    #pragma once
    #include <Siv3D.hpp>
    
    class Player {
    public:
        const double Speed;
        Vec2 pos;
        Player();
        void update();
        void draw();
    };
    
    #include "Player.h"
    
    Player::Player() :
        pos{ 320.0, 240.0 },
        Speed{ 50.0 }
    {
    }
    
    void Player::update() {
        //このフレームで進む距離の計算
        const double delta = Scene::DeltaTime() * Speed;
    
        //上下左右キーで移動
        if (KeyLeft.pressed()) {
            pos.x -= delta;
        }
        if (KeyRight.pressed()) {
            pos.x += delta;
        }
        if (KeyUp.pressed()) {
            pos.y -= delta;
        }
        if (KeyDown.pressed()) {
            pos.y += delta;
        }
    }
    
    void Player::draw() {
        Circle{ pos, 30.0 }.draw(Color{ 0, 0, 255 });
    }
    
    #pragma once
    #include <Siv3D.hpp>
    #include "Player.h"
    
    class Enemy {
    public:
        Vec2 pos;
        Vec2 velocity;
        Enemy(const Vec2& _pos);
        void update();
        void draw();
    };
    
    # include "Enemy.h"
    
    Enemy::Enemy(const Vec2& _pos) :
        pos{ _pos },
        velocity{ 0.0, 0.0 }
    {
    }
    
    void Enemy::update() {
        pos += Scene::DeltaTime() * velocity;
    }
    
    void Enemy::draw() {
        Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
    }
    
  2. EnemyがPlayerの方向に移動するようにしたい。Enemyクラスが「Playerクラスへのポインタ」をメンバに持つようにして、PlayerクラスとEnemyクラスのインスタンスを生成した後にEnemyのインスタンスににPlayerのインスタンスのポインタを渡し、そのポインタからPlayerクラスのx,yにアクセスすることでEnemyがPlayerの位置を取得し、その方向に移動できるようにせよ。
    (今回は、敵の追尾は大まかでも良い。例:PlayerのxがEnemyのxより大きければEnemyは右に、そうでなければ左に動く…など)

ヒント

  • Enemyクラス内でPlayerクラスを扱う為に、Enemy.hからPlayer.hをincludeしている。

  • EnemyクラスにPlayerへのポインタを保存するメンバ変数を追加し、コンストラクタで初期化できるようにする。
#pragma once
#include <Siv3D.hpp>
#include "Player.h"
class Enemy {
public:
    Vec2 pos;
    Vec2 velocity;
    Player* pPlayer; //追加
    Enemy(const Vec2& _pos, Player* ptr); //コンストラクタでpPlayerを初期化できるようにする
    void update();
    void draw();
};
Player player; //Playerをインスタンス化
Enemy enemy{ Vec2{ 480, 0 }, &player }; //Enemyをインスタンス化、playerのポインタを渡す

C++OpenSiv3D入門講座

C++OpenSiv3D入門講座 Vol. 03 vectorの基本

C++OpenSiv3D入門講座

今回は動的配列vectorの基本について学ぶ。 演習では、OpenSiv3Dを用いて複数の敵を生成する。

vectorの基本的な使い方

C言語の配列は固定長だったが、C++には必要に応じて自動的にサイズを変更してくれる配列、vectorがある。 vectorの要素には配列と同じように[]でアクセス出来る。 vectorを使うときはvectorヘッダをインクルードする。(vector.hでないことに注意!)

#include <iostream>
#include <vector>

int main(){

    std::vector<int> vec;//int型の動的配列

    // 10個の要素を追加していく
    for(int i = 0; i < 10; i++ ){
        vec.emplace_back(i);
    }

    //出力
    for(int i = 0; i < vec.size(); i++ ){
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;

    std::cout << "現在のサイズ : " << vec.size() << std::endl;

    return 0;
}

書き方

std::vector<動的配列の中に入れる型> 動的配列の変数名

のように書く。

例:double型の動的配列ary1を作成する場合
std::vector<double> ary1;

追加の方法

動的配列名.emplace_back(追加するデータ)

のように書く。

例:int型の動的配列aryに 1を入れるとしたら
ary.emplace_back(1);

また、クラス名{ コンストラクタ引数 }でクラスの実体を作成できることを利用して、以下のように書くこともできる。

例:Vector2型の動的配列aryに(1, 3)のVector2を入れるとしたら
ary.emplace_back(Vector2{ 1, 3 });
(Vector2{ 1, 3 }でVector2型の実体を作成し、それをaryに入れている。)

配列のサイズの取得

動的配列名.size()

クラスのvector

#include <iostream>
#include <vector>

class MyClass{
public:
    int x;

    MyClass(int _x) :
        x{ _x }
    {
    }
};


int main(){

    std::vector<MyClass> vec;

    //要素を追加
    for(int i = 0; i < 10; i++){
        vec.emplace_back(MyClass{ rand()%10 });
    }

    //要素MyClassのxを表示
    for(int i = 0; i < vec.size(); i++){
        std::cout << vec[i].x << " ";
    }

    std::cout << std::endl;//改行

    return 0;
}

Tips:インスタンスの作成と同時に関数を呼ぶ例

以下の例では、色を表すMyColorクラス、円を表すMyCircleクラスを用意し、インスタンスを作成しそのまま関数の引数に入れる例と、インスタンスの作成と同時に関数を呼ぶ例を示している。このように、C++はCに比べ、エレガント(変態的ともいう)な表記が可能になる。

#include <iostream>
// 色を表すクラス (赤成分,緑成分,青成分):(r,g,b)
class MyColor {
public:
    int r, g, b;
    MyColor(int _r, int _g, int _b) :
        r{ _r },
        g{ _g },
        b{ _b }
    {
    }
};
// 円を表すクラス 座標:(x,y)、半径:r
class MyCircle {
public:
    int x, y, r;
    MyCircle( int _x, int _y, int _r ) :
        x{ _x },
        y{ _y },
        r{ _r }
    {
    }
    // 自分の情報と、引数にとったMyColorの情報を出力する関数
    void showSelfAndColor(const MyColor& color) {
        std::cout << "Circle:(x, y, r) = (" << x << "," << y << "," << r << ")"
            << " Color:(r, g, b) = (" << color.r << "," << color.g << "," << color.b << ")" << std::endl;
        std::cout << std::endl;
    }
};
int main() {
    // MyCircleのインスタンスがmyCircleA変数に、MyColorのインスタンスがmyColorA変数に入り、それをshowSelfAndColorで表示する
    MyCircle myCircleA{ 10, 10, 5 };
    MyColor myColorA{ 70, 80, 90 };
    myCircleA.showSelfAndColor(myColorA);
    // MyCircleのインスタンスがmyCircleB変数に入り、MyColorのインスタンスは生成とともにshowSelfAndColorの引数となり表示される
    MyCircle myCircleB{ 20, 20, 6 };
    myCircleB.showSelfAndColor(MyColor{ 100, 110, 120 });
    // MyCircleのインスタンスの生成と同時にshowSelfAndColor関数を呼ぶ。
    // またMyColorのインスタンスも生成とともにshowSelfAndColorの引数となり表示される
    MyCircle{ 30, 30, 7 }.showSelfAndColor(MyColor{ 200, 210, 220 });
    return 0;
}

まとめ

  • クラス名{ コンストラクタ引数 }でクラスの実体を作成できる
  • std::vector<動的配列の中に入れる型> 動的配列の変数名のようにしてvectorを作成する
  • 動的配列名.emplace_back(追加するデータ)のようにしてvectorに要素を追加する。
  • 動的配列名.size()のようにしてvectorのサイズを取得する。

演習問題(コンソール)

  1. int型のvectorを用意し、ランダムに0~9の数を10個入れて、すべて表示せよ。 ヒント:ランダムな0~9の数を生成するにはrand()%10とすればよい。
  2. 以下のようなクラスMyClassを用意した。MyClassのvectorを用意し、vectorにMyClassを10個追加し、すべての要素のshow()関数を呼び出せ。xの値はランダムな数でよい。
    class MyClass{
    public:
        int x;
    
        MyClass(int _x) :
            x{ _x }  
        {
        }
    
        void show(){
            std::cout << "xは:" << x << std::endl;
        }
    };
    

演習問題(OpenSiv3D)

今回は、前回のプロジェクトからそのままEnemyクラスを書き換えてもいいし、1から作り直しても良い。

  1. 以下の様なEnemyクラスを用意した。Enemyクラスのインスタンスを作り、動作を確認せよ。
    #pragma once
    #include <Siv3D.hpp>
    
    class Enemy {
    public:
        Vec2 pos;
        Vec2 velocity;
        Enemy(Vec2 _pos);
        void update();
        void draw();
    };
    
    #include "Enemy.h"
    Enemy::Enemy(Vec2 _pos):
        pos{ _pos },
        velocity{ 0.0, 50.0 }
    {
    }
    
    void Enemy::update() {
        pos += Scene::DeltaTime() * velocity;
    }
    
    //エネミー(円)を描画
    void Enemy::draw() {
        Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
    }
    
  2. Enemyのvectorを作り、Enemyが複数出られるようにせよ。

  3. Zキーが押されたらランダムな座標に敵が出るようにせよ。 ヒント:以下のようにしてシーン内のランダムな座標を取得することができる。
    RandomVec2(Scene::Size().x, Scene::Size().y)
    

  4. Enemyを管理するEnemyManagerクラスを作る。

    現在、メインループ内でEnemyのvectorをfor文で回していると思うが、メインループ内はできるだけすっきりさせたい。そこで、Enemyのvectorをメンバに持ち、updateを呼ぶとメンバのEnemyのupdateをすべて呼ぶEnemyManagerクラスを作り、メインループ内ではEnemyManagerのupdateを呼ぶだけでEnemyすべてが更新され、EnemyManagerのdrawを呼ぶだけですべての敵が描画されるようにする。また、add関数を持ち、それが呼ばれると敵を追加する。

    EnemyManagerクラスを作り、敵を管理せよ。
    #pragma once
    #include <vector>
    #include <Siv3D.hpp>
    #include "Enemy.h"
    
    class EnemyManager {
    public:
        std::vector<Enemy> enemies;
        void update();
        void draw();
        void add(Vec2 pos);
    };
    
    #include "EnemyManager.h"
    
    void EnemyManager::update()
    {
        // ここを実装
    }
    
    void EnemyManager::draw()
    {
        // ここを実装
    }
    
    void EnemyManager::add(Vec2 pos)
    {
        // ここを実装
    }
    

Tips

OpenSiv3Dにはvectorと同じように使える動的配列、Arrayがある。本資料ではC++STLしか使えない極限の環境を想定して今後もvectorを使うが、Arrayはvectorより要素の削除が簡単に行えるので、Arrayも確認してみよう。

zenn.dev

C++OpenSiv3D入門講座

C++OpenSiv3D入門講座 Vol. 02 ファイル分け・コンポジション

C++OpenSiv3D入門講座

実際にゲームを作る時、ファイル分けをすることが必要になる。 今回はクラスの内容を.hと.cppに分ける。

ファイル分け

以下のように、.hと.cppにクラスの定義を分けて書くことが出来る。今後は基本的に.hと.cppにクラスの定義を分けて書くこと。(ただし、解答例ではファイル数が多くなるのを避けるためすべてMain.cppに書いている場合がある) .cppには、名前空間を指定するため、クラス名::関数名と書く必要がある。以下は、コンソールでのプレイヤー例。

#include <iostream>

class Player{
public: 
    int x, y;
    Player(int _x, int _y);
    void update();
    void draw();
    void showXY();
};
#include "Player.h"

Player::Player(int _x, int _y) {
    x = _x;
    y = _y;
}

void Player::update(){
    std::cout << "Playerのupdateが呼ばれました。" << std::endl;
}

void Player::draw(){
    std::cout << "Playerのdrawが呼ばれました。" << std::endl;
}

void Player::showXY(){
    std::cout << "PlayerのshowXYが呼ばれました。 x:" << x << " y:" << y << std::endl;
}
#include <iostream>
#include "Player.h"

int main(){

    Player player{ 20, 30 };
    player.update();
    player.draw();
    player.showXY();

    return 0;
}

Tips:定義への移動

複数のファイルに分けると関数やクラスの定義を見に行くのに時間がかかるが、おおよそのIDE(Visual StudioXcode)では、関数・クラスにカーソルを合わせて右クリックを押して出るダイアログ内の「定義に移動」等の項目を押すことで定義に移動できる。それぞれショートカットも用意されており、例えばVisual Studioであれば関数・クラスにカーソルを合わせてF12を押すことで定義に移動できる。他にも様々な機能やショートカットがあるので、調べてみよう。

コンストラクタの書き方 その2(メンバイニシャライザ)

コンストラクタの数値の初期化は以下のようにも書くことができる。自分で定義したクラスをなど初期化する場合、こう書いたほうが、インスタンスのコピーが発生しなくなるため処理が早くなる。以後はこの書き方、メンバイニシャライザを使って初期化を行う。

#include <iostream>

class Vector2{
public:
    int x;
    int y;

    Vector2(int _x, int _y):
        x{ _x },
        y{ _y }
    {
    }
};

int main(){

    Vector2 obj{ 1, 2 };
    std::cout << obj.x << " " << obj.y << std::endl;

    return 0;
}

コンポジション

クラスをクラスのメンバにすることが出来る。 以下の例では、Vector2クラスをまず定義し、PlayerクラスでVector2クラスのメンバを持っている。

#include <iostream>

class Vector2{
public:
    int x, y;
    Vector2(int _x, int _y) :
        x{ _x },
        y{ _y }
    {
    }
};

class Player{
public:
    Vector2 pos;
    int hp;

    Player(int _x, int _y, int _hp) :
        pos{ _x, _y },
        hp{ _hp }
    {
    }
};


int main(){
    Player player{ 100, 200, 64 };

    std::cout << "x:" << player.pos.x << ", y:" << player.pos.y << std::endl;
    std::cout << "hp:" << player.hp << std::endl;

    return 0;
}

コンポジションをしている場合、メンバイニシャライザでメンバ変数のコンストラクタを呼ぶ。 上記の例では、Vector2インスタンスのコンストラクタをメンバイニシャライザで呼んでいる。

クラスをコンポジションした場合、クラスに含まれているクラスのコンストラクタが先に呼ばれる。デストラクタは逆の順で呼ばれる。HogeやPiyoは日本では、「特に意味のない名前」を表す。

#include <iostream>

class Hoge{
public:
    int x;

    Hoge(int _x):
        x{ _x }
    {
        std::cout << "Hogeのコンストラクタが呼ばれました" << std::endl;
    }

    ~Hoge(){
        std::cout << "Hogeのデストラクタが呼ばれました" << std::endl;
    }
};

class Piyo{
public:
    Hoge Hoge_;
    int y;

    Piyo(int _x, int _y):
        Hoge_{ _x },
        y{ _y }
    {
        std::cout << "Piyoのコンストラクタが呼ばれました" << std::endl;
    }

    ~Piyo(){
        std::cout << "Piyoのデストラクタが呼ばれました" << std::endl;
    }
};


int main(){

    Piyo piyo{ 100, 200 };

    std::cout << piyo.Hoge_.x << " " << piyo.y << std::endl;

    return 0;
}

インクルード

#includeを使うと自分が定義したヘッダーをインクルードできる。インクルードすると、その部分にヘッダーの中身のコードが展開される。

pragma onceによるインクルードガード

Visual StudioXcodeなどのコンパイラでは、#pragma onceを使うと一度読み込まれたヘッダーは読み込まれないようになる。複数のファイルから読み込まれるヘッダーの一番上につけることで、多重定義を防ぐことが出来る。

以下の例では、PlayerクラスとEnemyクラスで共通で使うVector2クラスを作り、それをどちらのファイルからも読み込むコンソールプログラムの例である。よって、Vector2クラスの#pragma onceを外すと、コンパイルエラーが起きる。

#pragma once
#include <iostream>

class Vector2{
public:
    int x, y;
    Vector2(int _x, int _y) :
        x{ _x },
        y{ _y }
    {
    }
};
#include <iostream>
#include "Vector2.h"

class Enemy{
public:
    Vector2 pos;

    Enemy(int x, int y) :
        pos{ x, y }
    {
    }
};
#include <iostream>
#include "Vector2.h"

class Player{
public:
    Vector2 pos;

    Player(int x, int y) :
        pos{ x, y }
    {
    }
};
#include <iostream>
#include "Player.h"
#include "Enemy.h"

int main(){

    Player player{ 100, 200 };
    Enemy enemy{ 300, 400 };

    std::cout << player.pos.x << ", " << player.pos.y << std::endl;
    std::cout << enemy.pos.x << ", " << enemy.pos.y << std::endl;

    return 0;
}

実際にゲームを作るときは、ヘッダファイルにはとりあえず#pragma onceをつけよう。#pragma onceは特に害はない。

コンパイラによっては#pragma onceが対応していない場合がある。そのような場合にはこちらの方法でインクルードガードを作成しよう。

まとめ

  • クラスの定義は.hと.cppに分けて書く
  • IDEの「定義に移動」等の機能を活用しよう
  • クラスのメンバ変数の初期化はメンバイニシャライザで行う
  • コンポジションをしている場合、メンバイニシャライザでメンバ変数のコンストラクタを呼ぶ
  • ヘッダファイルにはとりあえず#pragma onceをつける

演習問題(OpenSiv3D)

  1. 前回の演習問題で作ったプログラムのPlayerクラス, Enemyクラスのコンストラクタの初期化を今回教えた形式、メンバイニシャライザで書き直せ。

  2. 前回Siv3Dで作ったプロジェクト上で、Player.h, Player.cpp, Enemy.h, Enemy.cppを作り、PlayerクラスとEnemyクラスをファイル分けせよ。

  3. OpenSiv3Dには2次元座標を表す、Vec2クラスがある。これを用いてPlayerクラス、Enemyクラスを書き換えよ。同時にdraw関数内の、Circleクラスの初期化もVec2クラスで行うよう書き換えよ。

ヒント

Vec2クラスを用いた円描写は以下のようになる。

Circle{ 座標(Vec2), 半径(double) }.draw(Color{ r, g, b }):

C++OpenSiv3D入門講座

C++OpenSiv3D入門講座 Vol. 11 演習問題の解答

C++OpenSiv3D入門講座

以下に演習問題の解答例を示す。

Vol. 01

演習問題(コンソール)

1問目

ソースコードを開く

#include <iostream>

int main() {
    int input;
    std::cout << "整数値を入力してください:" << std::endl;
    std::cin >> input;//inputに入力された数値を代入
    std::cout << "入力された値は、" << input << "です。" << std::endl;

    return 0;
}

2問目
ソースコードを開く

#include <iostream>

class Vector3 {
public:
    int x, y, z;

    // コンストラクタ
    Vector3(int _x, int _y, int _z) {
        x = _x;
        y = _y;
        z = _z;
    }

    // x, y, zの値を表示
    void show() {
        std::cout << x << "," << y << "," << z << std::endl;
    }

    // x * y * zの値を表示
    void showMultiple() {
        std::cout << x *y* z << std::endl;
    }
};

int main() {
    Vector3 vec3{ 1, 2, 3 };
    vec3.show();
    vec3.showMultiple();

    return 0;
}

演習問題(OpenSiv3D)

1~3問目

ソースコードを開く

# include <Siv3D.hpp>

class Player {
public:
    double x, y, speed;

    Player() {
        x = 320.0;
        y = 240.0;
        speed = 50.0;
    }
    void update() {
        //このフレームで進む距離の計算
        const double delta = Scene::DeltaTime() * speed;

        //上下左右キーで移動
        if (KeyLeft.pressed()) {
            x -= delta;
        }
        if (KeyRight.pressed()) {
            x += delta;
        }
        if (KeyUp.pressed()) {
            y -= delta;
        }
        if (KeyDown.pressed()) {
            y += delta;
        }
    }
    //自機(円)を描画
    void draw() {
        Circle{ x, y, 30.0 }.draw(Color{ 0, 0, 255 });
    }
};

class Enemy {
public:
    double x, y, speed;

    Enemy(double _x, double _y, double _speed) {
        x = _x;
        y = _y;
        speed = _speed;
    }

    void update() {
        //このフレームで進む距離の計算
        const double delta = Scene::DeltaTime() * speed;
        y += delta;
    }
    //敵(円)を描画
    void draw() {
        Circle{ x, y, 30.0 }.draw(Color{ 255, 0, 0 });
    }
};

void Main()
{
    //PlayerとEnemyをインスタンス化
    Player player;
    Enemy enemy{ 480, 0, 50 };

    while (System::Update())
    {
        //移動などの処理
        player.update();
        enemy.update();
        //描写
        player.draw();
        enemy.draw();
    }
}

Vol. 02

ソースコードを開く

#pragma once
# include <Siv3D.hpp> //Vec2型を使うためインクルード

class Player {
public:
    Vec2 pos;
    double speed;
    Player(); 
    void update();
    void draw();
};
#include "Player.h"
//Player.hで<Siv3D.hpp>をインクルードしているので# include <Siv3D.hpp>は必要なし

Player::Player() :
    pos{ 320.0, 240.0 },
    speed{ 50.0 }
{
}

void Player::update() {
    //このフレームで進む距離の計算
    const double delta = Scene::DeltaTime() * speed;

    //上下左右キーで移動
    if (KeyLeft.pressed()) {
        pos.x -= delta;
    }
    if (KeyRight.pressed()) {
        pos.x += delta;
    }
    if (KeyUp.pressed()) {
        pos.y -= delta;
    }
    if (KeyDown.pressed()) {
        pos.y += delta;
    }
}

void Player::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 0, 0, 255 });
}
#pragma once
# include <Siv3D.hpp> //Vec2型を使うためインクルード

class Enemy {
public:
    Vec2 pos;
    double speed;
    Enemy(double _x, double _y, double _speed);
    void update();
    void draw();
};
#include "Enemy.h"
//Enemy.hで<Siv3D.hpp>をインクルードしているので# include <Siv3D.hpp>は必要なし

Enemy::Enemy(double _x, double _y, double _speed) :
    pos{ _x, _y },
    speed{ _speed }
{
}

void Enemy::update() {
    //このフレームで進む距離の計算
    const double delta = Scene::DeltaTime() * speed;
    pos.y += delta;
}

void Enemy::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
}
# include <Siv3D.hpp>
# include "Player.h"
# include "Enemy.h"

void Main()
{
    //PlayerとEnemyをインスタンス化
    Player player;
    Enemy enemy{ 480, 0, 50 };

    while (System::Update())
    {
        //移動などの処理
        player.update();
        enemy.update();
        //描写
        player.draw();
        enemy.draw();
    }
}

Vol. 03

演習問題(コンソール)

1問目

ソースコードを開く

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec;//int型の動的配列
    // 0~9のランダムな10個の数を追加
    for (int i = 0; i < 10; i++) {
        vec.emplace_back(rand() % 10);
    }
    //出力
    for (int i = 0; i < vec.size(); i++) {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}

2問目

ソースコードを開く

#include <iostream>
#include <vector>

class MyClass {
public:
    int x;
    MyClass(int _x) :
        x{ _x }
    {
    }
    void show() {
        std::cout << "xは:" << x << std::endl;
    }
};

int main() {
    std::vector<MyClass> vec;//MyClass型の動的配列
    // 10個のMyClassインスタンスを作成し、vecに追加
    for (int i = 0; i < 10; i++) {
        vec.emplace_back(MyClass{ rand() % 10 });
    }
    //出力
    for (int i = 0; i < vec.size(); i++) {
        vec[i].show();
    }
    return 0;
}

 演習問題(OpenSiv3D)

1~3問目

ソースコードを開く

#pragma once
#include <Siv3D.hpp>
class Enemy {
public:
    Vec2 pos;
    Vec2 velocity;
    Enemy(Vec2 _pos);
    void update();
    void draw();
};
#include "Enemy.h"
Enemy::Enemy(Vec2 _pos) :
    pos{ _pos },
    velocity{ 0.0, 50.0 }
{
}
void Enemy::update() {
    pos += Scene::DeltaTime() * velocity;
}
//エネミー(円)を描画
void Enemy::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
}
# include <Siv3D.hpp>
# include <vector>
# include "Enemy.h"

void Main()
{
    std::vector<Enemy> enemies; //Enemyの動的配列
    enemies.emplace_back(Enemy{ Vec2{ 480, 0 } }); //Enemyの実体を作成してenemiesに追加


    while (System::Update())
    {
        //Zキーが押されたら敵を追加
        if (KeyZ.down()) {
            enemies.emplace_back(Enemy{ RandomVec2(Scene::Size().x, Scene::Size().y) }); //ランダムな座標に敵を生成して追加
        }
        //移動などの処理
        for (int i = 0; i < enemies.size(); i++) {
            enemies[i].update();
        }
        //描写
        for (int i = 0; i < enemies.size(); i++) {
            enemies[i].draw();
        }
    }
}

4問目

ソースコードを開く

#pragma once
#include <Siv3D.hpp>
class Enemy {
public:
    Vec2 pos;
    Vec2 velocity;
    Enemy(Vec2 _pos);
    void update();
    void draw();
};
#include "Enemy.h"
Enemy::Enemy(Vec2 _pos) :
    pos{ _pos },
    velocity{ 0.0, 50.0 }
{
}
void Enemy::update() {
    pos += Scene::DeltaTime() * velocity;
}
//エネミー(円)を描画
void Enemy::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
}
#pragma once
#include <vector>
#include <Siv3D.hpp>
#include "Enemy.h"
class EnemyManager {
public:
    std::vector<Enemy> enemies;
    void update();
    void draw();
    void add(Vec2 pos);
};
#include "EnemyManager.h"
void EnemyManager::update()
{
    //Zキーが押されたら敵を追加
    if (KeyZ.down()) {
        add(RandomVec2(Scene::Size().x, Scene::Size().y)); //ランダムな座標に敵を追加
    }

    //メンバのEnemyのupdateを呼ぶ
    for (int i = 0; i < enemies.size(); i++) {
        enemies[i].update();
    }
}
void EnemyManager::draw()
{
    //メンバのEnemyのdrawを呼ぶ
    for (int i = 0; i < enemies.size(); i++) {
        enemies[i].draw();
    }
}
void EnemyManager::add(Vec2 pos)
{
    enemies.emplace_back(Enemy{ pos });
}
# include <Siv3D.hpp>
# include <vector>
# include "EnemyManager.h"

void Main()
{
    EnemyManager enemyManager;
    enemyManager.add(Vec2{ 480, 0 }); //敵を一体追加

    while (System::Update())
    {
        //移動などの処理
        enemyManager.update();
        //描写
        enemyManager.draw();
        
    }
}

Vol. 4

演習問題(コンソール)

ソースコードを開く

#include <iostream>

void twiceRef(int& x) {
    x *= 2;
}


int main() {

    int num = 3;

    std::cout << num << std::endl;

    twiceRef(num);

    std::cout << num << std::endl;

    return 0;
}

演習問題(OpenSiv3D)

ソースコードを開く

#pragma once
#include <Siv3D.hpp>

class Player {
public:
    const double Speed;
    Vec2 pos;
    Player();
    void update();
    void draw();
};
#include "Player.h"

Player::Player() :
    pos{ 320.0, 240.0 },
    Speed{ 50.0 }
{
}

void Player::update() {
    //このフレームで進む距離の計算
    const double delta = Scene::DeltaTime() * Speed;

    //上下左右キーで移動
    if (KeyLeft.pressed()) {
        pos.x -= delta;
    }
    if (KeyRight.pressed()) {
        pos.x += delta;
    }
    if (KeyUp.pressed()) {
        pos.y -= delta;
    }
    if (KeyDown.pressed()) {
        pos.y += delta;
    }
}

void Player::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 0, 0, 255 });
}
#pragma once
#include <Siv3D.hpp>
#include "Player.h"
class Enemy {
public:
    Vec2 pos;
    Vec2 velocity;
    Player* pPlayer;
    Enemy(const Vec2& _pos, Player* ptr);
    void update();
    void draw();
};
# include "Enemy.h"

Enemy::Enemy(const Vec2& _pos, Player* ptr) :
    pos{ _pos },
    velocity{ 0.0, 0.0 },
    pPlayer{ ptr }
{
}

void Enemy::update() {
    //Playerの位置と比較してvelocityを決定(x方向)
    if (pPlayer->pos.x > pos.x) {
        velocity.x = 50.0;
    }
    else if (pPlayer->pos.x < pos.x) {
        velocity.x = -50.0;
    }
    else {
        velocity.x = 0;
    }

    //Playerの位置と比較してvelocityを決定(y方向)
    if (pPlayer->pos.y > pos.y) {
        velocity.y = 50.0;
    }
    else if (pPlayer->pos.y < pos.y) {
        velocity.y = -50.0;
    }
    else {
        velocity.y = 0;
    }

    //移動
    pos += Scene::DeltaTime() * velocity;
}

void Enemy::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
}
#include <Siv3D.hpp>
#include "Player.h"
#include "Enemy.h"

void Main()
{
    Player player; //Playerをインスタンス化
    Enemy enemy{ Vec2{ 480, 0 }, &player }; //Enemyをインスタンス化、playerのポインタを渡す

    //メインループ
    while (System::Update())
    {
        //移動等の処理
        player.update();
        enemy.update();
        //描写
        player.draw();
        enemy.draw();
    }
}

Vol. 5

1問目

ソースコードを開く

#include <iostream>

int mySquare(int x) {
    std::cout << "int型のmySquareが呼ばれました" << std::endl;
    return x * x;
}

float mySquare(float x) {
    std::cout << "float型のmySquareが呼ばれました" << std::endl;
    return x * x;
}

double mySquare(double x) {
    std::cout << "double型のmySquareが呼ばれました" << std::endl;
    return x * x;
}


int main() {
    int x = 1;
    float y = 0.5;
    double z = 2.1;
    std::cout << mySquare(x) << std::endl;
    std::cout << mySquare(y) << std::endl;
    std::cout << mySquare(z) << std::endl;
}

2問目

ソースコードを開く

#include <iostream>

class Vector2 {
public:
    int x, y;

    Vector2() :
        x{ 0 },
        y{ 0 }
    {
        std::cout << "引数なしのコンストラクタが呼ばれました" << std::endl;
    }

    Vector2(int _x, int _y) :
        x{ _x },
        y{ _y }
    {
        std::cout << "引数ありのコンストラクタが呼ばれました" << std::endl;
    }

    ~Vector2() {
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};

int main() {
    std::cout << "メイン関数に入りました" << std::endl;
    Vector2 point1;
    Vector2 point2{ 2, 3 };
    std::cout << "メイン関数を抜けました" << std::endl;
}

Vol. 6

演習問題(コンソール)

ソースコードを開く

#include <iostream>
#include <vector>

class MyClass {
public:
    int a;
    int b;

    MyClass(int _a, int _b) :
        a{ _a },
        b{ _b }
    {
    }
};

int main() {
    std::vector<MyClass> vec;

    for (int i = 0; i < 10; i++) {
        vec.emplace_back(MyClass{ rand() % 10, rand() % 10 });
    }

    for (auto iter = vec.begin(); iter < vec.end(); iter++) {
        std::cout << "(" << iter->a << ", " << iter->b << "), ";
    }

    std::cout << std::endl;

    auto iter = vec.begin();
    while (iter < vec.end()) {
        if (iter->a < iter->b) {
            iter = vec.erase(iter);
        }
        else iter++;
    }

    for (auto iter = vec.begin(); iter < vec.end(); iter++) {
        std::cout << "(" << iter->a << ", " << iter->b << "), ";
    }

    std::cout << std::endl;
}

演習問題(OpenSiv3D)

ソースコードを開く

#pragma once
#include <Siv3D.hpp>
class Enemy {
public:
    Vec2 pos;
    Vec2 velocity;
    Enemy(const Vec2& _pos);
    void update();
    void draw();
};
# include "Enemy.h"
Enemy::Enemy(const Vec2& _pos) :
    pos{ _pos },
    velocity{ RandomVec2(50.0) }
{
    // RandomVec2(double length)
    // 半径length(今回は5.0)の2次元ベクトルを返す
}
void Enemy::update() {
    pos += Scene::DeltaTime() * velocity;
}
void Enemy::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
}
# include <Siv3D.hpp>
# include <vector>
# include "Enemy.h"

void Main()
{
    std::vector<Enemy> enemies; //Enemyの動的配列
    //5つの要素を追加する
    for (int i = 0; i < 5; i++) {
        //ランダムな位置に敵を生成
        enemies.emplace_back(Enemy{ RandomVec2(Scene::Size().x, Scene::Size().y) });
    }

    while (System::Update())
    {
        //移動などの処理
        auto iter = enemies.begin();
        while (iter != enemies.end()) {
            iter->update();
            //問題3のシーン外に出たかどうかの条件文
            if ((iter->pos.x < 0) || (iter->pos.x > Scene::Size().x) || (iter->pos.y < 0) || (iter->pos.y > Scene::Size().y)){
            //if (iter->pos.y > 480) {  //問題2ならこちらの条件文
                iter = enemies.erase(iter);
            }
            else {
                iter++;
            }
        }
        //描写
        for (auto iter = enemies.begin(); iter != enemies.end(); ++iter) {
            iter->draw();
        }
    }
}

Vol. 7

演習問題(コンソール)

1問目

ソースコードを開く

#include <iostream>

int main() {

    auto twice = [](int x) {
        return x * 2;
    };

    int num = 3;

    std::cout << twice(num) << std::endl;

    return 0;
}

2問目

ソースコードを開く

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec;

    // vectorに0から9の数を入れる
    for (int i = 0; i < 10; i++) {
        vec.emplace_back(i);
    }

    //表示
    for (auto i = vec.begin(); i < vec.end(); ++i) {
        std::cout << *i << " ";
    }
    std::cout << std::endl;

    // 2の倍数を後ろに詰め、削除
    auto rmvIter = std::remove_if(vec.begin(), vec.end(), 
        [](int i) {return i % 2 == 0; }
    );
    vec.erase(rmvIter, vec.end());

    //表示
    for (auto i = vec.begin(); i < vec.end(); ++i) {
        std::cout << *i << " ";
    }
    std::cout << std::endl;

    return 0;
}

演習問題(OpenSiv3D)

ソースコードを開く

#pragma once
#include <Siv3D.hpp>

class Enemy {
public:
    Vec2 pos;
    Vec2 velocity;
    Enemy(const Vec2& _pos);
    void update();
    void draw();
};
# include "Enemy.h"

Enemy::Enemy(const Vec2& _pos) :
    pos{ _pos },
    velocity{ RandomVec2(50.0) }
{
    // RandomVec2(double length)
    // 半径length(今回は50.0)の2次元ベクトルを返す
}

void Enemy::update() {
    pos += Scene::DeltaTime() * velocity;
}

void Enemy::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
}
# include <Siv3D.hpp>
# include <vector>
# include <algorithm>
# include "Enemy.h"

void Main()
{
    std::vector<Enemy> enemies; //Enemyの動的配列
    //5つの要素を追加する
    for (int i = 0; i < 5; i++) {
        //ランダムな位置に敵を生成
        enemies.emplace_back(Enemy{ RandomVec2(Scene::Size().x, Scene::Size().y) });
    }

    while (System::Update())
    {
        //移動などの処理
        for (auto iter = enemies.begin(); iter != enemies.end(); ++iter) {
            iter->update();
        }
        //画面外に出た敵を削除
        auto rmvIter = std::remove_if(enemies.begin(), enemies.end(),
            [](const Enemy &e) {
                return (e.pos.x < 0) || (e.pos.x > Scene::Size().x) || (e.pos.y < 0) || (e.pos.y > Scene::Size().y);
            }
        );
        enemies.erase(rmvIter, enemies.end());
        //描写
        for (auto iter = enemies.begin(); iter != enemies.end(); ++iter) {
            iter->draw();
        }
    }
}

Vol. 8

ソースコードを開く

#pragma once
# include<Siv3D.hpp>

class Player
{
public:
    Vec2 pos;
    double speed;
    Player(const Vec2& _pos, double _speed);
    void update();
    void draw();
};
#include "Player.h"

Player::Player(const Vec2& _pos, double _speed) :
    pos{ _pos },
    speed{ _speed }
{
}

void Player::update() {
    //このフレームで進む距離の計算
    const double delta = Scene::DeltaTime() * speed;
    //上下左右キーで移動
    if (KeyLeft.pressed()) {
        pos.x -= delta;
    }
    if (KeyRight.pressed()) {
        pos.x += delta;
    }
    if (KeyUp.pressed()) {
        pos.y -= delta;
    }
    if (KeyDown.pressed()) {
        pos.y += delta;
    }
}

void Player::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 0, 0, 255 });
}
#pragma once
#include <Siv3D.hpp>

class Enemy {
public:
    enum Kind {
        Straight,
        Sin,
        Round
    };
    Kind kind; //種類
    Vec2 pos; //座標
    Color color; //色
    double t; //円軌道のための変数
    Enemy(Kind _kind, const Vec2& _pos);
    void update();
    void draw();
};
# include <cmath>
# include "Enemy.h"
# include "GameManager.h"

Enemy::Enemy(Enemy::Kind _kind, const Vec2& _pos) :
    kind{ _kind },
    pos{ _pos },
    t{ 0 }
{
    //kindによって色を変更
    switch (kind)
    {
    case Enemy::Kind::Straight:
        color = Color{ 255, 0, 0 };
        break;
    case Enemy::Kind::Sin:
        color = Color{ 255, 165, 0 };
        break;
    case Enemy::Kind::Round:
        color = Color{ 246, 152, 150 };
        break;
    default:
        color = Color{ 255, 0, 0 };
        break;
    }
}

void Enemy::update() {
    //kindによって動きを変更
    switch (kind) {
    case Enemy::Kind::Straight:
        //自機に向かって直進
        if (gameManager.player.pos.x < pos.x) {
            pos.x -= Scene::DeltaTime() * 50.0;
        }
        else if (gameManager.player.pos.x > pos.x) {
            pos.x += Scene::DeltaTime() * 50.0;
        }
        if (gameManager.player.pos.y < pos.y) {
            pos.y -= Scene::DeltaTime() * 50.0;
        }
        else if (gameManager.player.pos.y > pos.y) {
            pos.y += Scene::DeltaTime() * 50.0;
        }
        break;
    case Enemy::Kind::Sin:
        //sin軌道
        pos.x += Scene::DeltaTime() * 60.0;
        pos.y += Scene::DeltaTime() * 500.0 * std::cos(pos.x / 10.0);
        break;
    case Enemy::Kind::Round:
        //円軌道
        t += Scene::DeltaTime() * 2.0;
        pos.x -= Scene::DeltaTime() * 100.0 * std::sin(t);
        pos.y += Scene::DeltaTime() * 100.0 * std::cos(t);
        break;
    default:
        break;
    }
}

void Enemy::draw() {
    Circle{ pos, 30.0 }.draw(color);
}
#pragma once
# include<vector>
# include"Enemy.h"

class EnemyManager
{
public:
    std::vector<Enemy> enemies;
    EnemyManager(int num);
    void update();
    void draw();
};
#include "EnemyManager.h"


EnemyManager::EnemyManager(int num)
{
    //numで指定された数要素を追加
    for (int i = 0; i < num; i++) {
        //ランダムの種類とランダムの座標で敵を生成
        enemies.emplace_back(Enemy{ (Enemy::Kind)(rand() % 3) , RandomVec2(800, 600) });
    }
}

void EnemyManager::update() {
    //Enemyそれぞれのupdateを呼ぶ
    for (auto iter = enemies.begin(); iter != enemies.end(); ++iter) {
        iter->update();
    }
    //画面外に出た敵を削除
    auto rmvIter = std::remove_if(enemies.begin(), enemies.end(),
        [](const Enemy& e) {
        return (e.pos.x < 0) || (e.pos.x > Scene::Size().x) || (e.pos.y < 0) || (e.pos.y > Scene::Size().y);
        }
    );
    enemies.erase(rmvIter, enemies.end());
}

void EnemyManager::draw() {
    //Enemyそれぞれのdrawを呼ぶ
    for (auto iter = enemies.begin(); iter != enemies.end(); ++iter) {
        iter->draw();
    }
}
#pragma once
# include <vector>
# include "Player.h"
# include "EnemyManager.h"

class GameManager
{
public:
    Player player;
    EnemyManager enemyManager;
    GameManager();
    void update();
    void draw();
};


extern GameManager gameManager;
#include "GameManager.h"

GameManager::GameManager() :
    player{ Vec2{ 400.0, 300.0 }, 50.0 },
    enemyManager{ 10 }
{

}

void GameManager::update() {
    //それぞれのupdateを呼ぶ
    player.update();
    enemyManager.update();
}

void GameManager::draw() {
    //それぞれのdrawを呼ぶ
    player.draw();
    enemyManager.draw();
}

GameManager gameManager;
# include <Siv3D.hpp>
# include "GameManager.h"

void Main()
{
    while (System::Update())
    {
        gameManager.update();
        gameManager.draw();
    }
}

Vol. 9

演習問題(コンソール)

ソースコードを開く

#include <iostream>
#include <vector>

class MyClass {
public:
    int a;

    MyClass(int _a) :
        a{ _a }
    {
    }
};

int main() {
    std::vector<MyClass*> vec;

    // 適当な数でMyClassを追加
    for (int i = 0; i < 10; i++) {
        vec.emplace_back(new MyClass{ rand() % 10 });
    }

    // vectorの中身を表示
    for (const auto& i : vec) {
        std::cout << i->a << " ";
    }
    std::cout << std::endl;

    // 5以下の要素を削除
    auto it = vec.begin();
    while (it != vec.end()) {
        if ((*it)->a <= 5) {
            delete* it;
            it = vec.erase(it);
        }
        else {
            it++;
        }
    }

    // vectorの中身を表示
    for (const auto& i : vec) {
        std::cout << i->a << " ";
    }
    std::cout << std::endl;

    return 0;
}

演習問題(OpenSiv3D)

ソースコードを開く

#pragma once
#include <Siv3D.hpp>
class Enemy {
public:
    static const double Radius;
    Vec2 pos;
    Vec2 velocity;
    Enemy(const Vec2& _pos);
    void update();
    void draw();
};
# include "Enemy.h"
const double Enemy::Radius = 30.0;
Enemy::Enemy(const Vec2& _pos) :
    pos{ _pos },
    velocity{ RandomVec2(50.0) }
{
}
void Enemy::update() {
    pos += Scene::DeltaTime() * velocity;
}
void Enemy::draw() {
    Circle{ pos, Radius }.draw(Color{ 255, 0, 0 });
}
# include <Siv3D.hpp>
# include <vector>
# include "Enemy.h"

void Main()
{
    std::vector<Enemy*> enemies; //Enemy型ポインターの動的配列
    //5つの要素を追加する
    for (int i = 0; i < 5; i++) {
        //ランダムな位置に敵を生成
        enemies.emplace_back(new Enemy{ RandomVec2(Scene::Size().x, Scene::Size().y) });
    }

    while (System::Update())
    {
        //移動などの処理
        auto iter = enemies.begin();
        while (iter != enemies.end()) {
            (*iter)->update();
            //シーン外に出たかどうかの条件文
            if (((*iter)->pos.x < 0) || ((*iter)->pos.x > Scene::Size().x) || ((*iter)->pos.y < 0) || ((*iter)->pos.y > Scene::Size().y)){
                delete* iter;
                iter = enemies.erase(iter);
            }
            else {
                iter++;
            }
        }
        //描写
        for (const auto& e : enemies) {
            e->draw();
        }

    }

    //各要素をdelete
    for (const auto& e : enemies) {
        delete e;
    }
}

Vol. 10

演習問題(コンソール)

ソースコードを開く

# include<iostream>
# include<vector>

class IAnimal {
public:
    double weight;//重さ
    IAnimal(double w) :
        weight{ w }
    {
    }
    virtual void talk() = 0;
    virtual ~IAnimal() = default;
};
class Dog : public IAnimal {
public:
    Dog(double w) :
        IAnimal{ w }
    {
    }
    void talk() override {
        std::cout << "わんわん 重さは:" << weight << std::endl;
    }
};
class Cat : public IAnimal {
public:
    Cat(double w) :
        IAnimal{ w }
    {
    }
    void talk() override {
        std::cout << "ネコチャン 重さは:" << weight << std::endl;
    }
};
class Dialga : public IAnimal {
public:
    Dialga(double w) :
        IAnimal{ w }
    {
    }
    void talk() override {
        std::cout << "グギュグバァッ!!! 重さは:" << weight << std::endl;
    }
};
int main() {
    std::vector<IAnimal*> animals; //IAnimalのポインタの動的配列

    //それぞれのクラスのインスタンスを生成
    animals.emplace_back(new Dog(12.0));
    animals.emplace_back(new Cat(4.5));
    animals.emplace_back(new Dialga(683.0));

    //一つのループでそれぞれの型のtalk()を呼び出し
    for (auto&& a : animals) {
        a->talk();
    }
}

演習問題(OpenSiv3D)

ソースコードを開く ソースが多いため、vol. 8との変更・追加されたファイルのみ記載する。なお、Enemy.hとEnemy.cppは削除されている。

#pragma once
#include <Siv3D.hpp>

class IEnemy
{
public:
    Vec2 pos; //座標
    IEnemy(const Vec2& _pos) : //コンストラクタ
        pos{ _pos }
    {
    }
    virtual void update() = 0; //純粋仮想関数
    virtual void draw() = 0; //純粋仮想関数
    virtual ~IEnemy() = default; // 仮想デストラクタ
};
#pragma once
#include "IEnemy.h"

class StraightEnemy :
    public IEnemy
{
public:
    StraightEnemy(const Vec2& _pos);
    void update() override;
    void draw() override;
};
#include "StraightEnemy.h"
#include "GameManager.h"

StraightEnemy::StraightEnemy(const Vec2& _pos) :
    IEnemy{ _pos }
{
}

void StraightEnemy::update() {
    //自機に向かって直進
    if (gameManager.player.pos.x < pos.x) {
        pos.x -= Scene::DeltaTime() * 50.0;
    }
    else if (gameManager.player.pos.x > pos.x) {
        pos.x += Scene::DeltaTime() * 50.0;
    }
    if (gameManager.player.pos.y < pos.y) {
        pos.y -= Scene::DeltaTime() * 50.0;
    }
    else if (gameManager.player.pos.y > pos.y) {
        pos.y += Scene::DeltaTime() * 50.0;
    }
}

void StraightEnemy::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
}
#pragma once
#include "IEnemy.h"
class SinEnemy :
    public IEnemy
{
public:
    SinEnemy(const Vec2& _pos);
    void update() override;
    void draw() override;
};
#include "SinEnemy.h"

SinEnemy::SinEnemy(const Vec2& _pos) :
    IEnemy{ _pos }
{
}

void SinEnemy::update() {
    //sin軌道
    pos.x += Scene::DeltaTime() * 60.0;
    pos.y += Scene::DeltaTime() * 500.0 * std::cos(pos.x / 10.0);
}

void SinEnemy::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 255, 165, 0 });
}
#pragma once
#include "IEnemy.h"
class RoundEnemy :
    public IEnemy
{
public:
    double t;
    RoundEnemy(const Vec2& _pos);
    void update() override;
    void draw() override;
};
#include "RoundEnemy.h"

RoundEnemy::RoundEnemy(const Vec2& _pos) :
    IEnemy{ _pos },
    t{ 0 }
{
}

void RoundEnemy::update() {
    //円軌道
    t += Scene::DeltaTime() * 2.0;
    pos.x -= Scene::DeltaTime() * 100.0 * std::sin(t);
    pos.y += Scene::DeltaTime() * 100.0 * std::cos(t);
}

void RoundEnemy::draw() {
    Circle{ pos, 30.0 }.draw(Color{ 246, 152, 150 });
}
#pragma once
# include<vector>
# include"IEnemy.h"

class EnemyManager
{
public:
    std::vector<IEnemy*> enemies;
    EnemyManager(int num);
    void update();
    void draw();
};
#include "EnemyManager.h"
#include "StraightEnemy.h"
#include "SinEnemy.h"
#include "RoundEnemy.h"

EnemyManager::EnemyManager(int num)
{
    //numで指定された数要素を追加
    for (int i = 0; i < num; i++) {
        //ランダムの種類とランダムの座標で敵を生成
        switch (rand() % 3) {
        case 0:
            enemies.emplace_back(new StraightEnemy{ RandomVec2(800, 600) });
            break;
        case 1:
            enemies.emplace_back(new SinEnemy{ RandomVec2(800, 600) });
            break;
        default:
            enemies.emplace_back(new RoundEnemy{ RandomVec2(800, 600) });
            break;
        
        }
    }
}

void EnemyManager::update() {
    //Enemyそれぞれのupdateを呼ぶ
    for (auto&& e : enemies) {
        e->update();
    }
    //画面外に出た敵を削除
    auto iter = enemies.begin();
    while (iter != enemies.end()) {
        //シーン外に出たかどうかの条件文
        if (((*iter)->pos.x < 0) || ((*iter)->pos.x > Scene::Size().x) || ((*iter)->pos.y < 0) || ((*iter)->pos.y > Scene::Size().y)) {
            delete* iter;
            iter = enemies.erase(iter);
        }
        else {
            iter++;
        }
    };
}

void EnemyManager::draw() {
    //Enemyそれぞれのdrawを呼ぶ
    for (auto&& e : enemies) {
        e->draw();
    }
}

C++OpenSiv3D入門講座

C++OpenSiv3D入門講座 Vol. 01 クラスの基本

C++OpenSiv3D入門講座

今回はC++でのHello World、クラスの基本的な使い方について学ぶ。 演習では、実際にSiv3Dを用いてPlayer,Enemyを生成し、動かす。

Hello World!

C++流のHello World! C++ではstd::coutが用意されており、それを使って出力ができる。 std:: とは、「stdという名前空間にある」という意味。

#include <iostream>

int main(){
    std::cout << "Hello World!!" << std::endl;
    return 0;
}

出力

Cのprintf関数では%dや%sなどを使って型を指定する必要があったが、std::coutでは型を指定しなくても自動的に型を判定・表示してくれる。

#include <iostream>

int main(){

    int a = 12;
    double b = 3.14;
    char str[100] = "abcde";

    std::cout << a << " " << b << " " << str << std::endl;

    return 0;
}

入力

std::cinを使うと入力されたデータを変数に入れられる。

#include <iostream>

int main(){

    int input;
    std::cout << "年齢を入力してください" << std::endl;
    std::cin >> input;//inputに入力された数値を代入

    std::cout << "あなたは"<< input << "歳なんですね。" << std::endl;    

    return 0;
}

クラスの基本的な書き方

クラスは以下のように定義でき、インスタンス(実体)化して使う。 クラスの要素には.(コンマ)を使ってアクセスできる。 クラスは変数を要素に持つことができ、メンバ変数という。

#include <iostream>

class MyClass{
public:
    int a;
    int b;
};

int main(){

    MyClass obj; // インスタンス化
    obj.a = 10;
    obj.b = 20;
    std::cout << obj.a << "," << obj.b << std::endl;

    return 0;
}

二次元上の座標や、2次元ベクトルを表すクラス、Vector2を作った。 クラスの要素には.(コンマ)を使ってアクセスできる。 クラスは関数を要素に持つことができ、メンバ関数という。(下の例ではsetData関数がメンバ関数

#include <iostream>

class Vector2{
public:
    int x;
    int y;
    void setData(int _x, int _y){
        x = _x;
        y = _y;
    }
};

int main(){

    Vector2 obj;
    obj.setData(13, 29);
    std::cout << obj.x << " " << obj.y << std::endl;

    return 0;
}

コンストラク

さっきの例ではVector2クラスを作った後、setData関数をいちいち呼ばなければならない。 初期化をいちいち行うのは手間がかかるので、C++ではクラス生成時に呼び出されるコンストラクタというものを定義できる。

コンストラクタの書き方

クラスの名前(引数){ 初期化処理... }

Vector2クラスのコンストラクタ例

#include <iostream>

class Vector2{
public:
    int x;
    int y;

    Vector2(int _x, int _y){
        x = _x;
        y = _y;
    }
};

int main(){

    Vector2 obj{ 1, 2 }; //インスタンス化、コンストラクタの引数を{}内に指定
    std::cout << obj.x << " " << obj.y << std::endl;

    return 0;
}

デストラク

オブジェクトが破棄されるときにデストラクタが呼ばれる。

デストラクタの書き方

~クラスの名前(引数){ 初期化処理... }

デストラクタの例

#include <iostream>

class MyClass{
public:
    ~MyClass(){
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};

int main(){
    std::cout << "メイン関数に入りました" << std::endl;

    MyClass obj;

    std::cout << "メイン関数を抜けました" << std::endl;

    return 0;

}

メイン関数を抜ける時オブジェクトが破棄されるのでデストラクタが呼ばれる。

アクセス修飾子

publicやprivateをアクセス修飾子と言う。 publicの要素はクラス外からアクセスでき(公開)、 privateの要素はクラス外からアクセスできない(非公開)。 何も指定されていなければprivateメンバになる。 privateメンバ変数を用いることで、外から直接書き換えられたくないメンバ変数を書き換えることが出来ないようにできる。 privateは隠蔽、カプセル化などにおいて重要な機能だが、この講座では割愛する(基本的に全てpublicを使い、privateはあまり使用しない)。

#include <iostream>

class MyClass
{
    int a;
public:
    int b;
private:
    int c;
};

int main(){

    MyClass obj;

    //コメントを外すとコンパイルエラー

    //obj.a = 100;
    //obj.c = 100;

    obj.b = 100;

    return 0;
}

以下のようにすると、クラスの外から値を参照できるが、書き換えることは出来ないようにできる。

#include <iostream>

class MyClass
{
private:
    int data;
public:
    MyClass(){
        data = 3;
    }
    int getData(){
        return data;
    }
};

int main(){

    MyClass obj;

    // コメントを外すとコンパイルエラー
    // obj.data = 100;

    std::cout << obj.getData() << std::endl;

    return 0;
}

const変数

constをつけると値を変更不可能な変数を作ることができる。#defineの代わりに使って、定数を設定する時などに使える。

#include <iostream>

int main(){

    const int a = 100;

    //コメントを外すとコンパイルエラー
    //a = 200;

    std::cout << "aの値は:" << a << std::endl;

    return 0;
}

まとめ

  • std::coutで出力、std::cinで入力ができる。
  • クラスの定義とインスタンス化、メンバ変数・メンバ関数へのアクセスの方法を学んだ。
  • クラス生成時にはコンストラクタが呼び出され、オブジェクトが破棄されるときにデストラクタが呼び出される。
  • publicの要素はクラス外からアクセスでき、privateの要素はクラス外からアクセスできない。
  • constをつけると値を変更不可能な変数を作ることができる。

演習問題(コンソール)

  1. 以下のプログラムをstd::coutとstd::cinを使って書き直せ。
  2. #include <iostream>
    
    int main() {
        int input; //入力用
    
        printf("整数値を入力してください:");
        scanf("%d", &input);
    
        printf("入力された値は、%dです。\n", input);
    
        return 0;
    }
    
  3. 以下の様なクラスVector3を作った。
    • 適当なコンストラクタを作れ。
    • x,y,zの値を表示するメンバ関数showを作り、動作を確認せよ。
    • x*y*zの値を表示するメンバ関数showMultipleを作り、動作を確認せよ。
    class Vector3{
    public:
        int x, y, z;
    };
    

演習問題(OpenSiv3D)

  1. 以下の様なPlayerクラスを作り、矢印キーで動くようにせよ。
    class Player{
    public:
        double x, y, speed;
    
        Player(){
            x = 320.0;
            y = 240.0;
            speed = 50.0;
        }
        void update(){
            //ここを実装
            //矢印キーで移動
        }
        //自機(円)を描画
        void draw(){
            Circle{ x, y, 30.0 }.draw(Color{ 0, 0, 255 });
        }
    };
    
  2. Playerクラスの実体を作り、メインループ内でupdate関数とdraw関数を呼び出してPlayerクラスを動作させよ。
  3. 以下の様なEnemyクラスを作り、Playerクラスと同様に実体を作り動作を確認せよ。 今回作るEnemyはただ下に移動するだけでよい。
  4. class Enemy{
    public:
        double x, y, speed;
    
        //ここに座標と速さを指定できるようなコンストラクタを実装  
    
        void update(){
            //ここを実装
            //下方向に移動
        }
        //敵(円)を描画
        void draw(){
            Circle{ x, y, 30.0 }.draw(Color{ 255, 0, 0 });
        }
    };
    

ヒント

OpenSiv3Dでの処理は基本以下のような形になる

# include <Siv3D.hpp>

void Main()
{
    //毎フレーム使う変数の作成やゲーム開始前の処理

    while (System::Update())
    {
        // ここに書いた内容が毎フレーム実行される
    }
}
  1. while内でプレイヤーのインスタンスを作成すると、毎フレーム破棄されてしまう。

C++OpenSiv3D入門講座

C++OpenSiv3D入門講座

はじめに

本資料は、C++初学者向け資料です。

C++とOpenSiv3Dで実際にシューティングゲームを作り、「C++の機能(クラス, vector, ポリモーフィズム)をゲームを作るにあたって実際にどう使うか何となく理解」することが目的になっています。

読者の想定

  • CCSのC言語講座を受講した方
  • C、C++の本やサイトを読み、配列や関数は何となく使えるが、ゲームを作るにあたってクラスをどう使えば良いか分からない方

OpenSiv3Dの機能をフルで使わない面がありますが、ご了承下さい。

開発環境

OpenSiv3Dを導入することを考えて 開発に必要な環境、動作に必要な環境|Siv3D リファレンス v0.6.3 に適合する環境を構築しましょう。

WindowsであればVisual Studioでインストール時に「C++ によるデスクトップ開発」にチェックを入れましょう。(インストール済みの場合はVisual Studioバージョンの確認とVisual Studio Installerを起動し、インストール済み→変更から「C++ によるデスクトップ開発」にチェックがついているか確認しましょう。) f:id:chinimuruhi:20211104114335p:plain

macOSであればXCodeをインストールしておきましょう。

OpenSiv3Dの導入

開発を始める(SDK のインストール)|Siv3D リファレンス v0.6.3 を参考に導入してください。

導入出来たら、上記の手順に従ってサンプルプログラムを動かすところまでやってみましょう!

各回の資料について

記事を随時追加していきます。記事が追加されていない分は予定であり、変更の可能性があります。

  1. クラスの基本
  2. ファイル分け・コンポジション
  3. vectorの基本
  4. 参照・クラスのポインタ
  5. 関数オーバーロード・range-based-for
  6. イテレータ・vector要素の削除
  7. remove_if・ラムダ式
  8. GameManagerクラス
  9. new・delete・スマートポインタ
  10. 基底・派生クラス・仮想関数・ポリモーフィズム
  11. 演習問題の解答

演習問題について

Siv3D.hppをインクルードしているプログラムはOpenSiv3Dを使ったアプリケーションを、そうでないプログラムはコンソールアプリケーション(文字だけ出てくる黒画面のやつ)で解くことを想定しています。

演習問題の回答は随時追加していきます。

謝辞

本資料は、やぎりさんの作成したC++Siv3D入門講座のOpenSiv3D版として作成されました。ありがとうございました。

qiita.com

CCS Unity入門記事 2020

 

こちらはCCS Advent Calendar 2020の23日目の記事です.

adventar.org

22日目の記事はこちら! VTuber たま〜に見ますが 良いですよねぇ(字余り) yoooomaruuuu.hatenablog.com

はじめに

今年はUnityを触っていることが多かったのでUnityについて書こうと思います.Unity勢が増えると嬉しいなあ...

Unityとは

Unityとはゲーム開発ツールです.PCからスマホ,ゲーム機,Web,VR/ARなど向けの開発に対応しています.2Dゲームも3Dゲームもこれで作れます!

読者の想定

  • CCSのC言語講座を受けた方 (or 何かしらのプログラミング言語の入門書を読んだことがあるくらいのレベルの方?)

  • C#に関する知識はなくてもOK

  • クラスベースのプログラミングをしたことがなくてもOK

とりあえず動かしてみるというよりは,応用が利きやすい内容を目指します.知っておくべきだと考えるUnityの仕様等のことを挟み周り道することがありますが,是非お付き合い下さい.

質問があればいつでも私に聞いて下さい!

この記事内でやること

  • 世界を作る!

  • ものを置く!

  • もの(プレイヤー)を操作できるようにする!

f:id:chinimuruhi:20201220165329p:plain

f:id:chinimuruhi:20201223180838g:plain

です.本当は敵を出したりしたかったけど...時間切れ...

この状態から敵を追加したり,というアレンジはしやすい内容を目指して書いているので是非その先も作りこんでみて欲しいですね...需要があれば続きを書くかも...?  

目次

 

 

Unityのインストール

Unityをダウンロード

unity3d.com

 こちらからダウンロードしていきましょう.「Unity Hub をダウンロード」をクリック. インストールが終わったらUnity Hubを起動しましょう.

Unity Hubのセットアップ

Unity本体をインストールしていきます.

左サイドメニューの「インストール」→右上の「インストール」を押します.  f:id:chinimuruhi:20201217235957p:plain

今回は記事を書いている時に推奨リリースとなっていた「Unity 2019.4.16f1(LTS)」を選びました.特に理由が無ければ推奨版か正式版の最新で良いと思います.

f:id:chinimuruhi:20201218005046p:plain

インストールするモジュールを選択します.PC向けゲームを作るならそのままの設定で良いと思います.スマホや他のOS向けにビルドしたい場合は〇〇Build Supportを追加するべきですが,こちらは後からでも追加できます.

 f:id:chinimuruhi:20201218005241p:plain

 Dev toolsの「Microsoft Visual Studio Community」はそこそこ重いです.

軽いのが良いという人はこちらのチェックを外して「Visual Studio Code」+拡張機能を導入すると良いと思います.以下を参考に.

UnityでVisual Studio Codeを使用できるようにするまでの手順 - Qiita

「次へ」を押してしばらく待てばインストール完了です.

 

プロジェクトの作成

 左サイドメニューの「プロジェクト」→「新規作成」を押します.

f:id:chinimuruhi:20201218010335p:plain

今回は3Dのゲームを作成しようと思うので「3D」を選択します. 

「プロジェクト名」と「保存先」を好きなように決め,「作成」を押します.

f:id:chinimuruhi:20201218010451p:plain

Editor画面の紹介

f:id:chinimuruhi:20201218012107p:plain プロジェクトの作成が終了するとEditor画面が表示されます.この画面について紹介していきます.

基本画面の紹介

Project

f:id:chinimuruhi:20201218012150p:plain

プロジェクトフォルダ内の情報です.デフォルトで「Assets」フォルダに「Scenes」フォルダが入っていることがわかります.

Hierarchy

f:id:chinimuruhi:20201218012337p:plain

この世界に存在しているオブジェクトが見れるところです. デフォルトで「Main Camera」と「Directional Light」がありますね.名前の通りカメラと照明です.

Game

f:id:chinimuruhi:20201218012433p:plain

ゲーム内でどのように見えているのか確認できる画面です.現在は「Main Camera」が映す映像がここに表示されるようになっています.

Scene f:id:chinimuruhi:20201218012506p:plain

世界の中を自由に見る・編集することができる画面です.

デフォルトだとGameの隣のタブにあります.

ゲーム感覚でオブジェクトを移動したりできます(後述).操作は以下の通りです.

  • 視点の移動:右クリックを押しながらマウスを動かす

  • 前進,後退:マウスのホイール

  • 左右に移動:マウスの真ん中のボタンを押しながらマウスを動かす

  • オブジェクトの選択:左クリック

Inspector

f:id:chinimuruhi:20201218012823p:plain

選択されているオブジェクトの詳細を表示してくれるところです.

試しにHierarchy上の「Main Camera」をクリックしてみましょう. Inspector上に座標などいろいろな情報が表示されたと思います.

Play/Pause

f:id:chinimuruhi:20201218012858p:plain

上部中央の再生ボタンを押すと作成中のゲームを動かすことができます.また,一時停止ボタンを押せばゲームを一時停止することもできます.

Editor画面のカスタマイズ

 ウィンドウの表示/非表示,位置,レイアウト等は自由に弄れます.自分の使いやすいようカスタマイズしていくと良いでしょう. 絶対にやっておくべきカスタムが一つだけあるので紹介します.

実行時の画面の色の変更

Edit>Preferencesをクリック

f:id:chinimuruhi:20201218013234p:plain

Preferences>Color>Playmode tint の色を分かりやすい色に変えます. f:id:chinimuruhi:20201218013417p:plain

変更後,Playボタンを押してゲームを動かしてみましょう.ゲームの実行時の画面が先ほど指定した色に変わります.これにより,編集時/ゲーム実行時の区別がつきやすくなります. f:id:chinimuruhi:20201218013456p:plain

Q. 何故,編集時/ゲーム実行時の区別がつきやすくする必要がある?

A. ゲーム実行中に行った作業は(ほとんど)実行終了後に破棄されてしまう為.

ゲーム実行中だと気づかずにステージづくり等をしてしまうと...泣いてしまいます.

 

世界を作る

Unityでは「Scene」という世界の中にいろいろなオブジェクトを置いていきます.まずはこのSceneを作成します.

 

Assetsフォルダの整理

Projectウィンドウから作業していきます.基本的に使うのはAssetsフォルダ内です.

元々Assetsフォルダ内に「Scenes」というフォルダがありますが,削除します.

f:id:chinimuruhi:20201221204310p:plain
元々あったフォルダを消して空になったAssetsフォルダ

Assetsフォルダ直下は後々色々なものが置かれる可能性がある為,自分の作業用のフォルダを作ることをお勧めします.

Assetsフォルダのところで右クリック>Create>Folder 今回は自分の作業用のフォルダ名は「_GameFolder」としました.(_をつけたのは名前順にしたときに上の方に表示される為)

 f:id:chinimuruhi:20201221204651p:plain

Sceneを複数作ることが今後あるかも知れないので,Scene保存用のフォルダを作ります.

先ほどと同様に_GameFolderのところで右クリックからフォルダを作成.名前は「Scenes」としました.

f:id:chinimuruhi:20201221204958p:plain  

Sceneの作成

いよいよ世界(Scene)を作っていきます.

先ほど作成した「Scenes」フォルダのところで右クリック>Create>Scene

「Main」という名前にしました.

f:id:chinimuruhi:20201221205528p:plain

これにてSceneの作成は完了です.ダブルクリックして開いてみましょう.

(この際に現在開いているサンプルのSceneを保存するか聞かれる可能性がありますが,使わないので「Don't Save」でOKです.)

Hierarchyウィンドウの上部に「Main」と出ていれば正常に開けています. f:id:chinimuruhi:20201221205433p:plain

世界にモノを置いていく

さて,「Main」という世界(Scene)が作成できました.ここにモノ(オブジェクト)を置いてみましょう.

オブジェクトを置いてみる

Hierarchyウィンドウで右クリック>3D Object>Cube

f:id:chinimuruhi:20201218015601p:plain

すると,HierarchyウィンドウにCubeが追加されました. HierarchyウィンドウのCubeをクリックして,Inspectorウィンドウから詳細を見てみましょう.

f:id:chinimuruhi:20201218015647p:plain

Transform欄では,現在のCubeのPosition(座標),Rotation(角度),Scale(大きさ)が見られます.

f:id:chinimuruhi:20201218015943p:plain

オブジェクトのPositionの(X,Y,Z)が(0,0,0)ではない位置に生成されていることがあります.このような場合にはTransformの右側の3つの点のマーク>Resetを押して位置をリセットしましょう.

オブジェクトを見る

Sceneウィンドウから先ほど作成したCubeを確認してみましょう.

見つからないor見にくい場合にはHierarchyウィンドウから見たいオブジェクト「Cube」をダブルクリックしましょう.自動でCubeの前まで画面が移動してくれます.

f:id:chinimuruhi:20201218020108p:plain

画像のように生成されたCubeが灰色の場合,照明が機能していない可能性があります.Lightingウィンドウから「Auto Generate」のチェックを外し,「Generate Lighting」を押しましょう(私が忘れていたためしばらく灰色のCubeのまま作業が進行します...)

f:id:chinimuruhi:20201218020351p:plain

 

オブジェクトの編集

右上の赤丸で囲んだところから編集モードを選べます.左から順に紹介していきます.

f:id:chinimuruhi:20201218020055p:plain

Hand Tool:視点の移動に使えます.

Move Tool:オブジェクトの移動ができます.

Rotate Tool:オブジェクトの回転ができます.

Scale Tool:オブジェクトの大きさを変更できます.

Rect Tool:こちらもオブジェクトの大きさを変更できます.Scale Toolとの違いは触ってみればわかります.試してみよう!

Move, Rotate or Scale selected objects:Move Tool + Rotate Tool + Scale Toolの欲張りセット となっています.

まずはオブジェクトの移動をしたいので,Move Toolを選択しましょう.

Move Toolを選択したら,Sceneウィンドウ上(又はHierarchyウィンドウ上)からCubeを選択しましょう.

f:id:chinimuruhi:20201218020742p:plain

出てきた矢印をクリックして動かすことでオブジェクトを移動できます.

色々試してみよう

その他のツールも使ってオブジェクトを回転させたり,サイズを変えたりしてみましょう.結構感覚的に操作できます.

また,InspectorウィンドウのTransformからも操作できます.Inspector上の変更したい値(テキストボックスの外)をクリックして左右に動かすことでも値を変更できます

f:id:chinimuruhi:20201218020808g:plain

 

モノを動かしてみる

UnityではRigidbodyという物理エンジンを使うことが出来ます.

が,物理エンジンを使う前にまずはUnityのGameObjectの話をしていこうと思います.

GameObjectの話

先ほどSceneを作ってCubeを置いた通り,Sceneを(主に)色々なGameObjectから構成していきます.元々あった「Main Camera」や「Directional Light」,作成した「Cube」も全てGameObjectです.

f:id:chinimuruhi:20201218021106j:plain

GameObjectは様々なComponentから構成されています.GameObjectがどんなComponentを持っているかによって,性質が変わってきます.

f:id:chinimuruhi:20201218021132j:plain

例として先ほど作成したCubeをInspectorウィンドウから見てみましょう.「Transform」「Mesh Filter」「Mesh Renderer」「Box Collider」から構成されています.それぞれのComponentを軽く紹介します.

f:id:chinimuruhi:20201218021204p:plain

Transform:オブジェクトの座標,角度,大きさ等を保存

Mesh Filter:メッシュ(オブジェクトの形の情報)をMesh Rendererに渡す

Mesh Renderer:Mesh Filterに渡されたメッシュを描写する

Box Collider:直方体の形の当たり判定

つまり,Cubeは位置・角度・大きさの情報,オブジェクトの描写機能,当たり判定機能から構成されています.

これらのComponentを消したり,他のComponentをつけたりすることで求める性質を持つGameObjectへと変えていくことがUnityでのゲーム制作の根幹となります.

物理エンジンのComponentをつける

さて,このCubeが動くよう,物理エンジンのComponentを付けてみます.

CubeのInspector上でAddComponentを押す

f:id:chinimuruhi:20201218021753p:plain

「Rigidbody」と検索をかけ,出てきたものをクリック.

f:id:chinimuruhi:20201218021825p:plain

こうしてCubeに物理エンジン(Rigidbody)をつけることが出来ました.ここから質量や空気抵抗等のパラメータを変えられます.詳しい説明は割愛します.

f:id:chinimuruhi:20201218021840p:plain

実行してみる

上部中央の再生ボタンを押して,動作を確認してみましょう.

f:id:chinimuruhi:20201218021958g:plain

Cubeに重力が働き,落ちていきました.これだと寂しいのでもう一つCubeを作成して引き伸ばして傾けて,坂でも作ってあげましょう. 坂は作成したCubeの位置と角度と大きさを弄ってあげるだけでOKです,以下の2点の理由により.

  • 坂自体は動かない為物理エンジンが不要

  • 衝突判定を行うための「Box Collider」が元々ついている(衝突判定を行うにはぶつかるオブジェクトの少なくとも一方がRigidbodyコンポーネントを持っている必要があるので注意,今回は動くCubeが持っているのでOK)

f:id:chinimuruhi:20201218022047g:plain

良い感じに転がっていきましたね!神ゲームです.

 

 

ステージづくり

これから地面の上をプレイヤー(球)が動く,という動作を目指します.

f:id:chinimuruhi:20201220120759p:plain

まずは必要なオブジェクトを配置していきましょう.先ほどまで使用していたCubeは要らないので消してしまいます.

プレイヤーの配置

今回は球を配置してそれをプレイヤーとしましょう.Cubeを設置した時と同様に

Hierarchyウィンドウで右クリック>3D Object>Shpereを選択します.

Inspector上で名前を変更できます.分かりやすいように名前をつけておきましょう.また,今回はプレイヤーを物理エンジンを使って動かしたい為,「Rigidbody」コンポーネントも追加します.

f:id:chinimuruhi:20201220121953p:plain

ステージの配置

 ステージはいくつかのオブジェクト(地面,壁など)が組み合わさって構成される為,管理しやすいようにまとめておきます.

Hierarchyウィンドウで右クリック>Create Emptyを選択.空のGameObjectが作成されます.これをフォルダのように扱います.Inspector上から名前をStageとしておきました.

作成したStageで右クリック>3D Object>CubeとすることでStageの子要素にCubeを作成することができます.

f:id:chinimuruhi:20201220133508p:plain

Stageの子要素に壁と床をCubeで作成してみましょう.Sceneウィンドウからゲーム感覚で配置していくことができると思います.

f:id:chinimuruhi:20201220135632p:plain

Gameウィンドウでもステージが一覧できるようMain Cameraの位置も調整します.

f:id:chinimuruhi:20201220140611p:plain

ひとまずステージ完成.現在のHierarchyは以下のようになっています. f:id:chinimuruhi:20201220135828p:plain

 

Materialをつける

全てのオブジェクトが白だと見にくいので色をつけていきます.

Unityでは(見た目上の)材質を作成し,それをオブジェクト(のRenderer(描写用コンポーネント))に割り当てることでオブジェクトの見た目を変化させます.

Materialの作成

ProjectウィンドウからAssets/_GameFolder内に「Materials」という名前でフォルダを作成します.

作成したフォルダの中で右クリック>Create>Material

作成されたら好きな名前をつけましょう.

Materialの色の変更

作成したMaterialをクリックすると,InspectorにMaterialの詳細が表示されます.

Main MapsのAlbedoの色を好きな色に変更します.

f:id:chinimuruhi:20201220163736p:plain

他にもShaderを変えたりMetallic属性を変えたりしてガラスっぽい素材や金属感のある素材を作ることも出来ますが,今回は説明を割愛します.

Materialの割り当て

HierarchyウィンドウからPlayerのGameObjectを選択,InspectorからMeshRendererコンポーネント(オブジェクト描写用のコンポーネント)を見てみましょう.

Materialsという項目があります.こちらのElement0の欄に先ほど作成したMaterialをドラック&ドロップします.

f:id:chinimuruhi:20201220164504p:plain

 Playerオブジェクトの色が変わったことを確認しましょう.

同様にしてステージのオブジェクト用にもMaterialを作成し,色をつけて分かりやすくしました.ステージづくりはひとまずこれで終了です.

f:id:chinimuruhi:20201220165329p:plain

 

 

プレイヤーを動かす(Componentを作る)1

先ほど設置したPlayerが操作した通りに動くようにするには,「オブジェクトが自分の押したキーの方向の速度を持つようになるComponent」を追加すれば良いのです.しかしそんな都合の良いComponentが存在するはずもなく...(?)

みなさんお待ちかねプログラミングの時間です.自分でComponentを作りましょう!

Componentのソースファイルを作成

ProjectウィンドウからAssets/_GameFolder内に「Scripts」という名前でフォルダを作成します.

作成したフォルダの中で右クリック>Create>C# Script

名前を「PlayerMover」としました.一度Scriptファイルを作成したら極力ファイル名は変えない方が良いと思います.

もしファイル生成時に名前をつけ損ねて「NewBehaviourScript」という名前になってしまった場合,一度削除をして再度ファイルを作り直すことをお勧めします.

 

作成したファイルを開いてみる

作成したファイルをダブルクリックするとテキストエディタが起動します.デフォルトで以下のような内容が記述されていると思います.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMover : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
    }
}

このファイル内の以下の箇所でこのコンポーネントがどういうものなのかを定義していきます.

public class PlayerMover : MonoBehaviour
{
    //省略
}

定義していく際にクラスというものを使用します.クラスについては後述.

動かしてみる

これから作る予定のコンポーネントの完成品が以下のものになります.説明の前に,どのような動作になるか先に一度動かしてみます.

PlayerMover.csに以下のように打ち込んで保存.

using UnityEngine;

public class PlayerMover : MonoBehaviour
{
    [SerializeField] float speed;

    Rigidbody rb;
    Vector3 velocity;

    void Awake()
    {
        rb = GetComponent<Rigidbody>(); //rbの参照先をRigidbodyの実体に
        velocity = new Vector3(0, 0, 0); //速度の初期値を0ベクトルに
    }

    void Update()
    {
        //キーの入力から速度を決定
        float dx = Input.GetAxisRaw("Horizontal"); //横方向のキー入力を取得
        float dy = Input.GetAxisRaw("Vertical"); //縦方向のキー入力を取得

        Vector3 direction = new Vector3(dx, 0, dy); //方向を決定
        velocity = speed * direction.normalized; //速度ベクトルを決定
    }

    void FixedUpdate()
    {
        rb.velocity = velocity; //物体の速度を書き換える
    }
}

Unityの画面に戻り,HierarchyウィンドウからPlayerを選択,PlayerのInspectorからAddComponent>PlayerMover

f:id:chinimuruhi:20201223181148p:plain

 PlayerMoverコンポーネントが追加されたら,Speedを好きな値に設定しましょう.

f:id:chinimuruhi:20201223181306p:plain

再生ボタンを押して実行してみます.WASDや矢印キーでプレイヤーを動かせました.

f:id:chinimuruhi:20201223180838g:plain

さて,ここからはこのスクリプトの作成方法を説明していきます.

クラスの話

一度Unityから離れてクラスについて説明します.一応知らなくても動くものは作れるので,とりあえず動かしたい!という人はスキップしてもOKです. 予期せぬ動作を防ぐために知っておくべきだと思う点について書きます.

以下のソースコードはUnity上でも動きますが動作の確認が面倒な為,眺めて貰えればOKです.  

クラスとは

クラスとは自分で作る新しい型です(int型やfloat型とか言うあの型).既存の型の変数や関数を組み合わせて作ります.

クラスを使うには,次の2段階のステップを踏みます.

  1. クラスを定義する(どのような型なのかを書く)

  2. 作った型(クラス)をインスタンス化(実体化)する

例として,Human型(クラス)を作り,インスタンス化(実体化)してtaroと名前をつけ,使用してみます.

クラスの定義

身長と体重のデータと,BMIを取得する機能からなる型(クラス)を定義すると以下のようになります.

public class Human
{
    public float Height; //身長
    public float Weight; //体重
    
    //BMIを取得する関数
    public float GetBMI(){
        return Weight / ( Height * Height );
    }
}

Human型をfloat型の変数のHeightとWeight,関数GetBMI()から構成しました.クラスの中に含まれる変数や関数をそれぞれ「メンバ変数」,「メンバ関数」と言います.

インスタンス化,使用

クラスの定義では型がどういうものなのかを決めただけなので,使用するにはその型の実体(インスタンス)を作る必要があります.

先ほど人間というのがどういう型なのかを決めたので,使用するために太郎を作ります.

「new 型名()」によってHuman型の実体(インスタンス)を生成し,taroという名前に結び付けます.

Human taro =  new Human();

クラスの中のメンバ変数には「.(ドット)」でアクセスできます.「taro.Height」と書いたらtaroの中のHeightという意味になります. 太郎の身長と体重を代入してみます.

taro.Height = 2.26; //身長を代入
taro.Weight = 105.3; //体重を代入

クラスの中のメンバ関数も同様に「.(ドット)」でアクセスできます.「taro.GetBMI()」と書けばtaroの中のGetBMI関数を呼ぶことができます.

float bmi = taro.GetBMI(); //BMIを計算してbmiという変数に入れる
Componentのクラスのインスタンス

Humanクラスでは「new 型名()」によって新しい実体を作成していましたが,Unity上でのComponentのクラス(MonoBehaviourを継承するクラス(後述))は「new 型名()」によって実体を作成することはできません.

f:id:chinimuruhi:20201223224303j:plain

その代わり,GameObjectにComponentとして追加される際に実体化されます.Inspector上でAdd Componentをする際にクラスそのものをつけているのではなく,クラスを基にインスタンス化したものをつけています.

f:id:chinimuruhi:20201223230441p:plain

アクセス修飾子

「public float Height;」のように変数や関数の前,クラスの定義部分に書いていた「public」をアクセス修飾子と言います.

全てのメンバ変数・メンバ関数にアクセス出来てしまうと困ることがあるので,このアクセス修飾子によって利用できる範囲を制御します.

代表的なものを2つ紹介します.

public:どこからでもアクセス可能(クラスの外,他のファイルなどからでもOK)

private:クラスの内部でのみアクセス可能

例としてこんなクラスを作ってみます

public class Test{
    int a; //アクセス修飾子を省略するとprivate
    private int b; //privateメンバ
    public int c; //pulicメンバ

    private int Func1(){
        return b + c; //クラス内なのでbが使用可能
    }

    public int Func2(){
        int num = c + Func1(); //クラス内なのでfunc1が使用可能
        return num;
    }

    public void SetB(int _b){
        b = _b; //クラス内なのでbは使用可能
    }

    public int GetB(){
        return b; //クラス内なのでbは使用可能
    }

}

アクセス修飾子を省略するとprivate扱いになります.クラス内ではpublic,private両方の変数・関数の使用が可能です.

インスタンス化して使ってみます.どのメンバにアクセスできるのか,考えてみましょう.

int num;
Test test = new Test();
test.a = 10; //NG.aはprivate
test.b = 5; //NG.bはprivate
test.c = 3; //OK!cはpublicメンバ
num = test.Func1(); //NG.Func1はprivateメンバ
num = test.Func2(); //OK!Func2はpublicメンバ
tets.SetB(226); //OK! SetB関数の中でtest.bに226が代入される
num = test.GetB(); //OK! ↑でtest.bが226になっているためnumは226になる

動作についてはソースコード内にコメントで書き込んだ通りです.クラスの外からはprivateメンバにはアクセス出来ず,publicメンバにだけアクセスできます.

メンバがどこからでも使用可能という状態は危険であるため,出来る限りのメンバはprivateにしておくのが良いと思います.

値型と参照型

C#の型は大きく分けて2つの型に分けられます.

値型:変数に直接値が格納されるもの.数値型(int, float等),bool型,enum型,構造体など.

参照型:変数が持っているのは参照情報(実体が存在する場所の情報)だけのもの.クラス配列,文字列型など.

使用している型がどちらのタイプかにより,振る舞いが変わってきます.特に代入時(〇〇 = △△;)の振る舞いが大きく変わります.以下に例を示します.

まずは値型であるint型で以下のようなコードを考えます.

int a, b; 
a = 10;
b = a;
b = 53;

値型であるintにおいてはaとbはそれぞれ異なる実体を持ちます.また,「b=a;」とした時はaが持つ実体の中身をbにコピーするという動作をします.

aとbは異なる実体を持つため,bを書き換えてもaが書き換わることはありません.

f:id:chinimuruhi:20201223155122p:plain

続いて,参照型であるHumanクラスで以下のようなコードを考えます.

Human a, b; 
a = new Human();
a.Height = 2.26;
b = a;
b.Height = 1.53;

参照型の変数はそのままでは実体を持ちません.インスタンス化された実体への参照を持たせることができます.「b=a;」とした時にはbがaの指している実体を指すようになります(aの参照情報をbにコピーする).よって,aとbが同じ実体を指しているため,bの内容を変更するとaも変更されます.

f:id:chinimuruhi:20201223163443p:plain

値型と参照型を区別せずに使っていると変数の内容の意図しない変更を行ってしまう可能性があります.代入や関数との受け渡しには特に注意しましょう.

プレイヤーを動かす(Componentを作る)2

Unityの話に戻ります.プレイヤーを動かすComponentのクラスを作成していきます.

MonoBehaviourの話

まず,Componentを作るためのクラスについて説明します.

先ほど作成したソースファイル内で,クラスを定義している部分

public class PlayerMover : MonoBehaviour
{
    //省略
}

の「 : MonoBehaviour」はMonoBehaviourクラスの派生したもの,という意味です.つまり,ここでは「MonoBehaviour」クラスの機能を引き継いだクラスである「PlayerMover」クラスを定義しようとしています.(派生したクラスの作成をクラスの継承と言います.クラスの継承について詳しくは割愛します.)

MonoBehaviourとは?

Componentを作るための素だと思ってもらえば大丈夫です.

MonoBehaviour(を継承したクラス)はデフォルトで用意されたイベント関数(Unity側で勝手に呼んでくれる関数)が沢山あります.その中のいくつかを紹介します.

イベント関数の紹介

void Awake():ゲームが起動された後,オブジェクトが生成された瞬間一度呼ばれます.

void Start():ゲームが起動された後,最初のフレームの前に一度呼ばれます.

void Update():毎フレーム呼ばれます.入力の処理等はここに書くと良い.

void FixedUpdate():固定時間につき一回呼ばれます.物理挙動関連の処理はここに書くと良い.

これらのフローチャートは以下のような感じです.

(詳細なフローチャートを見たい方は→イベント関数の実行順序 - Unity マニュアル)

f:id:chinimuruhi:20201223191634p:plain

これらの関数を記述しておけばUnity側で勝手に呼び出してくれます.この関数の中に必要な処理を書いていくことでコンポーネントの性質を決めていきます.

他にも衝突が起こった時に呼ばれるイベント関数等様々なものがありますので,必要に応じてググりましょう.

スクリプトの作成

行いたい動作

ここで一度行いたい動作を整理します.必要な処理は以下のようになります.

  1. 入力した方向を取得する

  2. 入力した方向で,指定した大きさの速度ベクトルを作成する

  3. プレイヤーの物理エンジンの速度を書き換える

イベント関数を書く

PlayerMoverクラスの中に必要なイベント関数を作成します.初期化用のAwakeと毎フレーム呼ばれるUpdate,固定時間毎に呼ばれるFixedUpdateを作成しました.

using UnityEngine;

public class PlayerMover : MonoBehaviour
{
    void Awake()
    {
    }

    void Update()
    {

    }

    void FixedUpdate()
    {
    }
}
メンバ変数の作成

今回必要なメンバ変数は

です.以下のように作成します.

using UnityEngine;

public class PlayerMover : MonoBehaviour
{
    [SerializeField] float speed; //移動スピード

    Rigidbody rb; //Rigidbodyコンポーネントの参照を保存する変数
    Vector3 velocity; //速度ベクトル

     //省略
}

変数の前に"[SerializeField]"とつけることでUnityの画面上から値や参照を変えられるようになります.

f:id:chinimuruhi:20201223181306p:plain

speedを[SerializeField]指定しました.

[SerializeField] float speed;

物理エンジンのRigidbodyコンポーネントの型はRigidbodyです.Rigidbody型で変数を作成しました.

Rigidbody rb;

Vector3はUnityにある3次元ベクトルの型です.これを用いて算出した速度を保存しておくためのベクトルvelocityを作成しました.

Vector3 velocity;
Awakeの中身

続いてAwakeの中身です.Awakeでは今回はメンバ変数の初期化を行います.

speedはUnityの画面上で入力した値で初期化されるため,Awake内で初期化する必要があるのはrbとvelocityです. 以下のように初期化します.

using UnityEngine;

public class PlayerMover : MonoBehaviour
{
    [SerializeField] float speed; //移動スピード

    Rigidbody rb; //Rigidbodyコンポーネントの参照を保存する変数
    Vector3 velocity; //速度ベクトル


    void Awake()
    {
        rb = GetComponent<Rigidbody>(); //rbの参照先をRigidbodyの実体に
        velocity = new Vector3(0, 0, 0); //速度の初期値を0ベクトルに
    }

     //省略
}

細かい箇所について説明していきます.

 rb = GetComponent<Rigidbody>();

「GetComponent<型名>()」で自身と同じGameObjectに付いているComponentの参照を取得できます.GetComponent関数はそこそこ重いため,沢山呼ばれるようなUpdate関数やFixedUpdate関数の中で呼ばないようにすべきです.その為,初期化の段階で呼んで予め参照を取得しておきます.

今回は自身と同じGameObjectに付いているRigidbodyの参照を取得し,rbに入れました.

velocity = new Vector3(0, 0, 0);

初期の速度を0ベクトルにします.「new Vector3()」で新しいベクトルを作成しvelocityにコピー.この際,「new Vector3(0, 0, 0)」とすることでx成分,y成分,z成分それぞれに0を入れることができます.

(Vector3は値型(構造体)であり,変数作成時のデフォルトの値が(0, 0, 0)なのでこの処理は無くても動きます.)

Updateの中身

続いてUpdateの中身です.ここで行うべきことは入力の処理等であるので,

  1. 入力した方向を取得する

  2. 入力した方向で,指定した大きさの速度ベクトルを作成する

を記述します.以下のようになります.

using UnityEngine;

public class PlayerMover : MonoBehaviour
{
    //省略

    void Update()
    {
        //キーの入力から速度を決定
        float dx = Input.GetAxisRaw("Horizontal"); //横方向のキー入力を取得
        float dy = Input.GetAxisRaw("Vertical"); //縦方向のキー入力を取得

        Vector3 direction = new Vector3(dx, 0, dy); //方向を決定
        velocity = speed * direction.normalized; //速度ベクトルを決定

    }

     //省略
}

それぞれ細かい箇所について説明していきます.

//キーの入力から速度を決定
float dx = Input.GetAxisRaw("Horizontal"); //横方向のキー入力を取得
float dy = Input.GetAxisRaw("Vertical"); //縦方向のキー入力を取得

Input.GetAxisRaw();で方向キーの入力を取得,横方向と縦方向それぞれの成分をdxとdyに入れています.

Vector3 direction = new Vector3(dx, 0, dy); //方向を決定

取得した方向キーのデータから進行方向のベクトルを作成するため,「new Vector3()」で新しいベクトルを作成.この際,「new Vector3(dx, 0, dy)」とすることでx成分にdx,z成分にdyを入れることができます.

dyをz成分に入れた理由はUnityの座標軸が以下のようになっている為です. f:id:chinimuruhi:20201223220555p:plain

x:左右方向,右が正

y:上下方向,上が正

z:奥手前方向,奥が正

velocity = speed * direction.normalized; //速度ベクトルを決定

(Vector3の変数).normalizedで長さを1にしたベクトルが取得できます.「direction.normalized」(長さを1にした方向ベクトル)にspeedを掛けることで,入力された向きで大きさがspeedの速度ベクトルを算出しています.

FixedUpdateの中身

最後にFixedUpdateです.ここで行うべきことは物理挙動関連の処理であるので,

  • 速度の書き換え

を行います.

using UnityEngine;

public class PlayerMover : MonoBehaviour
{
    //省略

    void FixedUpdate()
    {
        rb.velocity = velocity; //物体の速度を書き換える
    }
}

FixedUpdate内部では以下のようにして速度を書き換えています.

  1. Awakeで取得しておいたRigidbodyの参照(rb)から速度にアクセス(rb.velocity).

  2. 速度をUpdateで算出しておいたvelocityに書き換える(rb.velocity = velocity)

以上で「PlayerMover」の完成です.

AwakeとStartの使い分け

今回のPlayerMoverでは初期化にAwakeのみを使いましたが,先ほど紹介した通り初期化のための関数は2つあります(AwakeとStart).それぞれ以下のような役割で使い分けると良いと思います.

void Awake():メンバ変数の初期化,他のオブジェクトの参照の取得等

void Start():他のオブジェクトの値の取得等

このような使い分けを行うことで,以下の問題を回避できます.

Unityでは,以下のような順番でイベント関数が呼ばれて行きます.

  1. 全てのオブジェクトのAwake

  2. 全てのオブジェクトのStart

  3. 全てのオブジェクトのFixedUpdate

  4. 全てのオブジェクトのUpdate

...

「どの種類のイベント関数から呼ばれるか」という順番は固定ですが,「どのオブジェクトから呼ばれるかは未定」です.

例えば,以下の画像のような場合,AとBのどちらのStartが先に呼ばれるかによって結果が変わってしまいます.

B内のStartにおいて,A内のNumを値を使用します.この時,

  • Aが先の場合,Num = 10として処理が進む

  • Bが先の場合,A内のStartが呼ばれていない為,Num = 0(デフォルト値)として処理が進む

f:id:chinimuruhi:20201223221311p:plain

このような事態を避けるため,先に呼ばれるAwakeでメンバ変数の初期化や他のオブジェクトの参照の取得を行い,他のオブジェクトの値の取得は後に呼ばれるStartで行うべきだと考えます.

f:id:chinimuruhi:20201223222354p:plain

今回のPlayerMoverでは必要な処理がメンバ変数の初期化と他のオブジェクトの参照の取得のみであったため,Startが必要なかったという訳です.以上でPlayerMoveコンポーネントスクリプトの説明を終わります.

さいごに

もし手元にUnityを開いてここまでの作業をして貰えていたら嬉しすぎておじさん泣いちゃいます.Unityの便利な機能の紹介があまりできなかったのが心残りです.是非使いながら色々調べてみて欲しい.

ゲームを作れるようになりたいなら実践が一番だと思います.ドンドン作ってみて,必要に応じて調べるというスタンスで良いのかなあと個人的には思います.

この記事を一度読んで全てのことを理解するのは難しいと思いますが,「あらゆるComponentをつけたり,消したりすることで求める性質を持つGameObjectへと変えていく」ということが抑えられていればゲームの制作,アレンジもスムーズに行えるようになるのではないかと思います.

敵の追加も「プレイヤーを追いかけてくるコンポーネント」だったり「敵にぶつかったら自身が消滅するコンポーネント」などを作成すれば実現できそうです.

また,全てのComponentを自分で作る必要はありません.「Assets Store」で色々DLできたり,ググると色々出てきたりします.個人でもクオリティ上げやすいのがUnityの良いところの一つだと思います.