0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Processing と Arduino でピンポンゲームを作る

Last updated at Posted at 2025-01-02

タイトルどおりの課題が大学院の課題で出ました。
参考サイトとして以下が提示されたのですが、流し読みだと頭に入ってこないので自分自身のために訳しつつ理解します。

Processing のコードサンプル中のコメントを日本語にしていますが、デフォルトのフォントだとコメント部分が文字化けする可能性があります。
以下のサイトを参考にフォントを変更することで、文字化けを回避できます。
https://blanche-toile.com/web/processing-editor-font

Step1. Setup()Draw() - Processing コードの基本操作

Processing スケッチは基本的に、setup()draw() の 2つの関数からなります。 Arduino のプログラミングと同様に、setup() は実行開始時に一度だけ実行され、draw() は実行中に繰り返し実行されます。デフォルトでは、draw()は1秒間に60回実行されます。

ゲーム用のウィンドウを作るには、ウィンドウのサイズを指定する必要があります。これは size() で実現できます。size() には、ウィンドウの幅と高さを引数として渡します。ウィンドウの背景色は background() 関数で指定します。

void setup(){
  // size(幅, 高さ);
  size(800,600);
}

void draw(){
  // ウィンドウの初期化
  background(0); 
}

size(x, y) を実行することで、xに渡した値が width, y に渡した値が height に代入される仕組み。

Step2. Processing における座標

image.png

ここで、processing の中で座標がどのように定義されるのかを説明します。

一般的な地理空間の実行方法として、原点が中央にあり、x軸の+が右、y軸の+が上を表す方法に親しんでいる人もいるかもません。

しかし Processing では(グラフィカルプログラミングでは一般的にそうですが)、原点は左上の角にあり、x軸は右に、y軸は下に向かって広がっていきます。

したがって、左上の角を (0,0) , 右上の角を (width, 0) , 左下の角を (0, height) , 右下の角を (widht, height) として表現することができます。

Step3. オブジェクト指向プログラミングでボールを表現する

image.png

ここで Processing におけるオブジェクトとクラスの基本的な概念を説明します。
もっと詳しい(そして長い)導入説明は、Processing の Web サイトで参照できます。

ピンポンゲームのプログラミングを開始するために、ボールを定義します。

Processing においては、ボールをオブジェクトとして作成することができます。

プログラミングにおいて、オブジェクトは変数とメソッドの集合です。より実践的な考え方をすると、プログラミングにおけるオブジェクトは、実世界のあらゆる物事と考えることができます。たとえばボールは直径、重さ、色、空間上の位置、速度、弾力性(bounciness)を持つことができ、メソッドとしてはたとえば転がる、弾む、などが考えられます。

私たちのゲームに必要なのは、空間上の位置(xとyの座標として表現)、スピード(xとyのコンポーネントとして)、直径、そして色のみです。

オブジェクトを作成するためには、そのためのテンプレートが必要です。 Processing では、このテンプレートを クラス と呼びます。クラスはオブジェクトの説明であり、変数と関数を持ちます。分類(クラス)と実物(オブジェクト)の関係がそうであるように、クラスが家の設計図と計画書だとすれば、オブジェクトは実際に人が住むことができる家の実物です。

また、クラスは コンストラクター という特殊な関数を持つことができます。コンストラクターは、そのクラスのオブジェクトを作成するための関数です。

我々のボールに関しては、位置と直径をコンストラクターの変数として取り、ボールオブジェクトが作成される際の定義として使うことができます。スピードはゼロ、色は白(グレースケールにおける255)がデフォルトです。これらのパラメーターは、ボールオブジェクトの作成後に変更することも可能です。


// ボールの定義
class Ball {
  float x;         // x座標
  float y;         // y座標
  float speedX;    // 横(x軸)方向のスピード
  float speedY;    // 縦(y軸)方向のスピード
  float diameter;  // 直径
  color c;         // 色
  
  // コンストラクターメソッド
  Ball(float tempX, float tempY, float tempDiameter) {
    x = tempX;
    y = tempY;
    diameter = tempDiameter;
    speedX = 0;
    speedY = 0;
    c = (225); 
  }
}

ボールを使って何かするには、メソッドを定義する必要があります。
私たちのケースでは、ボールを動かして、それをウィンドウ上に描画する必要があります。このために、 movedisplay 関数を定義します。

