之前,我偶看到 FAL 利用 Processing/p5.js 制作的一些迷你小游戏(倘若你对此感兴趣,请点击这里)。Simple but fun,这亦让我萌生了学习制作游戏的念头。文学、音乐、舞蹈、雕塑、绘画、建筑、戏剧、电影,“Game”作为备受争议的第九艺术,其优势在于,它是可以是多项艺术的结合。当然,我不想在此讲这些套话,而仅仅是尝试以一个游戏制作初学者的身份,just do it!
黄金矿工是一款什么游戏?如若你并不知晓,你可以看看度娘怎么说。而最好地是,Let’s play games together。当然,你也有更多的选择,根据这个案例,尝试编写自己的游戏。
So,我们该怎么做“Gold Miner”?
首先,我们得梳理一下自己的思路。这是我做的一张“Gold Miner”项目的简易流程图:
游戏开发者的思路清晰十分重要,如若不然,你可以先观看运行效果或者运行一下我已编写好的代码,这样对你会有帮助。
以下是本文的目录大纲:
好吧,just do it!
在这里,游戏主界面只有一个简单的功能——实现主界面与游戏界面之间的切换。因此,我们只需制作一个按钮即可。
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:游戏失败界面。
游戏界面是整个游戏的核心,它包括了目标分数界面和游戏进行界面。相对于其他界面而言,游戏 UI 比较复杂,除了背景与角色、金矿等的绘制,还要处理游戏时满足胜利与失败条件的事件。
为了让条理更加清楚,我们将一步一步地来实现它:
第一步,添加游戏 UI。condition=1 时,绘制目标分数界面;condition=2 时,绘制游戏进行界面。
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)。所以,我们要注意坐标值的设定。
第二步,编写金矿类。金矿不仅有各样的形态,而且代表了不同的分数。
// 金子、钻石、石头、炸弹
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);
}
}
第三步,编写绳索与钩子。当玩家按下鼠标左键,绳索伸长,倘若钩子接触到金矿,金矿会被收集起来。
// 绳索与钩子
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();
}
}
第四步,实现按钮功能和释放钩子的动作。
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);
}
}
当分数达到目标分数且时间值为零,游戏界面便切换到胜利界面。
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);
}
}
当分数未达到目标分数且时间值为零,游戏失败。
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);
}
}
添加升级系统。这里,我以迭代的方式编写了一个算分方法。每到新的一关,金矿就会根据算分方法得到的数目更新。
// 关卡的管理
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(),以播放和停止播放音频。
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();
}
}
曾经,我爱音乐、绘画、乐器、话剧、电影、文学,如今我斩断了所有的花心,只为留下你――“Game”,孕育出种子,开花、结点果实。来返四季,尝也尝不腻,游戏上的那些罂粟,有瘾,这些岁数了,不想戒了。
图片来自 Fumito Ueda 的作品,这是其访谈录——《Fumito Ueda: Colossus in the Shadow》。
完整代码已放到 CSDN 下载里,链接请点这里。