2019振り返り

2019年も終わりです.結構,年が変わったとて特に何かあるわけではないと思っているタイプですが,せっかくなので振り返りをします.時系列は全く考慮していません.

競プロ

競技プログラミングは,やはり今年はそれなりにやったと思います.10月ごろからは研究が忙しくなり,思うようにできませんでしたが,普通に楽しんでできているので,本当に良い趣味?だと思います.今年は

AtCoder: 891 -> 1142(緑)

CodeForces: 1067 -> 1462(水色)

という感じです.僕は問題を解きたいだけなのであまり色は気にしていませんが,色が上がればそりゃ嬉しいので,また いろいろな知識をつけていきたいと思います.

ICPCの国内予選に出ました

7月くらいにICPC国内予選に出ました.僕は3年で,4年とM1の先輩と3人で出ましたが,残念ながら予選突破とはなりませんでした.C問題は解くべきだったのですが,本番中は考察が追いつかず,解けませんでした.次は頑張りたいと思います.

yukicoderに問題を投稿しました

yukicoderにはたまに問題を投稿しています. 今年コンテストに出題された問題がどれか忘れましたが,

No.893 お客様を誘導せよ - yukicoder

これなんかは今年だったような気がします.

学内プロコンを主催しました

ハッカーランクで独自のコンテストが開けると知ったので,学内のICPCチームで,学内コンテストをやろうという提案を先生方にしていました.これが案外サクッと受け入れられて,無事開催に至りました.作問の流れはyukicoderで出題している経験からある程度わかっていたので,スムーズでした.が,各問題の計算量の見積もりが少し甘かったかなという感じです.でも,良いコンテストになりました.

自然言語処理研究室に配属されました

2年次で受けた とある授業から,その研究室にはお世話になっていました.そしてそのまま,配属になりました.早速研究を行っており,楽しくやっています.勉強会も盛んに行われているので,しっかり勉強して行きたいです.

言語処理学会2019に参加しました.

上述したように,既に2年次の早い段階から研究室に関わりを持っていたこともあり,言語処理学会に参加しました.流石に発表することはないので,聴講です.初めて学会というものに参加して,研究とはこういうものなのか,ということを知りました.名古屋大学は食堂でラーメンのサイズが選べたのでびっくりしました.

NLP2020若手の会第14回シンポジウムに参加しました

北海道で開催された

NLP若手の会 (YANS) 第14回シンポジウム (2019) - NLP 若手の会

に参加しました.2泊3日くらいで,言語処理を研究する若手研究者の発表が聴ける会です.ここで初めてポスター発表というものをしました.これは学会ほど厳しいものではないので,完璧に終わっていない研究でも発表することができます.僕が発表した研究は萌芽研究賞に選ばれて,表彰状をもらいました.ありがとうございました.

部活動もしていました

部活動は,KSWLという部活で,情報系の部活に入っていました.特に今年は役員になっており,会計としてもやっていました.文化祭では一筆書きのゲームを作り,楽しんでプレイしてもらえたような気がしています.このとき使用した知識はグラフ理論(特にオイラーグラフとか)と幅優先探索アルゴリズムで,競プロが大変役に立ちました.Gitリポジトリはこちら

github.com

他にも,監査委員会を煽ったら常任委員会に注意されるなど やんちゃしていましたが,まあこんなことできるのも部活ならではだろう,と思っています().

成績優秀者になりました

学部の上位10%程度が選ばれるらしい成績優秀者に選ばれました.なんか表彰されて,図書カードをもらいました.さらに,この中から2人が選ばれる大金がもらえる賞があるのですが,それには残念ならが選ばれませんでした.

終わりに

なんか他にもあった気がしますが,パッと思い当たるのはこの辺です.

来年もよろしくお願いします!

CodinGame「UNLEASH THE GEEK」参加記

CodinGameというサイトの10日間コンテスト、「UNLEASH THE GEEK」に参加しました。Bronzeリーグ到達、世界1354/2162、日本52/62の成績でした。 www.codingame.com

はじめに

これはCodinGameのコンテストに初めて参加した人の感想です。技術的な攻略はあまり参考にならないかもしれません。

ゲームの概要(ざっくり)

二人対戦のゲームです。各プレイヤーの目標は、5台の車を動かして、盤面に埋められた鉱石をできるだけ多く回収することです。盤面は30x15の固定で、ある程度のマスには鉱石(amadeus)が埋められています。車には「移動」「掘削」「レーダーの要求」「トラップの要求」の指示を出すことができ、いずれの操作も1ターンを消費します。レーダーは設置すれば、設置したチームはその周辺に埋めれられている鉱石の座標と量を知ることができます。また、トラップが設置されているマスを掘削しようとすると、その周辺で爆発が起こり、巻き込まれた車は失われます。

コンテストの様子

Wood Bossとの死闘

最初はWoodBossと戦いました(まだリーグの存在を知りませんでした。)

CodinGameのこの手のコンテストでは、Wood<Bronze<Silver<Gold<Legendという5つのリーグがあり、コンテスト期間の後半になるにつれて上位のリーグが解放されていく仕組みです。各リーグにはBossが存在し、Bossよりも高い評価を獲得すれば上のリーグに上がれます。よって、誰しも最初はWoodリーグなわけです。

WoodBossは必ず1つだけ鉱石を持ち帰るようでした。これなら勝てると思って実装しましたが、どこの入力を使えば鉱石の位置を取得できて、かつ車を向かわせられるかというところに少し苦労しました。データの持ち方にも少し悩みました。でもなんとか実装してWoodBossを撃破しました。

Bronze Bossとの死闘

Woodの次はBronzeリーグです。なんかいきなり強くなってびっくりしました。レーダーめっちゃ置くし、トラップめっちゃ置くし。でも目標は高いほうが良いです、頑張ります。

Woodの時点では、「とにかくランダムに動きランダムに掘る。偶然鉱石を掘れたら一直線に左端に帰る」を実装していました。これでは足りないことは明らかでした。

考えると、盤面は450マスしかないので、適当にループを回しても1ターンあたりの思考時間である50msは余裕で下回ります。また、レーダーさえ置けば鉱石の場所はわかるので、「自分に一番近い鉱石に向かって移動し、掘る」ことは簡単にできそうでした。これを実装すると、スコアが大きく伸びました。楽しい。

でもBronzeはトラップを置きまくります。見ていると、結構な確率で自分の車が2,3台死んでしまいます。なのでトラップを避けることを考えました。
トラップを埋められた時点で、そのマスには穴が空きます。なので、穴が空いているマスを掘らなければ絶対にトラップは引きません。相手が爆破して巻き込まれるケースは消せませんが。しかしこれを書くと、ore==3の時しか掘ってくれず効率がダメダメです。

そこで、「自分が過去に掘った座標を最新20件保存しておき、そこに含まれる座標も掘る」ことにしました。最近自分が掘ってトラップが置かれていなければ、ちょっとやそっとの時間では新たにトラップが置かれることはないだろう、という根拠のない考えに基づいています。これをやると、また結構スコアが伸びました。楽しい。ついでにレーダーの位置を決め打ちもしました。

