C++OpenSiv3D入門講座 Vol. 07 remove_if・ラムダ式

C++OpenSiv3D入門講座

今回は前回とは別の、vectorの要素を削除する方法を学ぶ。演習問題では前回と同様敵を削除する。 vectorはコンテナの一種である。algorithmヘッダーはコンテナに対して削除・ソートなどを行う事ができる。 今回はコンテナに対し条件に一致する要素を削除する関数、remove_ifを使い、要素を削除する。また条件の指定にラムダ式を用いる。

要素を削除 std::remove_if

remove_ifは条件にあった要素を削除する。 正確には、条件にあった要素を、vectorの後ろに配置しなおし、削除したように見せかけた後のvectorの最後のイテレータを返す。 つまり、remove_if関数を呼んだだけでは要素は削除されておらず、その後にerase関数を呼ぶ必要がある。 remove_ifを使うには、algorithmヘッダーをインクルードする。

remove_ifの使い方

//remove_ifを実行し、条件にあった要素を後ろに詰める。
auto rmvIter = std::remove_if(vec.begin(),vec.end(), 関数オブジェクト);

//後ろの方に詰められた要素を削除
vec.erase( rmvIter, vec.end() );

remove_ifの第一引数には削除するかを判定する範囲の先頭、第二引数には終端を書く。 remove_ifの第三引数には削除の条件を返す関数を書く。

3の倍数を削除する例

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

//3の倍数かどうかをたしかめて返す叙述関数
bool isMultipleThree(int x){
    return (x%3) == 0;
}

int main(){

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

    //0から数を入れる
    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;


    //vecの中から3の倍数を後ろに詰める
    //3の倍数かどうかの判定は3番目のパラメータで渡された関数で判定
    auto rmvIter = std::remove_if(vec.begin(), vec.end(), isMultipleThree);

    //実際に削除
    vec.erase( rmvIter, vec.end() );

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

    return 0;
}

remove_ifの第三引数には、remove_ifが対象にするvectorの中身を引数に取り、bool型(trueかfalse)を返す関数を入れる。以下が例。

bool Func(配列の要素の型 x){
    return (x%3) == 0;//条件に一致してるか確認してbool型を返す
}

動作の様子を、以下の図に示す。 f:id:chinimuruhi:20211126162618p:plain

ラムダ式

本項目ではC++の中でも特に書き方が変態的なラムダ式を扱う。 remove_ifを使う時、大きなプログラムになると削除するかを判定する関数の定義と使う部分が離れてしまう。そこでC++11からはラムダ式というものが導入された。 例えば2で割り切れるかを返す関数オブジェクトを生成する場合

#include <iostream>

int main() {

    //2乗した数を返す関数オブジェクトを生成
    auto mySquare = [](int num) {return num * num; };

    std::cout << mySquare(3) << std::endl;
    std::cout << mySquare(4) << std::endl;

    return 0;

}

上のコードの

 [](int num) {return num * num; };

の部分がラムダ式だ。ラムダ式の型はautoで推論させている。(明示的に型指定をした std :: function オブジェクトや、一部関数ポインタに代入することもできるが、autoで受け取った方が良い)
ラムダ式の書き方(簡易版)

[](引数){処理};

ところで返り値の型を指定していないことに気づいただろうか? これは多くの場合指定しなくてよいものだ。 ラムダ式の返り値を明示的に指定する場合は

[](引数)->返り値の型{
    処理
};

のように()のあとに「->(返り値の型)」という形式にする。その他にも様々なオプションが存在するため、興味があれば調べてみよう。

ラムダ式の活用法

ラムダ式の節での処理は以下のように今まで通り関数を作って使っているのと同じようなものである。

#include <iostream>
//2乗した数を返す関数
auto mySquare(int num) {
    return num * num;
}

int main() {
    std::cout << mySquare(3) << std::endl;
    std::cout << mySquare(4) << std::endl;

    return 0;

}

ラムダ式は関数の一時オブジェクトを作成することができるのが強みの一つである。どのような場面でラムダ式を使うのが良いか、実際に例を挙げる。


関数の引数に関数オブジェクトを渡すときの例

以下は要素を削除 std::remove_ifの節での例の中で、remove_ifを呼んでいる箇所である。

auto rmvIter = std::remove_if(vec.begin(), vec.end(), isMultipleThree);

remove_ifの第三引数には、削除する対象の要素であるかどうかを返す関数を入れていた。

bool isMultipleThree(int x){
    return (x%3) == 0;
}

第三引数に渡す関数オブジェクトを、ラムダ式を用いて直接書くことができる。こうすることにより、remove_if関数の近くに条件を判定する関数を置くことができる。

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

