1. 程式人生 > >Step 16:【PROCESSING 遊戲程式設計】之黃金礦工

Step 16:【PROCESSING 遊戲程式設計】之黃金礦工

之前,我偶看到 FAL 利用 Processing/p5.js 製作的一些迷你小遊戲(倘若你對此感興趣,請點選這裡)。Simple but fun,這亦讓我萌生了學習製作遊戲的念頭。文學、音樂、舞蹈、雕塑、繪畫、建築、戲劇、電影,“Game”作為備受爭議的第九藝術,其優勢在於,它是可以是多項藝術的結合。當然,我不想在此講這些套話,而僅僅是嘗試以一個遊戲製作初學者的身份,just do it!

這裡寫圖片描述

黃金礦工是一款什麼遊戲?如若你並不知曉,你可以看看度娘怎麼說。而最好地是,Let’s play games together。當然,你也有更多的選擇,根據這個案例,嘗試編寫自己的遊戲。

這裡寫圖片描述

So,我們該怎麼做“Gold Miner”?

首先,我們得梳理一下自己的思路。這是我做的一張“Gold Miner”專案的簡易流程圖:
這裡寫圖片描述

遊戲開發者的思路清晰十分重要,如若不然,你可以先觀看執行效果或者執行一下我已編寫好的程式碼,這樣對你會有幫助。

以下是本文的目錄大綱:

  • GameMain
  • GamePlay
  • GameWin
  • GameLost
  • Others
  • Last…

好吧,just do it!

GameMain

在這裡,遊戲主介面只有一個簡單的功能——實現主介面與遊戲介面之間的切換。因此,我們只需製作一個按鈕即可。

這裡寫圖片描述