他の人のを見る

でもBronze Bossは強いです。なんか強いです。何が強いのかはよくわかりませんでした。この辺で、自分では発想がなくなったのでリーダーボードから他の人の対戦の様子を見ることにしました。この頃にはシルバーあたりが解放されていましたが、Bronzeの人たちは案外トラップガン無視で、とにかく掘ることに専念している人が多い印象でした。この人たちがトラップ避けの対策をしているのかは分かりませんでした。この頃から、「トラップを意識すればするほど、鉱石入手量は下がる」ということは分かっていました。でも少なくともBronze Bossには勝てないといけない(ほんまか?)ので、トラップを避けることは続けました。

トップを走る上位陣の様子も見ましたが、あまり分かりませんでした。

終わり

そのまま特に策がないまま、コンテストは終了しました。

感想

とにかくどんな手法が効くか分からないので、思いついたものは悪そうでもやって見るのは大事そうでした。あと、入力としてどんな情報を知ることができるのか、というのは問題文をよく見返すのも大事そうでした。 他の人のを見ても今回は良く分かりませんでしたが、機能として公開されている以上、利用する価値が十分にありそうでした。

さらに、コンテスト中の思考回路的に、どんどん付け足す感じでコードが出来上がって行きます。少し試すつもりでベタ書きしたコードが良い性能を発揮したとき、流れでそのまま置いてしまうことはよくありました。結果、特に部品化されていないコードがmain関数内に溜まるので、非常に見栄えが悪いです。これはバグの元にもなり、反省点でした。

総合的には結果はどうであれ、(自分のAIを作るのも、観戦して他の日本勢を見ているのも)楽しかったので良かったです!また参加したいと思います。

requestsとbeautifulSoup4でスクレイピングをしてみた

スクレイピングとは

webサイトから必要な情報を抜き出してくることです。

requestsとは

pythonのモジュールの一つで、httpの通信ができます

pip install requests

beautifulSoupとは

pythonのモジュールの一つで、webサイトのデータを入力して、それを解析し、良い感じに表示してくれるものです。 今回はbeautifulSoup4を使います。インストール時には、bs4で指定します。

pip install bs4

今回は、はてなブログのトップページを例に、webサイトの見出しとかリンクとかの文章を取ってきたいと思います。

いざ、スクレイピング

スクレイピングをするときには、requestsを用いてwebサイトのhtmlデータを取って来て、それをbeautifulSoupに投げることで、必要箇所を良い感じに抜き出します。

import requests
from bs4 import BeautifulSoup

res=requests.get("https://hatenablog.com")
res.encoding='utf-8'
bf=BeautifulSoup(res.text,'html.parser')

2つのモジュールをimportし、まずはwebサイトのhtmlを取ってきます。これはrequests.get(サイトのURL)で行えます。今回ははてなブログのトップページのURLを入れます。
これの返り値resはResponseオブジェクトと言って、url先のwebサイトの色々な情報が入ったものになります。

次にこれをbeutifulSoupに解析させます。res.textとすると、webサイトのhtmlがそのまま得られるので、これを入力にします。また、これに合わせて第2引数をhtml.parserにします。

これで、解析結果がbfの変数に入りました。bfにも、様々な情報が入っています。 bf.find_all(タグ名)を使うと、htmlタグを指定して、当てはまるものを全て閲覧できます。試しに、aタグを指定してみます。

for e in bf.find_all('a'):
    print(e)

結果は以下のようになります。

<a href="https://hatenablog.com/guide/pro?plus_via=service_top_nav">はてなブログPro</a>
<a href="https://blog.hyouhon.com/entry/2019/09/09/143003">タコの丸ごとピザを焼きました</a>
<a href="https://blog.hyouhon.com/entry/2019/09/09/143003">私的標本:捕まえて食べる</a>
<a href="https://blog.hyouhon.com/entry/2019/09/09/143003">// またタコ釣りに行ってきました。 楽しかったです。 ということで、タコのピザを焼いてみます。 タコは生のまま冷…</a>
......
<a href="https://itunes.apple.com/jp/app/hatenablog/id583299321" target="_blank"><img alt="Download on the App Store" src="https://cdn.blog.st-hatena.com/images/banner/Download_on_the_App_Store_Badge_US-UK_135x40.svg?version=5eb1a238a24f928783bfdf3e8b093e1b38aebe88&amp;env=production"/></a>
<a href="https://play.google.com/store/apps/details?id=jp.ne.hatena.blog" target="_blank"><img alt="Get it on Google Play" height="40px" src="https://cdn.blog.st-hatena.com/images/banner/google-play-badge.png?version=4576ad6d91e1b649ca7282cb6fffa97a00d3f9c4&amp;env=production"/></a>

aタグであるような部分が全て取れていることがわかります。最後にはgoogle playへのリンクまでありました。

でも、これではまだ見にくいので、getText()の関数を使います。

for e in bf.find_all('a'):
    print(e.getText())

結果は以下です。

はてなブログとは
はてなブログPro
『ぼくたちは勉強ができない』126話 感想、小美浪あすみにとって唯我成幸とは...?
ふわふわな日記
ぼく勉 問126 感想「先人はかの日に備え[x]を蓄積する」 『ぼくたちは勉強ができない』 最新話 感想 ネタバレ注意 今…






タコの丸ごとピザを焼きました
私的標本:捕まえて食べる
.....
カラースター
はてなダイアリー
日本語
English

htmlの性質上、<a>タグの中には単なるテキストだけではなくて、画像が入ることもあります。(<a><img src="hoge"></a>のようなパターン。)この時は、空行になります(多分)。

これは適当にif文でマシにすることはできます。(getText() != '' みたいな処理を挟めばいいので)

最後にコード全体です。

import requests
from bs4 import BeautifulSoup

res = requests.get("https://hatenablog.com")
res.encoding = 'utf-8'
bf = BeautifulSoup(res.text, 'html.parser')
for e in bf.find_all('a'):
    if e.getText() != '':
        print(e.getText())

他にも適当にURLを変えたり、all_find('h1')、all_find('p')など、色々変えて見ると面白いかもしれません。

ABC136 D Gathering Children

問題

atcoder.jp

解説

  • 最終的にどのような状態になるのか

軽くシミュレートをすると、子供たちは、最終的に"RL"または"LR"であるような境界を行き来するだけになります。文字列の長さに対して10^ {100}回も移動するので、これは確実です。

そこで、このような境界において子供達が右側と左側のどちらに溜まるのかをうまく決定したいという気持ちになります。

  • 偶奇性

"RL"という地点があり、今"R"に居る状態からこの境界を行き来することを考えます。
1回目の移動:L
2回目の移動:R
3回目の移動:L
4回目の移動:R
というように、これはもちろん交互になるため、移動回数の偶奇を見れば分かりそうです。そして、問題文にある 10^ {100}というのは偶数回であると気づくことも重要です。

  • 遠くからやってくる子供達