move 関数はボールの現在の位置をとり、それにx軸とy軸それぞれのスピードを加算します。display 関数は使用する色を設定し、ボールを現在の位置上に表示します。

  void move() {
    // 現在の位置に x軸、y軸それぞれの方向へのスピードを加算
    y = y + speedY;
    x = x + speedX;
  }
  
  void display() {
    fill(c); //色を指定
    ellipse(x,y,diameter,diameter); //円を描画
  }

さらに、ボールの上下左右のx,y座標を計算する関数も作成します。この関数は、あとで衝突の検知に利用します。このような小さな「ヘルパー関数」により、メイン関数が数学的な関数の集まりから人間の言葉のようになり、書きやすく、理解しやすく、保守しやすくなります。

  //衝突検知の関数
  float left(){
    return x-diameter/2;
  }
  float right(){
    return x+diameter/2;
  }
  float top(){
    return y-diameter/2;
  }
  float bottom(){
    return y+diameter/2;
  }

オブジェクトの内部を完全に理解できなくても大丈夫です。重要なことは、操作に必要なパラメータと関数を持つオブジェクトが作成できるということです。以下のボールオブジェクト全体をコピーすることも可能です。

class Ball { 
  float x;
  float y;
  float speedX;
  float speedY;
  float diameter;
  color c;
  
  Ball(float tempX, float tempY, float tempDiameter) {
    x = tempX;
    y = tempY;
    diameter = tempDiameter;
    speedX = 0;
    speedY = 0;
    c = (225); 
  }

  void move() {
    // Add speed to location
    y = y + speedY;
    x = x + speedX;
  }
  
  void display() {
    fill(c);
    ellipse(x,y,diameter,diameter);
  }

  //衝突検知用の関数
  float left(){
    return x-diameter/2;
  }
  float right(){
    return x+diameter/2;
  }
  float top(){
    return y-diameter/2;
  }
  float bottom(){
    return y+diameter/2;
  }
}

Ball クラスというボールのテンプレートができたところで、ゲーム用の ball オブジェクトを作りましょう。 最初に、 ball をグローバルオブジェクトとして setup 関数と draw 関数の外側に作り、コードのどこからでもアクセスできるようにします。 そして setup() 関数の中で、new オペレーターを使って Ball クラスのコンストラクターを呼び出し、ball オブジェクトを作ります。

コンストラクターには、ボールのx座標とy座標および直径を渡します。Processing 内で利用可能な widthheight パラメーターを利用することで、座標をウィンドウの中央に位置させることができます。

Ball ball; // ボールをグローバルオブジェクトとして定義

void setup(){
  size(800,600);
  // 新しいボールをウィンドウの中央に作成(50はボールの直径)
  ball = new Ball(width/2, height/2, 50); 
}

ボールを表示させるためには、スクリーン上に描画する必要があります。

void draw(){
  background(0); //キャンバスをクリアする
  ball.display(); // ボールをウィンドウに表示する
}

Step4. ボールの動きを設定する

次のステップは、ゲーム内でボールを動かすことです。

前のステップで、すでに Ball クラスのメソッドとして動きを実装しているので、あとはそれを使うだけです。これは、setup の中で ball オブジェクトにスピードを与えることで実現できます。

今回は、x軸方向には 5, y軸方向には -3 から 3 までのランダムな数を定数として設定します。

void setup(){
  size(800,600);
  ball = new Ball(400,300,50); //ウィンドウの中央に新たな ball オブジェクトを作成
  ball.speedX = 5; // x軸方向のボールのスピード
  ball.speedY = random(-3,3); // y軸方向のボールのスピード
}

ボールが動けるように、move メソッドを呼び出す必要があります。これは draw() の中の各サイクルで、ボールをスクリーンに描画する前に行う必要があります。

void draw(){
  background(0);    //キャンバスのクリア
  ball.move();      //ボールの位置を計算
  ball.display();   // ボールをウィンドウに描画
}

これで、ウィンドウ内でボールを動くのを見ることができます。

Step5. ボールが壁にぶつかったときの動き

前のステップで、ボールが動くようにしました。しかし、スケッチを見ればわかるように、ボールはウィンドウの中にとどまらず、問題なくウィンドウの外に出て行ってしまいます。

ボールをウィンドウの中に留めるために、ボールがウィンドウの端にぶつかったことを検知する必要があります。これは、if 文で簡単な比較をすることで実現できます。

  // () 内の条件が true なら {} 内の処理を実行する
  if (ball.right() > width) { 
    ball.speedX = -ball.speedX;
  }

