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 下載裡,連結請點這裡。