"RRRRL"という文字列があるとします。
10^ {100}回移動するとき、それぞれの子供達はどちらに溜まるでしょうか。
あらかじめ子供達に仮の番号をつけた状態でシミュレーションすると、
[1] [2] [3] [4] [5]
1回目 [-] [1] [2] [3,5] [4]
2回目 [-] [-] [1] [2,4] [3,5]
3回目 [-] [-] [-] [1,3,5] [2,4]
4回目 [-] [-] [-] [2,4] [1,3,5]
(4(偶数)回目から10^ {100}回目までも偶数回の移動なので、変わらず)
10^ {100}回目 [-] [-] [-] [2,4] [1,3,5]

というように、偶数番目の子供達と奇数番目の子供達に分かれるということが言えそうです。
というわけで、"R"や"L"が連続して並んでいる区間に対して、これらの処理をしていけば、問題が解けそうです。

以下のコードでは、最初"R"だけについて処理をした後に、文字列を反転させて"L"だけについて処理をしています。こうすることで、常に右に進むものとして扱えるので分かりやすいです。

#define rep(i,j,k) for(int i=(int)j;i<(int)k;i++)
template<typename T> 
void print(vector<T> v){
    for(int i=0;i<v.size();i++){
        if(i)cout<<" ";
        cout<<v[i];
    }
    cout<<endl;
}

signed main (){
    string s;
    cin>>s;
    vector<int> ans(s.size(),0);
    //最初はRだけについて処理
    rep(i,0,s.size()){
        if(s[i]=='R'){
            int idx=1;
            int ct[2]={};
            ct[idx%2]++;
            while(i+1<s.size() and s[i]==s[i+1]){
                idx++;
                ct[idx%2]++;
                i++;
            }
            if(idx%2==1){  //区間の長さが奇数なら
                ans[i+1]+=ct[0]; //偶数番目の要素が境界の右に溜まる
                ans[i]+=ct[1];  //奇数番目の要素が境界の左に溜まる
            }else {
                ans[i+1]+=ct[1];
                ans[i]+=ct[0];
            }
        }
    }

    reverse(all(s));
    reverse(all(ans));
    //今度はLだけについて処理
    rep(i,0,s.size()){
        if(s[i]=='L'){
            int idx=1;
            int ct[2]={};
            ct[idx%2]++;
            while(i+1<s.size() and s[i]==s[i+1]){
                idx++;
                ct[idx%2]++;
                i++;
            }
            if(idx%2==1){
                ans[i+1]+=ct[0];
                ans[i]+=ct[1];
            }else {
                ans[i+1]+=ct[1];
                ans[i]+=ct[0];
            }
        }
    }
    reverse(all(ans));
    print(ans);
}

ct[2] が、連続する区間に対して偶数番目と奇数番目である要素を数えるための配列です。ct[0]、ct[1]が境界のどちらに溜まるのかは、その区間の長さによるので、そこだけ場合分けをしてあげます。

数を数えるところは関数化するのが綺麗なんでしょうが、競プロの範囲ではそこまで考えなくても良いと思っているのでそのままです。。

ABC117 D XXOR 解説

問題

atcoder.jp

数列Aの全ての要素とのxorを最大化するようなK以下の整数を求めよ。

解説

公式解説の序盤について、Xの候補となりうる整数とは

Xがなんでも良ければ難易度が下がりますが、K以下というところが難点です。 まずK以下の整数とはどのようなものかを確認します。

例えば、2進数表示で0101という整数Kがあるとします。これはつまり5なのですが、 1<2<3<4<5001<010<011<100<101ということを思い出せば、少なくとも3bit目が0であるような数に対しては、それより下の1,2bitがどんなものであろうと、Kよりも小さい数だと分かります。 このように、整数Kのあるiビット目が1であれば、そのビットをとりあえず0にした整数Xを持ってこれば、iビット目より下のビットはどんなでも良い、というのが公式解説の序盤です。上記はi=3のときの例となります。

001,010,011 < 101
↑1,2ビットをどう変えても3ビット目に応じて大小が決まる例。

このようなことは、Kのあるビットが1であれば、それをiビット目としていつでも言うことができます。言い換えれば、「iビット目」の候補になるのは、Kにおけるiビット目が1であるようなビットのみ、となります。

ビットの決め方

  • iビット目未満のビット

さて、上記の例における1,2ビットのように、どう変えても大小の結果に影響しないビットというのは、自分の都合の良いように変えることができるわけです。(もちろんXの3ビット目は0である、ということは厳守した上で。)

ここで、x(x<i)ビット目を都合の良いように変えるとは、N個の整数のxビット目の0の数と1の数を数えて、少ない方の数字を採用するいうことです。 例えば、8個の整数において、xビット目が0のものが5個、1のものが3個あれば、Xのxビット目を0にしたときは、0 xor 1 = 1より3つの整数について1が立ちます。つまり、f(X)は 3 \times 2^ x増えます。対して、xビット目を1にしたときは、1 xor 0 = 1より、f(X)は 5 \times 2^ x増えます。ということは、より数が少ない方にXのxビット目を設定すればよいです。 さらに、実はX自体が何かを知る必要はなくて、上記のようにf(X)にどれだけ足されるかという値がわかっているので、直接f(X)を求めることができます。

よって、都合の良いように変えていいビットについては、 2^ x\times max(ct, N-ct)となります。ここで、 ctとは、xビット目が立っている整数の数です。

  • iビット目

このビットは、唯一Kのビットが1で、Xのビットが0であるようなところです。つまりXのビットは必ず0にしないといけないビットです。この問題ではxorを取ることを目的としているので、N個の整数の中でiビットが1であるようなものだけf(X)に影響します。よって、iビット目は必ず 2^ i \times ct です。

  • iビット目より上のビット

ここではK以下の整数という制約を守るため、Kのビットに合わせます。

Kのy(y>i)ビット目が1ならXのyビットも1です。つまり、N個の整数のうちyビット目が0のものだけf(X)に影響します。このときは 2^ y(N-ct)だけ影響します。

Kのyビットが0ならXのyビットも0です。つまり、N個の整数のうちyビット目が1のものだけf(X)に影響します。このときは 2^ y\times ctだけ影響します。

これで全てのビットについてf(X)への影響度がわかったので、f(X)を求めることができます。

ただし、これはiビット目が決まっているからこそできることなので、i の候補が40程度なことから、全探索することを考えます。全探索する中で、Kのビットが1であるようなものについて上記の計算をして、一番大きいものを取りましょう。

コードは一部省略(using namespaceとか)

#define rep(i,j,k) for(int i=(int)j;i<(int)k;i++)
signed main (){
    int N,K;
    cin>>N>>K;
    vector<int> A(N);
    rep(i,0,N)cin>>A[i];
    int res=0;
    // X_i < K_i、つまりX_i=0で、K_i=1であるようなiを全探索
    rep(i,-1,41){ 
        if(i!=-1 && !(K&(1LL<<i)))continue; //Kのiビット目が0ならcontinue
        int ans=0;
        //a[i]に対して、jビット目が立っているものを数える = ct
        rep(j,0,41){ 
            int ct=0;
            rep(k,0,N){ 
                if(A[k]&(1LL<<j))ct++;
            }
            // iビット目より上のビット
            if(j>i){
                if(K&(1LL<<j))ans+=(1LL<<j)*(N-ct);
                else ans+=(1LL<<j)*ct;
            // iビット目
            }else if(i==j){
                ans+=(1LL<<j)*ct;
            // iビット目未満のビット
            }else {
                ans+=(1LL<<j)*max(ct,N-ct);
            }
        }
        res=max(res,ans);
    }
    cout<<res<<endl;
}