int main(){

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

    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;




    //vecの中から3の倍数を後ろに詰める
    auto rmvIter = std::remove_if(vec.begin(),vec.end(),
        [](int x){
            return (x % 3) == 0;
        }
    );

    //実際に削除
    vec.erase( rmvIter, vec.end() );


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

    return 0;
}

std等に関数オブジェクトを引数にとる関数はいくつか存在する。( std:sort()std::foreachなど) 引数に渡すためなど一度きりしか使われなく、内容が複雑でない関数には積極的にラムダ式を使っていこう。

まとめ

  • vectorの条件に合った要素を削除するのにremove_ifが使える
  • remove_ifでは不要な要素を後ろに並び替えるだけであり、削除は行われていない
  • 関数の一時オブジェクトを作成するのにラムダ式が使える
  • 関数の引数に関数オブジェクトを渡す場合で、内容が複雑でない関数には積極的にラムダ式を使っていこう

演習問題(コンソール)

  1. int型の変数を引数にとり、2倍にして返す関数をラムダ式を用いて作成せよ。
  2. int型のvectorに0から9を入れ、remove_ifとラムダ式を用いて2の倍数を削除せよ。

演習問題(OpenSiv3D)

  1. 前回同様にEnemyクラスを用意した。Enemyのvectorを作り、敵を複数生成せよ。
    #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 });
    }
    
  2. 画面外に出た敵を削除せよ。要素の削除にはalgorithmのremove_ifを使い、その条件の指定にはラムダ式を使用せよ。

C++OpenSiv3D入門講座

C++OpenSiv3D入門講座 Vol. 06 イテレータ・vector要素の削除

C++OpenSiv3D入門講座

今回はvectorの要素を削除する方法を学ぶ。Siv3Dを用いた演習では、画面外に出た敵を削除する。 C++にはSTLという便利なライブラリが標準でついている。STLにはコンテナがあり、コンテナには便利な配列やリスト構造などがはじめから用意されている。vectorはコンテナの一種である。

イテレータ

ポインタを抽象化してstd::vectorや他のコンテナでも似た形で使えるようにしたイテレータというものが存在する。 ポインタが変数をさすのに対して、イテレータvectorの要素をさす。 begin()関数は、配列の先頭へのイテレータを返す関数、end()関数は、配列の最後の要素の次へのイテレータを返す関数。 下の例では、これらを用いてfor文を回している。

#include <iostream>
#include <vector>

int main(){

    std::vector<int> vec;

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

    //イテレータを用いて出力
    for(std::vector<int>::iterator iter =vec.begin(); iter !=vec.end(); ++iter){
        std::cout << *iter << " ";
    }
    std::cout << std::endl;


    //begin()関数は、配列の先頭へのイテレータを返す関数
    std::vector<int>::iterator iter2 = vec.begin();
    std::cout << *iter2 << std::endl;

    //イテレータはポインタと同じく+することで指す要素を進められる
    //はじめの要素を指すイテレータを4つ進めたので、5番目の要素が出力される
    iter2 += 4;
    std::cout << *iter2 << std::endl;

    return 0;
}

std::vector<int>::iteratorは長いので、C++の機能である型推論を使い表記を省略できる。 型名の代わりに、autoと書くことで、コンパイラが型を推測してくれる。

#include <iostream>
#include <vector>

int main(){

    std::vector<int> vec;

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

    //イテレータを用いて出力
    for(auto iter =vec.begin(); iter !=vec.end(); ++iter){
        std::cout << *iter << " ";
    }
    std::cout << std::endl;

    return 0;
}

クラスのvectorイテレータ

MyClassにxという要素があったとする。「MyClassのvectorイテレータ」からそれが指すMyClassのxにアクセスするには、イテレータiterとすると、下のように書けばよい。

iter->x

例:Vector2クラスのvectorへのイテレータを使ってVector2クラスのx, yを出力

#include <iostream>
#include <vector>

