Step 16:【PROCESSING 游戏编程】之黄金矿工

之前,我偶看到 FAL 利用 Processing/p5.js 制作的一些迷你小游戏(倘若你对此感兴趣,请点击这里)。Simple but fun,这亦让我萌生了学习制作游戏的念头。文学、音乐、舞蹈、雕塑、绘画、建筑、戏剧、电影,“Game”作为备受争议的第九艺术,其优势在于,它是可以是多项艺术的结合。当然,我不想在此讲这些套话,而仅仅是尝试以一个游戏制作初学者的身份,just do it!

Step 16:【PROCESSING 游戏编程】之黄金矿工_第1张图片

黄金矿工是一款什么游戏?如若你并不知晓,你可以看看度娘怎么说。而最好地是,Let’s play games together。当然,你也有更多的选择,根据这个案例,尝试编写自己的游戏。

Step 16:【PROCESSING 游戏编程】之黄金矿工_第2张图片

So,我们该怎么做“Gold Miner”?

首先,我们得梳理一下自己的思路。这是我做的一张“Gold Miner”项目的简易流程图:
Step 16:【PROCESSING 游戏编程】之黄金矿工_第3张图片

游戏开发者的思路清晰十分重要,如若不然,你可以先观看运行效果或者运行一下我已编写好的代码,这样对你会有帮助。

以下是本文的目录大纲:

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

好吧,just do it!

GameMain

在这里,游戏主界面只有一个简单的功能——实现主界面与游戏界面之间的切换。因此,我们只需制作一个按钮即可。

Step 16:【PROCESSING 游戏编程】之黄金矿工_第4张图片

代码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 比较复杂,除了背景与角色、金矿等的绘制,还要处理游戏时满足胜利与失败条件的事件。

Step 16:【PROCESSING 游戏编程】之黄金矿工_第5张图片
Step 16:【PROCESSING 游戏编程】之黄金矿工_第6张图片

为了让条理更加清楚,我们将一步一步地来实现它:

第一步,添加游戏 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 golds;

  LevelSystem(int targetScore) {
    this.targetScore=targetScore;
    nums=new int[8];
    golds=new ArrayList();
    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; nnew Gold(new PVector(random(width), random(200, height)), i));
    }
  }

  // 显示众多 Gold
  void show() {
    // 分开写,防止闪屏
    for (int i=0; iif (dist(golds.get(i).pos.x, golds.get(i).pos.y, width/2, 95) 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; iget(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; ifloat 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”,孕育出种子,开花、结点果实。来返四季,尝也尝不腻,游戏上的那些罂粟,有瘾,这些岁数了,不想戒了。

Step 16:【PROCESSING 游戏编程】之黄金矿工_第7张图片

Step 16:【PROCESSING 游戏编程】之黄金矿工_第8张图片

Step 16:【PROCESSING 游戏编程】之黄金矿工_第9张图片

Step 16:【PROCESSING 游戏编程】之黄金矿工_第10张图片
图片来自 Fumito Ueda 的作品,这是其访谈录——《Fumito Ueda: Colossus in the Shadow》。

完整代码已放到 CSDN 下载里,链接请点这里。

你可能感兴趣的:(Thinking,In,Processing,Thinking,In,Art,Hewes,的编程艺术)