ICPC2019 国内予選参加記

開始前

リハーサルと授業がモロ被りしてたので、リハーサル残り20分地点でJ,KをAC。問題のアルファベットが本当にJ,Kだったかは忘れました。その後15分くらいで「短句」を解こうとしましたが、本当に何が間違えているのか分からず解けなくて、「まあもう時間が終わるので本気でやってないんだぜ」感を出してごまかしていました。
リハも終わり、本番は長丁場につきお菓子と水分が必要だということで、コンビニに向かいました。ディスカバリーチャンネルで一時期話題になったサバイバル番組でも、「エド」という人が言っていました。「サバイバルに必要不可欠なのは、water,fire,shelter」だと。ここにお菓子はありませんが、少なくとも水は必要なんですね。コンビニへの行きしなに僕が所属してる部活の展示をちょうどやっていて、先輩づらして様子を見に行ったらICPCを見てくれている教授のうち2人が居て結構びっくりしました。
その後、部活の書類を提出するのをすっかり忘れていたり、学生部からの問い合わせに対応していたりしてドタバタしていました。

なんか本番が始まった

同じ部屋でお菓子の取り合いをしたり、先輩が下の階にいる後輩たちのチームからお菓子を巻き上げたりしているうちに、本番が始まりました。
座った位置的に僕がAをやる流れだったので、Aを通しました。すると先輩が「Bはやるだけ」とか言うので「じゃあおなしゃす」と言っていたらBが通りました。ここまで25分。
その後2時間半椅子を温めて終了しました。(C,Dとも結構惜しいとこまではできた)
2冠160位?くらい。一応学内1位なので、実質優勝です。

終了後

同じ部屋のチームからC問題の解法を軽く聞きました。僕らのチームは下手に考えてて実装をややこしくしていました。
部屋を片付けて、残ったお菓子を適当に袋に詰め込んで撤収です。後半何も分からず暇になって、お菓子食べながら踊っているだけかと思いましたが、案外そんなことはなく、お菓子と飲み物が減りませんでした。
初参加とかの後輩チームも、2冠とかしてたので優秀だなあ、と思いました。ただ問題のレベルが去年の折り紙と比べれば格段に易化していたとは思いますが。

終わり

結果はともあれ、楽しかったのでよかったです。

【入門】processingで最小限のシューティングゲームを作る

こんにちは、ごつちやんです。
これは最小限のシューティングゲームを作るためのチュートリアル記事です。初心者でも完成までたどり着けるように、詳しく書いていきたいと思います。
出てくるプログラムの知識は int型, float型、変数、if、配列、for程度です。どれも事前知識があると嬉しいですが、全て解説は入れているので、問題ないのではないかなと思います。
ではでは。

f:id:gotutiyan:20190512235557g:plain
完成予想図(少し異なるかもしれません)

何か不明な点があれば、twitterの@gotutiyan_kapiまでリプ・DMを飛ばしてください。

はじめに

座標系

processingを使うと、自由に図形を描くことができます。図形を描画する際に必ず指定するのが座標なのですが、processingの場合は座標系における軸の取り方がいつもとは違います。
数学における座標系は、上に行くほどy座標が増えて、右に行くほどx座標が増えます。また、原点は左下です。
対して、processingにおける座標系は、下に行くほどy座標が増えて、右に行くほどx座標が増えます。また、原点は左上です。

f:id:gotutiyan:20200102190010p:plain
座標系の違い(左:数学 右:processing)
最初は戸惑うかもしれませんが、徐々に慣れていきましょう。

setup()とdraw()

ゲームを作るに欠かせないのは「パラパラ漫画」の機構です。ページを素早くめくるように、ゲームは高速に画面が更新されています。テトリスを想像してみてください。ブロックが落ちていくだけでも、ゲーム画面は非常に高速に更新されています。この更新の速さを「フレームレート」と呼んだりしますが、フレームレートが低いことは、いわゆる「カクつく」「処理が重い」と感じる原因です。パラパラ漫画をめくる速度が遅ければ、当然絵もカクつきますね。

さて、processingにはこの「高速に画面を更新する」仕組みがあります。それがsetup()draw()です。これらはプログラミングの中では非常に重要な「関数」というものですが、今は割愛します。
setup()は、ゲームが始まった瞬間に一度だけ処理が行われます。
draw()は、ゲームを遊んでいる間、裏で何回も何回もひたすら処理が行われます。draw()が何回も実行されて、画面がその度に更新されることによって、パラパラ漫画はめくられます.ブロックは落ちるし、キャラは動きます。

具体的には、以下のようなテンプレを元に作成することになります。

void setup(){
  //1回だけ実行される処理
}

void draw(){
  //何回も実行される処理
}

初期設定

今回ゲームを作るにあたって、初期設定をしましょう。
まずは実行画面の大きさです。ゲームをプレイする画面を小さくしたり、大きくしたりできます。
これはsize()によって指定することができます。サイズは一度設定すればそれ以降固定されるので、setup()に書きます。
サイズは何でも良いですが、本記事では500*500にすることにします。

void setup(){
  size(500,500);
}

void draw(){
  //何回も実行される処理
}

実行ボタンを押すと、ある程度の大きさのウィンドウが表示されます。

自機を作る

自機、つまり操作する味方を作ります。自機は以下のようなものを目指してみます。
・形は円で、直径は50
・マウスの座標に追従する
・クリックしている間、画面上部に向けてビームを撃つ

順番にやって行きましょう。

マウスに追従させる

円を描くには、ellipse()を使えば描くことができます。正確には、ellipse(中心のx座標, 中心のy座標, 横の直径, 縦の直径)です。
横と縦の直径を別々で指定できるので、楕円も描くことができますが、今回は円なので同じ値にすれば良いです。
さらに、マウス座標はmouseX、mouseYで取得できます。これはそのまま、マウスのx座標とy座標を表します。

自機は動くもの、つまり高速に更新されるべきものなので、draw()に書くことにします。

void setup(){
  size(500,500);
}

void draw(){
  ellipse(mouseX,mouseY,50,50); //自機
}

ここで実行すると分かるのですが、円がたくさん描かれてしまいます。これは一度描かれた円がずっと残るからで、draw()が回るたびに全消ししていけば解決します。
「円が描かれる」→「全消し」→「また円が描かれる」→「全消し」・・これにより、プレイヤーからは常に1つの円しか見えず、かつ動いているように見えます。
これを実現するためにbackground()を利用しましょう。
processingには描画の順序があります。プログラム的により下の方に書いた図形ほど、前面に描かれます。塗りつぶす動作は一番背面で行いたいので、draw()の直下に書きます。
background(0)なら黒背景、background(255)なら白背景です。