class Vector2{
public:
    int x;
    int y;

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

int main(){

    std::vector<Vector2> vec;

    //適当な値のVector2を5個入れる
    for (int i = 0; i < 5; i++){
        vec.emplace_back(Vector2{ rand() % 100, rand() % 100 });
    }

    //出力
    for (auto iter = vec.begin(); iter != vec.end(); ++iter){
        std::cout << iter->x << " " << iter->y << std::endl;
    }

    return 0;
}

vectorの要素の削除

erase関数で要素を削除できる。引数には削除する要素のイテレータを入れる。

#include <iostream>
#include <vector>

int main(){

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

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

    //イテレータを用いて出力
    for(auto iter = vec.begin(); iter != vec.end(); ++iter){
        std::cout << *iter << " ";
    }
    std::cout << std::endl;


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

    //4つめの要素の消去
    vec.erase(vec.begin() + 3);

    //イテレータを用いて出力
    for(auto iter =vec.begin(); iter !=vec.end(); ++iter){
        std::cout << *iter << " ";
    }
    std::cout << std::endl;

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

    return 0;
}

条件にあった要素の削除

条件にあった複数の要素を削除するには以下のようにする。わりとトリッキー。 vectorの要素を削除すると削除した要素を指していたイテレータが迷子になってしまう。erase関数を使って削除をすると、返り値として削除した要素の次の要素へのイテレータが返ってくるので、それをループ用のイテレータに入れる。

#include <iostream>
#include <vector>

int main(){

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

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

    //出力
    for(auto iter =vec.begin(); iter !=vec.end(); ++iter){
        std::cout << *iter << " ";
    }
    std::cout << std::endl;

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

    //要素の消去(偶数を消去)
    auto it = vec.begin();
    while(it != vec.end()){
        if(*it % 2 == 0){//偶数だったら
            std::cout << *it << "を消去" << std::endl;
            //eraseの返り値をitで受ける(返り値は削除したものの次につながっていた要素へのイテレータ)
            it = vec.erase(it);
        }else{
            it++;
        }
    }
    std::cout << std::endl;

    //出力
    for(auto iter =vec.begin(); iter !=vec.end(); ++iter){
        std::cout << *iter << " ";
    }
    std::cout << std::endl;

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

    return 0;
}

まとめ

  • イテレータは配列等で扱いやすくし、抽象化されたポインタ
  • begin()関数は、配列の先頭へのイテレータを返す
  • end()関数は、配列の最後の要素の次へのイテレータを返す
  • ループ内での要素の削除はトリッキーになるので気を付けよう
  • erase関数を使って削除をすると、返り値として削除した要素の次の要素へのイテレータが返ってくる

演習問題(コンソール)

  1. 以下のようなクラスを用意した。MyClassのvectorを作り、適当なa,bの値(0~100ぐらい)をもつデータを10個ほど格納し、イテレータを用いたfor文によって表示せよ。
    #include <iostream>
    
    class MyClass{
    public:
        int a;
        int b;
    
        MyClass(int _a, int _b):
            a{ _a },
            b{ _b }
        {
        }
    };
    
  2. aがbより小さいデータを削除し、再度表示せよ。

演習問題(OpenSiv3D)

今回はvol_3のSOpenSiv3D演習問題の続きから作るか、新しくサンプルプロジェクトからプログラムを書くことをオススメする。いずれにせよ今回はプレイヤーはいなくていいし、敵がポインタ経由でプレイヤーの情報を受け取る必要はない。

  1. 以下のようなEnemyクラスを用意した。Enemyのvectorを作り、敵を複数出せ。forループは、イテレータを用いて書くこと。
    #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 });
    }
    
  2. y座標が480を超えた敵(下の方の画面外に出た敵)を削除するようにせよ。
  3. シーン外に出た敵を削除するようにせよ。
  4. ヒント:```Scene::Size()```関数でシーンのサイズをVec2型で取得することができる。デフォルトではシーンのサイズはウィンドウサイズ(拡大縮小倍率がのっていないもの)と同じサイズになる。 zenn.dev

C++OpenSiv3D入門講座

C++OpenSiv3D入門講座 Vol. 05 関数オーバーロード・range-based-for・constメンバ関数

C++OpenSiv3D入門講座

今回は関数オーバーロードと、range-based-forについて説明する。

関数オーバーロード

C++では、引数が異なる同じ名前の関数を定義できる。そのように関数を多重定義することを関数オーバーロードという。 実行中に、引数によって呼ばれる関数が選択される。

#include <iostream>

//絶対値を返す関数、myAbsを3通りにオーバーロードする
int myAbs(int n){
    std::cout << "int型のmyAbsが呼ばれました" << std::endl;
    if(n > 0){
        return n;
    }else{
        return -n;
    }
}

float myAbs(float n){
    std::cout << "float型のmyAbsが呼ばれました" << std::endl;
    if(n > 0){
        return n;
    }else{
        return -n;
    }
}

double myAbs(double n){
    std::cout << "double型のmyAbsが呼ばれました" << std::endl;
    if(n > 0){
        return n;
    }else{
        return -n;
    }
}


int main(){

    int a = -10;
    float b = -2.71f;
    double c = -3.14;

    std::cout << myAbs(a) << std::endl << std::endl;
    std::cout << myAbs(b) << std::endl << std::endl;
    std::cout << myAbs(c) << std::endl << std::endl;


    return 0;

}