このコードで実施しているのは、ボールの右端のx座標をウィンドウの右辺の座標(ウィンドウ幅である width に等しい)と比較することです。ボールの右端のx座標がウィンドウの幅(width)より大きければ、ボールはウィンドウの辺を超えたことになり、{}内のコードが実行されます。ここで、x軸方向のスピードを逆転させることでボールの弾みをシミュレートしています。

残るは、draw() 内で同じ関数をすべての辺について実装することだけです。

if (ball.left() < 0) {
    ball.speedX = -ball.speedX;
  }

  if (ball.bottom() > height) {
    ball.speedY = -ball.speedY;
  }

  if (ball.top() < 0) {
    ball.speedY = -ball.speedY;
  }

ここで、ボールが壁で弾み、ウィンドウ内に止まるコードが実装できました。

Step6. パドルを追加する

image.png

ピンポンゲームを遊ぶためには、ボールを打つためのパドルが必要です。ボールと同様、パドルについてもオブジェクトを作成します。直径ではなく、幅と高さ(wh)を設定し長方形を描画します。

パドルを作成する
class Paddle{

  float x;
  float y;
  float w;
  float h;
  float speedY;
  float speedX;
  color c;
  
  Paddle(float tempX, float tempY, float tempW, float tempH){
    x = tempX;
    y = tempY;
    w = tempW;
    h = tempH;
    speedY = 0;
    speedX = 0;
    c=(255);
  }

  void move(){
    y += speedY;
    x += speedX;
  }

  void display(){
    fill(c);
    rect(x-w/2,y-h/2,w,h);
  } 
  
  //helper functions
  float left(){
    return x-w/2;
  }
  float right(){
    return x+w/2;
  }
  float top(){
    return y-h/2;
  }
  float bottom(){
    return y+h/2;
  }
}

パドルは、ボールと同じ方法で作成できます。
まず、パドルをグローバルオブジェクトとして定義します。

Paddle paddleLeft;
Paddle paddleRight;

次に、setup() の中でパドルのオブジェクトを作り、draw() 関数の中で動かし描画します。今回の例では、幅30ピクセル、高さ200ピクセルのパドルをウィンドウの上下の中央、左右の壁から見た15ピクセルの位置に描画しています。

  paddleLeft = new Paddle(15, height/2, 30,200);
  paddleRight = new Paddle(width-15, height/2, 30,200);

そして、やはり ball に対してしたのと同じように、パドルの動作と描画も draw() の中に追加します。

  paddleLeft.move();
  paddleLeft.display();
  paddleRight.move();
  paddleRight.display();

Step7. キーボードの入力でパドルを動かす

ゲームをプレイするためには、プレイヤーがキーボードでパドルを動かせる必要があります。

これは、Processing の keyPressed()関数 と keyReleased() 関数を使って実現できます。

keyPressed()関数 と keyReleased() 関数は、キーが押されたときと離されたときに実行される独立した関数です。これらの関数の中で、if 文を使ってどのキーが押されたかを判定できます。押されたキーにしたがってパドルのスピードを設定し、離されたらスピードを 0 にすることでパドルの動きを止めます。

void keyPressed(){
  if(keyCode == UP){
    paddleRight.speedY=-3;
  }
  if(keyCode == DOWN){
    paddleRight.speedY=3;
  }
  if(key == 'a'){
    paddleLeft.speedY=-3;
  }
  if(key == 'z'){
    paddleLeft.speedY=3;
  }
}

void keyReleased(){
  if(keyCode == UP){
    paddleRight.speedY=0;
  }
  if(keyCode == DOWN){
    paddleRight.speedY=0;
  }
  if(key == 'a'){
    paddleLeft.speedY=0;
  }
  if(key == 'z'){
    paddleLeft.speedY=0;
  }
}

これで、AキーとZキー、▲(上矢印)キー、▼(下矢印)キーによってパドルを動かすことができるようになったはずです。

この状態だと、パドルがウィンドウの上下の辺を超えて動いてしまうのに気づくでしょう。

これを防ぐために、draw() の中にボールと同様の衝突検知の関数を作る必要があります。今回は動きの向きを変えるのではなく、yの値をパドルがウィンドウの中に止まれる限界の値に設定します。こうすることで、パドルがウィンドウの外に出ようとしても、即座にウィンドウの中に戻すことができます。

  if (paddleLeft.bottom() > height) {
    paddleLeft.y = height-paddleLeft.h/2;
  }

  if (paddleLeft.top() < 0) {
    paddleLeft.y = paddleLeft.h/2;
  }
    
  if (paddleRight.bottom() > height) {
    paddleRight.y = height-paddleRight.h/2;
  }

  if (paddleRight.top() < 0) {
    paddleRight.y = paddleRight.h/2;
  }