void setup(){
    size(500,500);
}

void draw(){
    background(255); //白背景
    ellipse(mouseX,mouseY,50,50); //こっちの方が下の行に書いているので前面にでる
}
ビームを撃つ

次に「クリックしている間、画面上部に向けてビームを撃つ」ことですが、これは
・クリックしているかどうかを判定する
・ビームを撃つ
のように分けて考えることができます。

クリックしているかどうかを判定するためには、mousePressed、そして、if文を使います。
if文とは、単語の通り「もし〜なら」を実現する機構です。if(条件) のように条件を指定して使い、条件を満たせば「true」となり、処理が実行されます。満たさなければ、「false」となり、実行されません。
この条件の書き方には色々なものがあるので、以下で感覚を掴んでください。

if(2==6){
  //2と6は等しくないのでこの処理は実行されない
}

if(4>1){
  //4の方が1より大きく、条件を満たすのでこの処理は実行される
}

if(5<7 && 4==4){
  //&&で繋いだ条件は両方満たさないといけません。5<7と4==4の両方を満たしているのでこの処理は実行されます
}

if(5<7 && 4==3){
  //5<7は満たしますが4==3は満たしていません。両方ともは満たしていないのでこの処理は実行されません
}

そしてmousePressedは、マウスが押されているかどうかを検知するものです。押されていればtrue、そうでなければfalseを表します。
つまり、if文の条件式にmousePressedを当てはめることによって、押されていればtrueとなり実行され、そうでなければ実行されない機構を作れます。

さらに、その機構によって実行される処理が「ビームを撃つこと」です。何となく、分かったでしょうか。

void setup(){
  size(500,500);
}

void draw(){
    background(255);
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        //ビームを撃つ処理がここに入る
    }
}

では、ビームを撃つにはどうすれば良いでしょうか。本記事ではあくまで「最小限」の構成で作るので、ビームと言ってもただの線にしましょう。
線はline()で描くことができます。正確には、line(x1,y1,x2,y2)と値を4つ指定し、点(x1,y1)、点(x2,y2)をつなぐ線を描けます。
今回は、自機から画面の上部に向けてビームを撃つことを考えるので、指定する2点は以下の座標になります。
・(mouseX, mouseY)
・(mouseX,0)
この2点を繋げば、自機から真上にビームを撃つことができます。X座標は常にマウス座標に合わせるイメージです。

void setup(){
  size(500,500);
}

void draw(){
    background(255);
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0); 
    }
}

敵を作る

敵を作ります。敵は複数存在するべきですが、まずは1体だけ作ってみましょう。
敵は上から降ってきて、特に攻撃などはしてきません。が、常に同じ位置から降ってきてもゲームとしては面白くありません。

まずは敵の仕様です。
・形は円で、直径は50
・上から下に降ってくる
・画面下に出てしまえば、また上から降ってくる
・また上から降ってくるとき、落ちるスピード、落ちてくる位置をランダムにする
・HPが設定されており、ビームを当てるとHPが減る。
・HPが無くなった敵はその場で消滅し、また上から降ってくる

形は円なので自機と同様ellipse()を使いますが、この時の座標はどのように設定するのが良いでしょうか。
ここで、int型の変数というものを使ってx,y座標にしましょう。
例えば、

ellipse(100,100,50,50);

と書けば、中心座標は(100, 100)の点に描かれます。しかし、実行中にこの円は動かせません。もちろん、プログラムの中心座標を書き換えて、再度実行すれば円を違う位置に描くことができますが、それは「実行し直す」操作が必要で、ふさわしくありません。

こんな時、変数を使うことによってうまく処理できます。先ほどの100とか50とかの、ただの数字というのは、変数に対して定数と呼ばれたりします。
変数の型は整数値のintにし、x,y座標としてのenemy_x, enemy_y を作成します。

int enemy_x, enemy_y; //敵のx,y座標
void setup(){
    size(500,500);
}

void draw(){
    background(255); 
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }
}
乱数

さて、enemy_xは敵のx座標であり、初期値をランダムにしたいです。そうでなければ、ゲーム開始時は毎回同じ位置から敵が現れるゲームになってしまい、面白くありません。
そこで登場するのが乱数を得られるrandom(最小値, 最大値)です。一応、最大値自身は得られないことに注意します。

float rand=random(5,100);  //5~99.999...
float rand2=random(100);  //0~99.9999

値を1つしか指定しなければ、最小値には勝手に0が入ります。
random()は最小値と最大値を指定して、その間の値の乱数を得ます。しかし、これはfloat型といって小数点まで許容する型なので、今用意したint型変数enemy_xとは型が合いません。そこで型変換を使います。型変換は、「目的の型(値)」と書くことで、型をその時の一瞬だけ変更できます。

int x=random(100); //intにfloatを代入しようとしてエラー
int x=int(random(100)); //これは両方intになっているので代入できる

ちなみに、float型をint型に変換した際は、小数点以下は切り捨てされます。1.9でも1.1でも変換後は1です。

というわけで乱数の準備が整ったので、使っていきます。
まずはenemy_xの初期値をランダムに、enemy_yの初期値を-50にします。-50にするのは、最初は画面に映らないようにするためです。
初期化は最初の1回だけ行えば良いので、setup()に書きます。
ついでに敵もellipse()で描画しましょう。

int enemy_x, enemy_y; //敵の座標
void setup(){
    size(500,500);
    enemy_x=int(random(width));
    enemy_y=-50;
}

void draw(){
    background(255); 
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }

    ellipse(enemy_x,enemy_y,50,50); //敵
}

widthというのは画面の幅を表すものです。これはシステム変数と呼ばれるもので、最初からprocessingが用意してくれている変数です。実はmousePressedもシステム変数です。
size()で画面サイズを設定しましたが、この時に自動でwidth、heightという名前で幅と高さが代入されます。

上から落とすということ

次にこれを上から下に落とします。
この落とすスピードもランダムにしたいので、enemy_speedも変数として作成しましょう。また、setup()でランダムに初期化します。乱数の範囲は、適当に2~5あたりにすれば良いのではないでしょうか。
動かすということは、draw()が実行される度に座標になんらかの値が足されるということです。つまりenemy_yにenemy_speedを足していけば、敵は下に落ちていきます。
ついでにhpも、enemy_hpという名前で作成し、setup()で100で初期化しておきます。

int enemy_x, enemy_y;
int enemy_speed; //敵のスピード
int enemy_hp;  //敵の体力
void setup(){
    size(500,500);
    enemy_x=int(random(width));
    enemy_y=-50;
    enemy_speed=int(random(2,6)); //スピードの初期化。6は含まないので2~5になる
    enemy_hp=100 //体力の初期化
}

void draw(){
    background(255); 
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }

    ellipse(enemy_x,enemy_y,50,50); 
    enemy_y+=enemy_speed; //座標を増やしていく
}
下まで落ちればまた戻る