以下は、引数の数によって呼ばれる関数が変わるパターンである。

#include <iostream>

void myFunc(int a){
    std::cout << "引数1つのmyFuncが呼ばれました。引数は " << a << std::endl;
}

void myFunc(int a, int b){
    std::cout << "引数2つのmyFuncが呼ばれました。引数は " << a << " " << b << std::endl;
}

int main(){
    myFunc(1);
    myFunc(2, 3);

    return 0;
}

関数オーバーロードと同じ要領で、コンストラクタも複数定義できる。

#include <iostream>

class MyClass {
public:
    int value;

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

    MyClass(int _value) :
        value{ _value }
    {
        std::cout << "引数ありのコンストラクタが呼ばれました" << std::endl;
    }
};

int main() {

    MyClass obj1{ 10 };
    std::cout << obj1.value << std::endl;

    MyClass obj2;
    std::cout << obj2.value << std::endl;

    return 0;

}

Tips:range-based-for

これまでvectorに対して、for文で用いる時、毎回始めのイテレータと終わりのイテレータを書いていたが、これは面倒である。C++にはrange-based-forがあり、これを用いると、配列のそれぞれの要素に対して行う処理が簡単に書ける。以下に書き方を示す。

for(配列の要素の型 受け取る変数 : 配列名){

}

以下にサンプルコードを用意した。今回は、自分で用意したMyClass型のvectorのそれぞれの要素のaを表示している。配列の要素の型はautoで推論している。constを使うことで値を間違って変更することを防いでおり、参照を用いて、無駄なコピーをが発生しないようにしている。

#include <iostream>
#include <vector>

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

int main() {

    std::vector<MyClass> vec;
    for (int i = 0; i < 5; i++) {
        vec.emplace_back(MyClass{ i });
    }

    for (const auto& i : vec) {
        std::cout << i.a << std::endl;

        // コメントを外すと値を書き換えることになるのでコンパイルエラー
        // i.a = 99;
    }

    return 0;
}

以下はMyClassのaを全て2倍にする例。参照を用いて、vectorの中身を書き換えている。auto&&と、&が1つではなく2つあるがタイプミスではない。今回の場合auto&でも問題ないが、auto&&を用いると右辺値にも対応できるようになり、より汎用的な書き方になるので、range-based-forを用いる際は基本的にこちらを推奨する。(ちなみに、VisualStudio上でマウスカーソルをiの上に持っていくとわかるが、iはちゃんとMyClass&と推論されていることがわかる)詳しくは、range-based for loopsの要素の型について - Qiita参照。

#include <iostream>
#include <vector>

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

int main() {

    std::vector<MyClass> vec;
    for (int i = 0; i < 5; i++) {
        vec.emplace_back(MyClass{ i });
    }

    // 参照を用いてそれぞれのMyClassのaを2倍に
    for (auto&& i : vec) {
        i.a *= 2;
    }

    // 中身を表示
    for (const auto& i : vec) {
        std::cout << i.a << std::endl;
    }

    return 0;
}

Tips:constメンバ関数

constメンバ関数を使うと、メンバ変数を変更できないメンバ関数を作ることができる。これを用いることで、誤ってメンバを変更することを防ぐことができ、また多人数開発の際にメンバ変数を変更していないことを明示できる。

#include <iostream>

class MyClass {
public:
    int a;
    MyClass(int _a);
    void func() const;
};


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

void MyClass::func() const {
    // コメントを外すとメンバ変数を変更してしまうのでコンパイルエラー
    // a = 100;

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


int main() {
    MyClass obj{ 10 };
    obj.func();
    return 0;
}

まとめ

  • 引数が異なる同じ名前の関数を定義でき、実行中に引数によって呼ばれる関数が選択される(関数オーバーロード)
  • C++にはrange-based-forがあり、配列のそれぞれの要素に対して行う処理が簡単に書ける。
  • constメンバ関数は、メンバ変数を変更できないメンバ関数である。

演習問題(コンソール)

  1. 引数で受け取った値の2乗を返す関数mySquare()を作成せよ。mySquare()をint,float,double型の3通りに対応できるようオーバーロードしなさい。
  2. 以下の様なクラスVector2を作った。コンストラクタとデストラクタを作れ。
    コンストラクタ・デストラクタがよばれたら、"~が呼ばれました"と出力するようにせよ。コンストラクタは引数有りのものと引数なし(引数なしの時はx=0,y=0)のものを作ること。
    class Vector2{
        public:
            int x, y;
    
            //以下にコンストラクタとデストラクタ書く
    }
    

C++OpenSiv3D入門講座

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();
        }
    }
}

C++OpenSiv3D入門講座