Step8. ボールとパドルの衝突

ゲームで遊べるようにするための最後のステップとして、パドルでボールをブロックする機能を実装します。

これを実現するために、ボールとパドルの衝突を検知する必要があります。まずボールの右端か左端のx座標がパドルの端の座標を超えているかを検知し、次にボールがパドルの領域内にあるかを検知します。if 文の中で論理演算子として && を使うことで、すべての条件が揃ったときにだけ if の判定が成功するようにします。ボールが衝突していたら、x軸の方向を反転させることでボールがパドル上で弾むようにします。

  // ボールがパドルを超え、「なおかつ」
  // ボールがパドルの範囲内(パドルの上下の間)にあれば、
  // ボールを逆方向に弾ませる

  if ( ball.left() < paddleLeft.right() && ball.y > paddleLeft.top() && ball.y < paddleLeft.bottom()){
    ball.speedX = -ball.speedX;
  }

  if ( ball.right() > paddleRight.left() && ball.y > paddleRight.top() && ball.y < paddleRight.bottom()) {
    ball.speedX = -ball.speedX;
  }

Step9. スコアリングと新しいゲームの開始

ゲームをもっとおもしろくするために、スコアを記録することもできます。
これをやるためには、ボールがパドルを超えて壁にぶつかったことを検知します。
このコードは、すでに draw() の中にあることを思い出しましょう。

  if (ball.right() > width) {
    ball.speedX = -ball.speedX;
  }
  if (ball.left() < 0) {
    ball.speedX = -ball.speedX;
  }

今は、ボールは左右の壁に衝突したら、方向を変えて弾むようになっています。次はそのかわりに、プレイヤーに得点を与え、ボールを中央に戻してゲームを再開するようにします。

スコアをつけるために、初期値がゼロのふたつのグローバル変数を定義します。

int scoreLeft = 0;
int scoreRight = 0;

壁への衝突が起きたら、相手のスコアを加算します。

  if (ball.right() > width) {
    scoreLeft = scoreLeft + 1;
    ball.speedX = -ball.speedX;
  }
  if (ball.left() < 0) {
    scoreRight = scoreRight + 1;
    ball.speedX = -ball.speedX;
  }

ボールは方向を変えるのではなく、画面中央に表示して新しいラウンドを始められるようにします。

  if (ball.right() > width) {
    scoreLeft = scoreLeft + 1;
    ball.x = width/2;
    ball.y = height/2;
  }
  if (ball.left() < 0) {
    scoreRight = scoreRight + 1;
    ball.x = width/2;
    ball.y = height/2;
  }

スコアをつけるために、スコアをウィンドウの上部に表示します。
この機能は、draw() の最後に実装します。

まず、textSize()textAlign() を使ってテキストを表示させるルールを定義します。

  textSize(40);
  textAlign(CENTER);

このスコアは、スクリーン上部に表示します。

  text(scoreRight, width/2+30, 30); // Right side score
  text(scoreLeft, width/2-30, 30); // Left side score

Step10. ボールがはずむ方向のコントロール

ゲームをもっとおもしろくするために、ボールをパドルから異なる方向に弾ませることができます。

これは、ボールがパドルにぶつかったときのy軸のスピードを変えることで実現できます。
y軸方向のスピードをランダムに設定するのではなく(そうしたければしてもいいのですが)、ボールがパドルのどこにぶつかったかをy軸のスピード計算に使うことができます。

  // ボールがパドルを超え、「なおかつ」
  // ボールがパドルの範囲内(パドルの上下の間)にあれば、
  // ボールを逆方向に弾ませる

  if ( ball.left() < paddleLeft.right() && ball.y > paddleLeft.top() && ball.y < paddleLeft.bottom()){
    ball.speedX = -ball.speedX;
    ball.speedY = ball.y - paddleLeft.y;
  }

  if ( ball.right() > paddleRight.left() && ball.y > paddleRight.top() && ball.y < paddleRight.bottom()) {
    ball.speedX = -ball.speedX;
    ball.speedY = ball.y - paddleRight.y;
  }

ここで行っているのは、ボールが当たった点のパドルの中央からの距離を計算し、それを新しいy軸方向のスピードに利用するということです。