次に「下まで落ちればまた上から降ってくる」です。方針としては 下まで行った円の座標を再び上に戻す ことを考えます。
下に落ちるとはどういうことでしょうか。それは円が画面の外に出てしまうことであり、円のy座標がheightを超えてしまうことです。ここでheightは先ほど出てきたシステム変数で、実行画面の高さが自動で格納されています。

この判定はif文で書くことができ、条件式は不等号を使って書けます。

int enemy_x, enemy_y;
int enemy_speed; 
int enemy_hp;
void setup(){
    size(500,500);
    enemy_x=int(random(width));
    enemy_y=-50;
    enemy_speed=int(random(2,6)); 
    enemy_hp=100;
}

void draw(){
    background(255); 
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }

    ellipse(enemy_x,enemy_y,50,50); 
    enemy_y+=enemy_speed;
    if(enemy_y-25>height){ //画面の下に出たら
        //元に戻す
    }
}

if(enemy_y-25>height)がそれです。-25している理由は、円の座標はあくまで円の中心なので、ここだけで判定すればまだ上半分が見えているのに上に戻ってしまい、不自然になるからです。完全に見えなくなってから上に戻すためには、円の上側、つまりenemy_y-25である点で比べる必要があります。
f:id:gotutiyan:20190512123319p:plain
では、このような条件で敵の位置をまた上に戻すにはどうすれば良いでしょうか。実はこの処理は、敵を初期化することと同じになります。setup()を見てみると、ここには既にx座標をランダムにして、y座標を上に持って行って、スピードをランダムし、そしてhpを0にする4行の処理が書かれています。これをそのままもらってこれば良いです。

int enemy_x, enemy_y;
int enemy_speed; 
int enemy_hp;
void setup(){
    size(500,500);
    enemy_x=int(random(width));
    enemy_y=-50;
    enemy_speed=int(random(2,6)); 
    enemy_hp=100;
}

void draw(){
    background(255);
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }

    ellipse(enemy_x,enemy_y,50,50); 
    enemy_y+=enemy_speed;
    if(enemy_y-25>height){ //画面の下に出たら
        //初期化、setup()の4行と同じ
        enemy_x=int(random(width));
        enemy_y=-50;
        enemy_speed=int(random(2,6)); 
        enemy_hp=100;
    }
}

ここで、「では関数に分ければ良いのでは」と思った方は素晴らしいです。でもここでは関数の概念は割愛することにしています。

敵を「倒せる」ようにする

次に敵を倒せるようにします。この周辺の処理は以下のようになるでしょう。
・ビームと敵との当たり判定を取る
・当たり判定が起これば、敵のHPを減らす
・HPが0になった敵は消滅し、また上から降ってくる

敵とビームとの当たり判定はどう書けばよいでしょうか。
この当たり判定もやはりif文で書けます。ビームが敵に当たるということは、ビームのx座標が敵のx座標と被っていることと同じです。

ビームは線なので、x座標はただ1点、mouseXです。
でも敵は円なので、x座標と言っても範囲を持っています。これはenemy_x-25~enemy_x+25で求められます。
この2つから、数学みたいに書くとenemy_x-25<=mouseX<=enemy_x+25 を満たすとき、当たっていると言えます。

f:id:gotutiyan:20190512124237p:plain
黄色い範囲にmouseXが入れば良い
プログラミングではこのような不等式では書けないので、&&を使って両方満たすような条件式で書きます。

int enemy_x, enemy_y;
int enemy_speed; 
int enemy_hp;
void setup(){
    size(500,500);
    enemy_x=int(random(width));
    enemy_y=-50;
    enemy_speed=int(random(2,6)); 
    enemy_hp=100;
}

void draw(){
    background(255);
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }

    ellipse(enemy_x,enemy_y,50,50); 
    enemy_y+=enemy_speed;
    if(enemy_y-25>height){ //画面の下に出たら
        //初期化、setup()の3行と同じ
        enemy_x=int(random(width));
        enemy_y=-50;
        enemy_speed=int(random(2,6)); 
        enemy_hp=100;
    }
    //当たり判定
    if(enemy_x-25<mouseX && mouseX<enemy_x+25){
        //hpを減らす
    }
}

でも、これだけではうまくいかない場合があります。自機は上方向にビームを出しているので、仮に自機より下に敵がいればその敵は倒せないはずです。でもx座標だけで判定しているので、上も下も関係なく当たり判定が起こってしまいます。ということで、敵が自機より上にいる、つまりenemy_y< mouseYの条件を追加してあげます。
このような条件の時、enemy_hpを減らします。とりあえず適当に1引くようにしましょう。

int enemy_x, enemy_y;
int enemy_speed; 
int enemy_hp;
void setup(){
    size(500,500);
    enemy_x=int(random(width));
    enemy_y=-50;
    enemy_speed=int(random(2,6)); 
    enemy_hp=100;
}

void draw(){
    background(255);
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }

    ellipse(enemy_x,enemy_y,50,50); 
    enemy_y+=enemy_speed;
    //画面下に出たら
    if(enemy_y-25>height){ 
        enemy_x=int(random(width));
        enemy_y=-50;
        enemy_speed=int(random(2,6)); 
        enemy_hp=100;
    }
    //当たり判定
    if(enemy_x-25<mouseX && mouseX<enemy_x+25 && enemy_y<mouseY){ //条件を追加した
        enemy_hp--;
    }
}

最後に、HPが無くなれば消滅するところですが、これも結局、初期化と同じ作業になります。下に落ちずとも瞬間的に画面上に戻せば、プレイヤーは消滅したかのように感じるでしょう。この時、HPを元に戻したり、x座標、スピードを再度ランダムに設定するので、やはりsetup()の4行を持ってくることにします。
HPがなくなることはenemy_hp=0になることなので、if文でenemy_hp<0で表現できます。

int enemy_x, enemy_y;
int enemy_speed; 
int enemy_hp;
void setup(){
    size(500,500);
    enemy_x=int(random(width));
    enemy_y=-50;
    enemy_speed=int(random(2,6)); 
    enemy_hp=100;
}

void draw(){
    background(255);
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }

    ellipse(enemy_x,enemy_y,50,50); 
    enemy_y+=enemy_speed;
    //画面下に出たら
    if(enemy_y-25>height){ 
        enemy_x=int(random(width));
        enemy_y=-50;
        enemy_speed=int(random(2,6)); 
        enemy_hp=100;
    }
    //当たり判定
    if(enemy_x-25<mouseX && mouseX<enemy_x+25 && enemy_y<mouseY){ //条件を追加した
        enemy_hp--;
    }
    if(enemy_hp<0){
        enemy_x=int(random(width));
        enemy_y=-50;
        enemy_speed=int(random(2,6)); 
        enemy_hp=100;
    }
}
1体から複数へ

ここまで、敵を1体だけ作り、その敵についての動きを定義してきました。でもシューティングゲームは1体の敵とたわむれるゲームではなく、複数の敵と戦うから楽しいのです(これは筆者の主観が入っていますが笑)。
ということで、敵を増やすことを考えます。

複製するということと、配列