程式碼1 主介面實現:
void draw() {
  if (condition
==0) { // 遊戲初始介面 image(pic1, 0, 0); image(button1, 600, 300); } } void mousePressed() { if (mouseButton == LEFT&&dist(665, 360, mouseX, mouseY)<50) { // 切換到開始遊戲介面 condition=1; } }

注:condition 這個 int 型的引數,即用於介面間切換。condition=0:主介面;condition=1:目標分數介面;condition=2:遊戲進行介面;condition=3:勝利介面;
condition=4:遊戲失敗介面。

GamePlay

遊戲介面是整個遊戲的核心,它包括了目標分數介面和遊戲進行介面。相對於其他介面而言,遊戲 UI 比較複雜,除了背景與角色、金礦等的繪製,還要處理遊戲時滿足勝利與失敗條件的事件。

這裡寫圖片描述
這裡寫圖片描述

為了讓條理更加清楚,我們將一步一步地來實現它:

第一步,新增遊戲 UI。condition=1 時,繪製目標分數介面;condition=2 時,繪製遊戲進行介面。

程式碼2 新增遊戲 UI:
void draw() {
  if (condition==1) {   // 開始遊戲介面
    image(pic2, 0, 0);
    image(object1, width/2-object1.width/2, 0);
    image(button2, width/2-button2.width/2, 360);
    fill(#98295F);
    textSize(50);
    textAlign(LEFT);
    text(ls.targetScore, width/2-50, 300);
  } else if (condition==2) {  // 遊戲介面
    // 時間記錄
    passTime1= int((millis() - startTime)/1000) % 60;
    passTime2=int((millis() - startTime)/(60*1000)) % 60;
    imageMode(CORNER);
    background(pic2);
    image(tB1, 0, -5);
    fill(#98295F);
    textSize(20);
    textAlign(LEFT);
    text(ls.targetScore, 60, 35);
    image(tB2, 0, 40);
    text(yourScore, 60, 80);
    image(tB3, 660, 20, 60, 60);
    textAlign(CENTER);
    textSize(30);
    text(level, 690, 70);
    image(tB4, 730, 20, 60, 60);
    textSize(20);
    text(timer-passTime1-passTime2*60, 760, 60);

    // 預設設定為 imageMode(CENTER)
    miner.draw();
    ls.show();
    rope.show();
    if (rope.state==1) {
      ls.catchGold();
    }
    image(button5, 620, 50, 60, 60);
    upgrade();  // 升級
  }
}

注:每次呼叫 miner.draw() 方法(來自 Sprites 庫),程式都會預設設定為 imageMode(CENTER)。所以,我們要注意座標值的設定。

第二步,編寫金礦類。金礦不僅有各樣的形態,而且代表了不同的分數。

程式碼3 各式金礦類:
// 金子、鑽石、石頭、炸彈

class Gold {
  PVector pos;
  float size;
  int shape, score;
  float speed, angle;

  Gold(PVector pos, int shape) {
    this.pos=pos;
    this.shape=shape;
    if (shape!=7)
      size=objs[shape].width;
    else
      size=objs[shape].width/3;  // 大金塊圖片的尺寸較大
    setupScore();
  }


  // 初始化分數
  void setupScore() {
    if (shape==0) {
      score=10;
    } else if (shape==1) {
      score=20;
    } else if (shape==2) {
      score=50;
    } else if (shape==3) {
      score=100;
    } else if (shape==4) {
      score=250;
    } else if (shape==5) {
      score=500;
    } else if (shape==6) {
      score=600;
    } else if (shape==7) {
      score=1000;
    }
  }

  void move() {
    pos.x+=speed*sin(angle);
    pos.y-=speed*cos(angle);
  }

  void show() {
    move();
    image(objs[shape], pos.x, pos.y, size, size);
  }
}

第三步,編寫繩索與鉤子。當玩家按下滑鼠左鍵,繩索伸長,倘若鉤子接觸到金礦,金礦會被收集起來。

程式碼4 繩索與鉤子類:
// 繩索與鉤子

class Rope {
  PVector pos;
  float angle, da;
  float speed;
  int state;
  ArrayList<PVector> vertexs;

  Rope() {
    pos=new PVector(0, 15);
    state=0;  // 繩索搖擺
    speed=4;
    da=0.025;
    vertexs=new ArrayList<PVector>();
  }

  // 搖擺
  void shake() {
    if (state==0)
      angle+=da;

    if (angle>PI/2.5) {
      angle=PI/2.5;
      da=-da;
    } else if (angle<-PI/2.5) {
      angle=-PI/2.5;
      da=-da;
    }
  }

  // 伸縮
  void extend() {
    if (state==1)
      pos.y+=speed;
    else if (state==2) {
      pos.y-=speed;
      if (pos.y<=15) {
        miner.setFrameSequence(0, 3, 0);
        state=0;
        pos.y=15;
        rope.speed=4;
      }
    }
  }

  void show() {
    shake();  // 搖擺
    extend();  // 伸縮
    pushMatrix();  
    translate(width/2-5, 80);
    rotate(angle);
    noStroke();
    beginShape(QUADS);
    texture(object3);  // 繩索貼紙
    vertex(-3, 0, 0, 0);
    vertex(3, 0, object3.width, 0);
    vertex(3, pos.y, object3.width, object3.height);
    vertex(-3, pos.y, 0, object3.height);
    endShape();
    image(object2, 0, pos.y, 20, 20);  // 鉤子
    popMatrix();
  }
}

第四步,實現按鈕功能和釋放鉤子的動作。

程式碼5 按鈕與鉤子的功能:
void mousePressed() {
  if (mouseButton == LEFT&&dist(width/2, 420, mouseX, mouseY)<50&&condition==1) {  // 切換到遊戲介面
    condition=2;
    startTime = millis();
  } else if (mouseButton == LEFT&&dist(620, 50, mouseX, mouseY)<=50&&condition==2) {
    skip=true;
  } else if (mouseButton == LEFT&&condition==2&&rope.state==0) {  // 按下滑鼠左鍵,釋放鉤子
    rope.state=1;
    miner.setFrameSequence(0, 3, 0.2);
  }
}

GameWin

當分數達到目標分數且時間值為零,遊戲介面便切換到勝利介面。

程式碼6 勝利介面的實現:
void draw() {
  if (condition==3) {  // 遊戲通關介面
    imageMode(CENTER);
    image(pic3, width/2, height/2);
    // 字型放大效果
    if (textRate>=PI/2) {
      textRate=PI/2;
      image(button3, width/2, height/2+150, 100, 100);
    } else {
      textRate+=0.02;
    }
    image(object4, width/2, height/2-50, 650*sin(textRate), 154*sin(textRate));
    imageMode(CORNER);
  }
}

GameLost

當分數未達到目標分數且時間值為零,遊戲失敗。

程式碼7 失敗介面的實現:
void draw() {
  if (condition==4) {  // 遊戲失敗介面
    imageMode(CENTER);
    image(pic4, width/2, height/2);
    // 字型放大效果
    if (textRate>=PI/2) {
      textRate=PI/2;
      image(button4, width/2, height/2+100);
    } else {
      textRate+=0.01;
    }
    image(object5, width/2, height/2-50, 480*cos(textRate), 260*cos(textRate));
    imageMode(CORNER);
  }
}

Others

添加升級系統。這裡,我以迭代的方式編寫了一個算分方法。每到新的一關,金礦就會根據算分方法得到的數目更新。

程式碼8 升級系統:
// 關卡的管理

class LevelSystem {
  int targetScore, totalScore;
  int[] nums;  // 各種 Gold 的數目
  ArrayList<Gold> golds;

  LevelSystem(int targetScore) {
    this.targetScore=targetScore;
    nums=new int[8];
    golds=new ArrayList<Gold>();
    if (level==1) {
      totalScore=(int)(targetScore*2);  // 實際總分與目標分數的關係
    } else {
      totalScore=targetScore;
    }
    initLevelStart(totalScore);
    genGolds();
  }

  // 新的一關,生成金子等
  void initLevelStart(int tS) {
    tS=scoring(tS, 7, 1000);
    tS=scoring(tS, 6, 600);
    tS=scoring(tS, 5, 500);
    tS=scoring(tS, 4, 250);
    tS=scoring(tS, 3, 100);
    tS=scoring(tS, 2, 50);
    tS=scoring(tS, 1, 20);
    tS=scoring(tS, 0, 10);

    if (tS>25) {
      initLevelStart(tS);  // 開始迭代
    }
  }

  // 算分
  int scoring(int tS, int i, int score) {
    int num=(int)random(0.5*(tS-tS%score)/score, (tS-tS%score)/score);
    tS-=num*score;
    nums[i]+=num;
    return tS;
  }

  void genGolds() {
    genGold(7, nums[7]);
    genGold(6, nums[6]);
    genGold(5, nums[5]);
    genGold(4, nums[4]);
    genGold(3, nums[3]);
    genGold(2, nums[2]);
    genGold(1, nums[1]);
    genGold(0, nums[0]);
  }

  // 生成 Gold
  void genGold(int i, int num) {
    for (int n=0; n<num; n++) {
      golds.add(new Gold(new PVector(random(width), random(200, height)), i));
    }
  }

  // 顯示眾多 Gold
  void show() {
    // 分開寫,防止閃屏
    for (int i=0; i<golds.size(); i++) {
      if (dist(golds.get(i).pos.x, golds.get(i).pos.y, width/2, 95) <golds.get(i).size/2) {
        yourScore+=golds.get(i).score;
        word="+"+golds.get(i).score+"!";
        miner.setFrameSequence(0, 3, 0);
        showText=true;
        golds.remove(i);  // 消除 Gold
      }
    }
    showText();
    for (int i=0; i<golds.size(); i++) {
      golds.get(i).show();
    }
  }

  void showText() {
    if (showText==true) {
      image(tB5, width/2-90, 45, 120, 80);
      textSize(25);
      text(word, width/2-90, 50);
      textRate+=0.3;
    }
    if (textRate>10) {
      showText=false;
      textRate=0;
    }
  }

  // 抓到 Gold
  void catchGold() {
    for (int i=0; i<golds.size(); i++) {
      float x1=golds.get(i).pos.x;
      float y1=golds.get(i).pos.y;
      float x2=width/2-5-sin(rope.angle)*rope.pos.y;
      float y2=80+cos(rope.angle)*rope.pos.y;

      if (dist(x1, y1, x2, y2)<=golds.get(i).size/2) {
        if (golds.get(i).shape!=6) {
          rope.speed=4-golds.get(i).size/30;
        } else {
          rope.speed=3.5;
        }

        if (golds.get(i).shape==0||golds.get(i).shape==1) {
          file[4].play();
        } else if (golds.get(i).shape==6) {
          file[5].play();
        } else {
          file[3].play();
        }

        if (rope.state==1) {
          miner.setFrameSequence(0, 3, 0.6-rope.speed/10);
        }
        rope.state=2;
        golds.get(i).pos.x=width/2-5-sin(rope.angle)*(rope.pos.y+golds.get(i).size/3.5);
        golds.get(i).pos.y=80+cos(rope.angle)*(rope.pos.y+golds.get(i).size/3.5);
        golds.get(i).angle=rope.angle;
        golds.get(i).speed=rope.speed;
      } else if (x2<0||x2>width||y2>height) {
        rope.speed=5;
        showText=true;
        word="lonely…";
        if (rope.state==1) {
          miner.setFrameSequence(0, 3, 0.6-rope.speed/10);
        }
        rope.state=2;
      }
    }
  }
}

新增音效。我們使用 file[i].play() 和 file[i].stop(),以播放和停止播放音訊。

程式碼9 新增音效:
import processing.sound.*;

SoundFile[] file;

// 遊戲素材載入
void loading() {
  // 載入遊戲音樂
  numsounds=10;
  file = new SoundFile[numsounds];
  for (int i = 0; i < numsounds; i++) {
    file[i] = new SoundFile(this, (i+1) + ".wav");
  }
}

void mousePressed() {
  if (mouseButton == LEFT&&dist(665, 360, mouseX, mouseY)<50) {  // 切換到開始遊戲介面
    condition=1;
    file[0].stop();
    file[1].play();
  } else if (mouseButton == LEFT&&dist(width/2, 420, mouseX, mouseY)<50&&condition==1) {  // 切換到遊戲介面
    condition=2;
    file[2].play();
    startTime = millis();
  } else if (mouseButton == LEFT&&dist(620, 50, mouseX, mouseY)<=50&&condition==2) {
    file[6].play();
    skip=true;
  } else if (mouseButton == LEFT&&condition==2&&rope.state==0) {  // 按下滑鼠左鍵,釋放鉤子
    rope.state=1;
    miner.setFrameSequence(0, 3, 0.2);
  } else if (mouseButton == LEFT&&dist(width/2, height/2+150, mouseX, mouseY)<50&&condition==3&&textRate==PI/2) {  // 按下滑鼠左鍵,釋放鉤子
    file[9].play();
    condition=1;  // 下一關開始
    textRate=0;
    ls=new LevelSystem((int)(ls.targetScore*1.5));
  } else if (mouseButton == LEFT&&dist(width/2, height/2+100, mouseX, mouseY)<100&&condition==4&&textRate==PI/2) {  // 按下滑鼠左鍵,釋放鉤子
    file[9].play();
    condition=0;  // 首頁
    file[0].play();
    reset();
  }
}

Last…

曾經,我愛音樂、繪畫、樂器、話劇、電影、文學,如今我斬斷了所有的花心,只為留下你――“Game”,孕育出種子,開花、結點果實。來返四季,嘗也嘗不膩,遊戲上的那些罌粟,有癮,這些歲數了,不想戒了。

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

完整程式碼已放到 CSDN 下載裡,連結請點這裡