これを実行すると、ボールがかなり高速になり得ることに気づくでしょう。
そうさせないように、最高スピードをたとえば 10 から -10 の間に制限することができます。 これは、map() 関数によって実現できます。map() 関数は、簡単に言えば自分で数式を考えることなく、ある値を一定のスケールに当てはめることができます。

  if ( ball.left() < paddleLeft.right() && ball.y > paddleLeft.top() && ball.y < paddleLeft.bottom()){
    ball.speedX = -ball.speedX;
    ball.speedY = map(ball.y - paddleLeft.y, -paddleLeft.h/2, paddleLeft.h/2, -10, 10);
  }

  if ( ball.right() > paddleRight.left() && ball.y > paddleRight.top() && ball.y < paddleRight.bottom()) {
    ball.speedX = -ball.speedX;
    ball.speedY = map(ball.y - paddleRight.y, -paddleRight.h/2, paddleRight.h/2, -10, 10);
  }

Step11. おまけ:Arduinoのコントロール

ここまでで、遊べるピンポンゲームを完全に実装することができました。

しかしながら、オリジナルのピンポンでは、パドルは可変抵抗器で制御されています。
以下はおまけの演習として、Arduinoと2つの可変抵抗器を Processing のスケッチに接続し、可変抵抗器からの制御を実装します。

これをやるためには、2つの可変抵抗器を回路図に従って Arduino のボードに接続する必要があります。Arduino の IDE を開き、接続用のスケッチを Examples > Communication > SerialCallResponse から開き、Arduinoにアップロードします。

image.png

コードサンプルはこちらのものと思われます

コードサンプルの最後に Processing 側のサンプルコードも載っているので、そこからコードを選んでコピーしてくることができます。

最初に、シリアル接続のライブラリをインクルードしていくつかのグローバル変数を定義する必要があります。たとえば、シリアルポート、受信したシリアルデータの配列、受信したシリアルのバイト数のカウンターや、Arduino と接続できたかどうかのフラグなどです。

import processing.serial.*;
Serial myPort;                       // シリアルポート
int[] serialInArray = new int[3];    // 受信した信号を入れる配列
int serialCount = 0;                 // バイト数のカウンター
boolean firstContact = false;        // マイコンから受信できたかのフラグ

setup() 関数の中に、Arduinoに接続するコードを追加します。このコードは、Processingコンソールにシリアルポートのリストを表示します。このリストの中から、Arduinoとの接続に使うポート番号を選ぶことができます。

    // デバッグ用に、シリアルポートの一覧を表示
    // Processing 2.1 以降を使っている場合は、Serial.printArray() を使用
    println(Serial.list());

    // ここでは自分の Mac の最初のシリアルポートが常に FTDI だとわかっているので、
    // Serial.list()[0] を開いています。
    // Windows の場合は、これは一般的には COM1 を開きます。
    // 自分が使っているポートを開いてください。
    // Open whatever port is the one you're using.
    String portName = Serial.list()[0];
    myPort = new Serial(this, portName, 9600);

Arduino から値を受け取るために、SerialEvent()関数を追加します。この関数の中で、受信したシリアルデータを扱います。

関数の内部で何をやっているかは、コードのコメントで説明しています。

void serialEvent(Serial myPort) {
    // シリアルポートからバイトを読み込む:
    int inByte = myPort.read();
    // 最初のバイトを受信し、それが「A」なら、
    // シリアルバッファをクリアして最初の接続ができたことを通知する
    // そうでなければ、受信したバイトを配列に追加する
    if (firstContact == false) {
      if (inByte == 'A') {
        myPort.clear();          // clear the serial port buffer
        firstContact = true;     // you've had first contact from the microcontroller
        myPort.write('A');       // ask for more
      }
    }
    else {
      // シリアルポートの最後のバイトを配列に追加
      serialInArray[serialCount] = inByte;
      serialCount++;

      // 3バイトに達した場合:
      if (serialCount > 2 ) {
        // 可変抵抗器の値を読み、パドルのy軸の位置にマップする
        paddleLeft.y = map(serialInArray[0], 0, 255, paddleLeft.h/2, height-paddleLeft.h/2);
        paddleRight.y = map(serialInArray[1], 0, 255, paddleRight.h/2, height-paddleRight.h/2);

        // 新たなセンサーの値を要求するために、大文字の「A」を送信
        myPort.write('A');
        // serialCount のリセット:
        serialCount = 0;
      }
    }
}

Arduino を接続し、正しいシリアルポートを選択できていれば、可変抵抗器でパドルをコントロールすることが可能です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?