先ほど、1体の敵を作るためにはx,y,座標、スピード、体力の4つの変数が必要なことは体験できたと思います。ならば、同じように変数を増やしていけば、敵を増やすことができます。試しに以下のように作成してみましょう。

int enemy_x,enemy_y,enemy_speed,enemy_hp; //1体目
int enemy2_x,enemy2_y,enemy2_speed,enemy2_hp; //2体目
int enemy3_x,enemy3_y,enemy3_speed,enemy3_hp; //3体目
/*
めっちゃめんどくさそう
*/

これは良くない例です。確かにこの数の変数は必要になるのですが、このような作り方では日が暮れてしまいます。
ここで登場するのが「配列」です。これはいくつかの変数をまとめたようなもので、[ ]の記号を使って、どの変数のことなのかを一括管理できます。
使い方は以下の通りです。

int x[]=new int[100]; //100個の変数を作ったことと同じ
int y=x[0]; //1つ目の変数をyに代入
/*
x[0]で1つ目の変数
x[1]で2つ目の変数
.....
x[99]で100つ目の変数として表記できる
*/

変数を作るときに名前の後ろに[]をつけると、配列として定義できます。作った配列は、名前の後ろに[数字]をつけることで、何番めの変数なのかを表します。ここで、番号を指定する数字は0始まりであることに注意です。例えば、100個の変数を配列で作れば、[0]~[99]までの番号で指定できます。また、この番号のことを添字と言います。
作るときの構文は、一般化すれば
・ 型 変数名[ ]=new 型[個数]
ですね。

敵を増やすには、x,y,スピード、HPの4つの変数を全て配列にすれば良さそうです。

効率の良い配列へのアクセス

今、配列という概念を導入しました。でも、現段階では「変数の宣言」は綺麗に書くことができます。しかし、これを使うときはどうするのでしょうか。

int enemy_x[]=new int[100]
void setup(){
    //ほんまにこれ書くの・・?
    enemy_x[0]=int(random(width));
    enemy_x[1]=int(random(width));
    ..................
    enemy_x[99]=int(random(width));
}

ということで、for文を導入します。構文はこうです。

//iは変数名なのでなんでも良いが、for文にはよくi,j,kが使われる
for(int i=0 ; i<100 ; i++){
    //処理
}

/*
一般化すれば
for(型 変数名 = 初期値; 変数名<終わりの値+1 ; 変数への加算処理){

}
とかけます。
*/

for文では、ある変数(上ではi)を用意して、それを好きな範囲で順番に増やすような操作になります。
上のコードであれば、iは0,1,2,3,4,5.....99まで増えます。

と、ここまできたところで、数字の範囲が共通していることに気づきます。
・100個の要素を持つ配列の添字 → 0,1,2,3,4....99
・for(int i=0;i<100;i++)と書いたときの変数iの動き → 0,1,2,3,4....99
つまり、for文と配列は非常に相性の良いことがわかります。

よって、簡単な例では以下のような処理を書くことができます。

int x[]=new int[100];
void setup(){
    //一気に初期化
    for(int i=0;i<100;i++){
        x[i]=int(random(width));
    }
}
いざ、改良へ

では、いよいよ敵を複数にしていきましょう。とは言っても、変数を全て配列にして、それをfor文で回すような処理に書き換えれば良いです。
素数は今回は10程度にしてみます。
念のため、具体例としては
int enemy_x→int enemy_x[]=new int[10];
のようにすれば良いです。
以上から、以下のようにコードが書き換えられます。少し一気に変えますよ!

int enemy_x[]=new int[10], enemy_y[]=new int[10];
int enemy_speed[]=new int[10]; 
int enemy_hp[]=new int[10];
void setup(){
    size(500,500);
    for(int i=0;i<10;i++){
        enemy_x[i]=int(random(width));
        enemy_y[i]=-50;
        enemy_speed[i]=int(random(2,6)); 
        enemy_hp[i]=100;
    }
}

void draw(){
    background(255);
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }
    for(int i=0;i<10;i++){
        ellipse(enemy_x[i],enemy_y[i],50,50); 
        enemy_y[i]+=enemy_speed[i];
        //画面下に出たら
        if(enemy_y[i]-25>height){ 
            enemy_x[i]=int(random(width));
            enemy_y[i]=-50;
            enemy_speed[i]=int(random(2,6)); 
            enemy_hp[i]=100;
            
        }
        //当たり判定
        if(enemy_x[i]-25<mouseX && mouseX<enemy_x[i]+25 && enemy_y[i]<mouseY){ //条件を追加した
            enemy_hp[i]--;
        }
        if(enemy_hp[i]<0){
            enemy_x[i]=int(random(width));
            enemy_y[i]=-50;
            enemy_speed[i]=int(random(2,6)); 
            enemy_hp[i]=100;
        }
    }
}

enemy関係の変数を全て配列にして全体をfor文で回すようにしただけで、ほとんどコード長を変えずに敵の数を増やすことができました。
draw()の中でfor文を回す感覚がよくわからないかもしれませんが、「毎フレーム10個の敵を一度に描画する」と考えれば良いのではないでしょうか。
draw()の最初ではbackground(255)が効いているので、「10個の敵を描画、座標の更新」→「塗りつぶし」→「10個の敵を描画、座標の更新」→「塗りつぶし」・・が繰り返されることになります。

スコアづけ

これは「おまけ」みたいな要素です。
ゲーム というのは、プレイヤーからすれば何かしらの達成感が欲しいところで、それは報酬という形で与えることができます。今回はスコアという報酬をつけて、モチベーションの向上にしたいと思います。
スコアの仕様はただ一点です。
・敵を1体倒すことで1増える。

この実装は簡単で、int score;のように変数を作ったのちに、敵の体力が0になってまた上に戻るようなときにscoreを増やせば良いです。

次に、このスコアの表示はどうすれば良いでしょうか。実はtext()というものを使えば書くことができます。
text()の使い方は以下の通りです。

text("aaa",10,10); //「aaa」が点(10,10)のところに表示される。
int x=100;
text(x,10,10); //「100」が点(10,10)のところに表示される。
text("aaa"+x,10,10); //「aaa100」が点(10,10)に表示される。

このように、text()で表示する内容にはプラスの記号で繋ぐことで、複数並べることができます。
今回は、"score: "+scoreのようにすれば、「score: 4」のようにスコアが表示されるはずです。ここで、ただの文字列としての"score"と、変数名としてのscoreの違いに注意します。多くのプログラミング言語では、文字列は"ダブルクオーテーション"で囲むことで表せます。processingも例外ではありません。

int enemy_x[]=new int[10], enemy_y[]=new int[10];
int enemy_speed[]=new int[10]; 
int enemy_hp[]=new int[10];
int score; //スコアを格納する変数
void setup(){
    size(500,500);
    for(int i=0;i<10;i++){
        enemy_x[i]=int(random(width));
        enemy_y[i]=-50;
        enemy_speed[i]=int(random(2,6)); 
        enemy_hp[i]=100;
    }
    score=0; //0で初期化
}

void draw(){
    background(255);
    fill(0); //文字色は黒
    text("score: "+score,10,10); //scoreを表示
    fill(255); //でも他は白色
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }
    for(int i=0;i<10;i++){
        ellipse(enemy_x[i],enemy_y[i],50,50); 
        enemy_y[i]+=enemy_speed[i];
        //画面下に出たら
        if(enemy_y[i]-25>height){ 
            enemy_x[i]=int(random(width));
            enemy_y[i]=-50;
            enemy_speed[i]=int(random(2,6)); 
            enemy_hp[i]=100;
            
        }
        //当たり判定
        if(enemy_x[i]-25<mouseX && mouseX<enemy_x[i]+25 && enemy_y[i]<mouseY){ //条件を追加した
            enemy_hp[i]--;
        }
        if(enemy_hp[i]<0){
            enemy_x[i]=int(random(width));
            enemy_y[i]=-50;
            enemy_speed[i]=int(random(2,6)); 
            enemy_hp[i]=100;
            score++;
        }
    }
}

textによる文字の色は、fill()を使って変えることができます。このときの色はbackground()の時と同じで、fill(0)で黒、fill(255)で白です。ただし、fill()はellipse()の塗りつぶす色も変えてしまうので、fill(0)→テキスト描画→fill(255)→図形の描画 という流れにより、テキストだけ黒にすることができます。
他にも、文字サイズを変えたいときにはtextSize(整数)で変えることもできます。

textSize(50);
text("aaa",10,10); //文字サイズ50で「aaa」と表示

以上でスコアの表示できるようになりました.今回扱う内容は,これで終了です.

おわりに

今回は最小構成のシューティングゲームを作成しました。何かの糧になれば幸いです。

ありがとうございました.

さらなる発展

余裕があればやりたいところです。

ゲーム性の拡張

このシューティングゲームには、この記事には書ききれない余地がまだまだあり、かなり発展させられます。
・敵も攻撃してくるようにする
・敵のHPもなんらかの形で見えるようにする
・自機の攻撃を、ただの線だけではなくて、点線のようにしてみる(より細かい弾を連射しているような描画)
・白黒だけでなく、他の色を使ってみる
・敵を倒したとき、敵が爆発するような描画、もしくは何かしらのエフェクトを追加する
・進行方向を変えることでマンネリ化を防ぐ(例えば、「自機は上方向にビームを出して敵は上から落ちてくる」を「自機は下方向にビームを出して敵は下から上がってくる」みたいに、y座標の増減を反転させたり、戻したりする)
・敵を倒すとスコアが増えるだけでなくお金がもらえて、それを使うことで自機を強化できる。
・敵を複数倒すと必殺技みたいなのが使えて、敵を一掃できる。

などなど、様々な拡張が考えられます。
今回の記事では紹介しきれませんでしたが、ぜひ、取り組んでみてください。

シーン遷移の導入

スタート画面や操作説明画面などを追加すれば,もっとゲームらしくなります.
この話題は以下の記事で扱っているので,興味がある方はご覧ください.

gotutiyan.hatenablog.com

PVector型の利用

今回は全ての値をint型で作成しました。その中で、enemy_xとenemy_yというのが出てきましたが、これらの宣言は独立していました。
でも、xとyの組はよく現れるものであり、1つの塊として扱えれば少し楽になれます。つまり、xの変数、yの変数という作り方ではなくて、x,yの組の変数として作るのです。
これを実現するのがPVector型です。PVector型は2つの値を保持できます。具体的には以下の通りです。

PVector point;
point=new PVector(1,2); //配列みたいにnewを使うことに注意
ellipse(point.x , pount.y , 100 , 100); //ellipse(1,2,100,100)と同じ

このコードでは、変数pointは点(1,2)を表します。かつ、.x , .yのようにドットを使うことでそれぞれの値を見ることもできます。
もちろん、これの配列も作ることができるので、改良の余地があります。
ぜひ使い方に慣れて、コードを書きやすくしてみてください。

関数の利用

記事でも一瞬、関数という言葉を出した時がありましたが、関数を使えばより処理が簡潔にかけます。
例えば、敵の情報を初期化するコードは、setup()、敵が下まで落ちきった時、敵を倒したときの3つの場面で実行される可能性があり、プログラム的にも全く同じ記述が3箇所に現れています。しかしこれは冗長なので、関数を使って処理をくくり出してやることで簡潔になります。
関数を利用したものがこちらです。

int enemy_x[]=new int[10], enemy_y[]=new int[10];
int enemy_speed[]=new int[10]; 
int enemy_hp[]=new int[10];
int score; //スコアを格納する変数
void setup(){
    size(500,500);
    for(int i=0;i<10;i++){
        enemy_init(i); //これ!
    }
    score=0; 
}

void draw(){
    background(255);
    fill(0); 
    text("score: "+score,10,10); 
    fill(255); 
    ellipse(mouseX,mouseY,50,50); 
    if(mousePressed){
        line(mouseX,mouseY,mouseX,0);
    }
    for(int i=0;i<10;i++){
        ellipse(enemy_x[i],enemy_y[i],50,50); 
        enemy_y[i]+=enemy_speed[i];
        //画面下に出たら
        if(enemy_y[i]-25>height){ 
            enemy_init(i); //これ!!
        }
        //当たり判定
        if(enemy_x[i]-25<mouseX && mouseX<enemy_x[i]+25 && enemy_y[i]<mouseY){ //条件を追加した
            enemy_hp[i]--;
        }
        if(enemy_hp[i]<0){
            enemy_init(i); //これ!!!
            score++;
        }
    }
}

//初期化する処理をenemy_initという名前をつけてくくり出した。
//これは整数iを与えて、i番目の敵について初期化するもの。
void enemy_init(int i){
    enemy_x[i]=int(random(width));
    enemy_y[i]=-50;
    enemy_speed[i]=int(random(2,6)); 
    enemy_hp[i]=100;
}

どうでしょう、結構見やすくなったと思いませんか?関数を使えるようになると、処理を分けて考えやすくなるし、その結果見やすく理解しやすいコードを書くことができます。

クラス

クラスというのは、もっと大きな処理をまとめて、それを複製するイメージで増やすことができます。
例えば、今回で言う敵を増やすところは、配列を何個も作ってそれを合体させることで実現していました。でもこれは、あくまで独立に宣言された配列を、「分かっている」人間がそれをひとまとまりのように扱っているにすぎません。しかしクラスというのは、先に1まとまりとしての処理(これには変数の宣言、初期化のための関数、実際に下に落とすような処理、当たり判定をする処理など)を書いた後で、その処理ごと複製するイメージです。プログラム的にも、これはひとまとまりな処理系というのが明示的に示せます。

2つ上の項目で紹介したPVectorは、まさにクラスの考えと一致しています。x,yを独立に宣言するなら、それをまとめたものを作れば全て一度に宣言できて便利、というような気持ちです。

他の最小限ゲーム制作シリーズ

本記事ではシューティングを扱いましたが,他のゲームについても,同じくらいの粒度で説明しているものがあります.合わせてご覧ください.

gotutiyan.hatenablog.com

gotutiyan.hatenablog.com