我目前正在自学游戏编程并重新制作一些经典游戏以学习不同的技巧和东西(正如gamedev.net上的一篇好文章所建议的那样)。
我成功编写了 PONG 代码,现在正在研究 Snake(我认为这就是 Worm 的意思)。现在我想出了很多东西,除了两件事我无法理解。
我的算法很简单:玩家控制头部,身体跟随。头部有它自己的类,从那里每个段都被控制为一个单独的对象。每个片段控制由速度矢量定义的自己的运动。第一个段独立于控制其余段的数组。所以头部只是将命令发送到第一个段,而第一个段将它传输到其余的段。
该系统基于 BendinPoints。基本上,每个段都有一个变量,用于存储 BendingPoint 坐标和到达该 BendingPoint 时要采用的速度矢量。一个标志告诉它它当前是否拥有一个 BendingPoint 或者可以自由地接受一个新的坐标。
因此,头部在转动时会传输它转动的位置和转动的方向(速度矢量)。第一段传递给第二段,第二段传递给第三段,以此类推。每个段只有在有一个要通过时才将一个订单传递给下一个。每个部分只有在旧订单完成后才会收到新订单。
现在我的问题有两个。一:这在 Java 模式下工作正常,但在 Javascript 模式下不工作,我不知道为什么。第二:有时,当我改变方向太快时,除了头部和第一节之外,身体的其他部分似乎会迷失方向并四处走动。
我希望代码注释能解释其余部分。原谅我的新手。
String MODE;
Menu menu;
String[] menuItems={"START","INSTRUCTIONS","CREDITS","EXIT"};
/*@ pjs font="data/waved.ttf" */
/*@ pjs font="data/sixty.ttf" */
PFont sMenu=createFont("waved",72);
PFont sItem=createFont("sixty",35);
String gOverText="GAME OVER";
String hScoreText="Your score is: ";
String iControl="W,A,S,D turns the Snake in the respective direction.";
String iScore="Each Food increases Score by 1 and a segement is added.";
String iScore2="After every 10 points, number of segments added per Food increases by 1.";
String cBy="coded By";
String cName="Le Shaun";
MenuItem back;
Snake snk;
Food fd;
int hScore;
int dF;
float sWidth=800;
float sHeight=600;
PVector sLoc=new PVector(sWidth/2,sHeight/2);
PVector sVel=new PVector(0,-1);
float sRad=10;
color sCol=#9D6C0A;
PVector fLoc=new PVector(450,300);
float fRad=10;
color fCol=#FCF18C;
void setup(){
size(int(sWidth),int(sHeight));
snk=new Snake(sLoc,sVel,sRad,sCol);
fd=new Food(fLoc,fRad,fCol);
frameRate(60);
hScore=0;
dF=1;
menu=new Menu("SNAKE",menuItems,sMenu,sItem,color(#9D6C0A),color(#8CC610),color(#EDE724),color(#674707),color(255,0));
MODE="NIL";
back=new MenuItem("BACK",sItem,width/2,height/1.5,height/25,color(#8CC610),color(#EDE724),color(#674707),color(255,0)); //Common back button for some of the screens.
}
//Current screen is controlled by MODES. Each MODE defines which parts of the game will run, whether it be individual screens or the main gameplay itself.
void draw(){
background(#EDB824);
if(MODE.equals("NIL")){
menu.render();
MODE=menu.whichItem();
}
else if(MODE.equals("START")){
fd.render();
if(fd.isEaten(snk)){
for(int i=1;i<=dF;i++){
snk.sInc();
}
hScore++;
}
snk.render();
snk.update();
if(snk.isDead()){
MODE="GAMEOVER";
sLoc=new PVector(width/2,height/2);
fLoc=new PVector(width/2+100,height/2+100);
sVel=new PVector(+1,0);
snk=new Snake(sLoc,sVel,sRad,sCol);
}
dF=int(hScore/10)+1;
textFont(sItem);
textSize(height/25);
text(str(hScore),width-textWidth(str(hScore))*3,height-height/25);
}
else if(MODE.equals("GAMEOVER")){
stroke(0);
fill(#9D6C0A);
textFont(sMenu);
textSize(72);
text(gOverText,width/2-textWidth(gOverText)/2,height/3);
text(hScoreText+hScore,width/2-textWidth(gOverText)/2,height/2);
back.render();
back.update();
if(back.getClicked()){
back.unClick();
MODE="NIL";
hScore=0;
frameRate(60);
}
}
else if(MODE.equals("INSTRUCTIONS")){
stroke(0);
fill(#9D6C0A);
textFont(sMenu);
textSize(72);
text("INSTRUCTIONS",width/2-textWidth("INSTRUCTIONS")/2,height/3);
textFont(sItem);
textSize(20);
text(iControl,width/2-textWidth(iControl)/2,height/2);
text(iScore,width/2-textWidth(iScore)/2,height/2+35);
text(iScore2,width/2-textWidth(iScore2)/2,height/2+70);
back.render();
back.update();
if(back.getClicked()){
back.unClick();
MODE="NIL";
}
}
else if(MODE.equals("CREDITS")){
stroke(0);
fill(#9D6C0A);
textFont(sItem);
textSize(35);
text(cBy,width/2-textWidth(cBy)/2,height/2);
textSize(45);
text(cName,width/2-textWidth(cName)/2,height/1.7);
back.render();
back.update();
if(back.getClicked()){
back.unClick();
MODE="NIL";
}
}
//println(MODE);
}
void keyReleased(){
if(MODE.equals("START")){
String temp="";
temp+=key;
temp=temp.toUpperCase();
snk.changeDir(temp);
if(key=='v' || key=='V'){
frameRate(60);
}
}
}
void keyPressed(){
if(MODE.equals("START")){
if(key=='v' || key=='V'){
frameRate(180);
}
}
}
void mouseClicked(){
if(MODE.equals("NIL")){
menu.passTo(mouseX,mouseY);
}
if(MODE.equals("GAMEOVER") || MODE.equals("INSTRUCTIONS") || MODE.equals("CREDITS")){
back.mClicked(mouseX,mouseY);
}
}
//Menu class uses the objects from the MenuItem and forms a menu with a title and a list of MenuItem objects.
/*
Constructor: Str-MenuTitle, Str[]-MenuItems, PF-MenuFont, PF-MenuItemFont, c-TitleColor, c-ItemTextColor, c-ItemBackColor, c-ItemHoverTextColor, c-ItemHoverBackColor.
Methods:
void render() - Renders the MenuTitle and the MenuItems.
void passTo(float,float) - Passes the mouse coords to each MenuItem to check whether it has been clicked.
void passTo(int) - Resets the clickState of the specified MenuItem by calling the unClick() method on that MenuItem.
String whichItem() - Checks all the MenuItems for a their clickState and returns the one that's been clicked.
*/
class Menu{
String titleT;
PFont titleF;
PFont menuItem;
color titleC;
float spacer; //This is used to define the space between successive MenuItem objects.
float iniY=height/2.5;
MenuItem[] menuItems;
Menu(String titleT,String[] menuItemsNames,PFont titleF,PFont menuItemF,color titleC,color menuItemC,color menuBackC,color itemHoverC,color backHoverC){
this.titleT=titleT;
this.titleF=titleF;
this.titleC=titleC;
menuItems=new MenuItem[menuItemsNames.length]; //Initializes the MenuItem objects depending on the array passed to it. This makes the menu system very flexible.
spacer=48;
for(int i=0;i<menuItemsNames.length;i++){
menuItems[i]=new MenuItem(menuItemsNames[i],menuItemF,width/2,iniY+(spacer*i),height/25,menuItemC,menuBackC,itemHoverC,backHoverC);
}
}
void render(){ //Renders the menu.
textFont(titleF);
textSize(92);
fill(titleC);
text(titleT,width/2-(textWidth(titleT)/2),height/3.8);
for(int i=0;i<menuItems.length;i++){
menuItems[i].update();
menuItems[i].render();
}
}
void passTo(float mX,float mY){ //This accepts the X,Y mouse coords when the mouse is clicked and passes it to the relevant MenuItem object to check if the click occurs on that object.
for(int i=0;i<menuItems.length;i++){
menuItems[i].mClicked(mX,mY);
}
}
/*void passTo(int item){ //This accepts an ineteger value and resets that particular menu item's click state.
menuItems[item].unClick();
}*/
String whichItem(){ //Checks each time if the clickState of any MenuItem object is true. If it is, returns the array position of the relevant object.
for(int i=0;i<menuItems.length;i++){
if(menuItems[i].getClicked()){
menuItems[i].unClick();
return menuItems[i].menuItem;
}
}
return "NIL";
}
}
//MenuItem holds the attributes and methods relating to each single item on the menu. Thus each item is treated as a separate object.
//Each MenuItem object comprises mainly of a foreground text and a background object.
class MenuItem{
String menuItem;
PFont menuFont;
float itemX;
float itemY;
float itemSize;
color itemColor;
color backColor;
color pressedColor;
color pressedBack;
color presentItem;
color presentBack;
float textWidth;
boolean clickState=false; //This vairable is used to check the clickState of the menu item. If the mouse is clicked over the menu item, this variable becomes true.
MenuItem(String menuItem,PFont menuFont,float itemX,float itemY,float itemSize,color itemColor,color backColor,color pressedColor,color pressedBack){
this.menuItem=menuItem;
this.menuFont=menuFont;
this.itemX=itemX;
this.itemY=itemY;
this.itemSize=itemSize;
this.itemColor=itemColor;
this.backColor=backColor;
this.pressedColor=pressedColor;
this.pressedBack=pressedBack;
}
void render(){ //Handles the rendering for individual menu objects.
textFont(menuFont);
textSize(itemSize);
textWidth=textWidth(menuItem);
stroke(0);
fill(presentBack);
rectMode(CENTER);
rect(itemX,itemY,textWidth*1.3,itemSize*1.4,50);
fill(presentItem);
text(menuItem,itemX-textWidth/2,itemY+itemSize*.3);
}
void update(){ //Constatnly checks for the state of the object. If the mouse is over it a certain style is show and otherwise another style is shown.
if(mouseX<(itemX+(textWidth*1.3)/2) && mouseX>(itemX-(textWidth*1.3)/2) && mouseY<(itemY+(itemSize*1.4)/2) && mouseY>(itemY-(itemSize*1.4)/2)){
presentItem=pressedColor;
presentBack=pressedBack;
noStroke();
}
else{
presentItem=itemColor;
presentBack=backColor;
}
}
boolean getClicked(){ //Returns the clickState of the object.
return clickState;
}
void unClick(){ //Resets the click state after having been clicked once.
clickState=false;
}
void mClicked(float mX,float mY){ //Changes the clickState of the object depending on the position of the mouse as inputs.
if(mX<(itemX+(textWidth*1.3)/2) && mX>(itemX-(textWidth*1.3)/2) && mY<(itemY+(itemSize*1.4)/2) && mY>(itemY-(itemSize*1.4)/2)){
clickState=true;
println(menuItem);
}
}
}
/*
All control comes from the Snake's head. The head works directly with the first segment(SnakeBits object) and the first segement works with the rest of the body.
Each time a food is consumed, a new segment is created, it's position and velocity calculated as per the position of the last segment.
A loop checks whether each segment is open to receiving a new set of orders(BendingPoint and the velocity for that point), and passes on if so.
*/
class Snake{ //Controls the snake's head as well as the segment objects.
PVector sLoc; //Location and Velocity.
PVector sVel;
float sRad; //Radius and Color
float shRad;
color sCol;
float baseVel; //The base velocity of the snake.
SnakeBits[] sBits={}; //Array of SnakeBits objects that forms the segments.
PVector hold;
Snake(PVector sLoc,PVector sVel,float sRad,color sCol){
this.sLoc=sLoc;
this.sVel=sVel;
this.sRad=sRad;
this.shRad=sRad*1.;
this.sCol=sCol;
this.baseVel=abs(sVel.x>0 ? sVel.x : sVel.y); //The snake is initially given a vector in one of the cardinal directions. Whatever the value of velocity is in either direction is stored.
hold=PVector.mult(sVel,shRad+sRad);
hold=PVector.sub(sLoc,hold);
sBits=(SnakeBits[])append(sBits,new SnakeBits(hold,sVel,sRad,sCol));
}
void update(){ //Updates the movement of the head as well as the segments.
updateBP();
sLoc.add(sVel);
for(int i=0;i<sBits.length;i++){
sBits[i].update();
}
}
void render(){ //The display.
stroke(0);
fill(sCol);
ellipse(sLoc.x,sLoc.y,shRad*2.2,shRad*2.2);
for(int i=0;i<sBits.length;i++){
sBits[i].render();
}
}
void sInc(){ //Gets called each time a food item is eaten, and increases the size of the snake by adding segments based on the velocity vector of the last segment.
int lastInd=sBits.length-1;
hold=PVector.mult(sBits[lastInd].sbVel,sRad*2);
hold=PVector.sub(sBits[lastInd].sbLoc,hold);
PVector appVel=new PVector(sBits[lastInd].sbVel.x,sBits[lastInd].sbVel.y);
SnakeBits appBits=new SnakeBits(hold,appVel,sRad,sCol);
sBits=(SnakeBits[])append(sBits,appBits);
}
void changeDir(String dir){ //Gets called when a directional button is pressed.
PVector chng=new PVector(0,0); //Direction change can only occur perpendicular to the current direction. Uses baseVel to set the new direction.
if(!sBits[0].hasBP){
if(degrees(sVel.heading())==0 || degrees(sVel.heading())==180){
if(dir.equals("W")){
chng=new PVector(0,-baseVel);
sVel=chng;
updateFBP();
}
else if(dir.equals("S")){
chng=new PVector(0,baseVel);
sVel=chng;
updateFBP();
}
}
else if(degrees(sVel.heading())==90 || degrees(sVel.heading())==-90){
if(dir.equals("D")){
chng=new PVector(baseVel,0);
sVel=chng;
updateFBP();
}
else if(dir.equals("A")){
chng=new PVector(-baseVel,0);
sVel=chng;
updateFBP();
}
}
}
}
boolean isDead(){ //Checks for collision against the wall or it's own tail.
if((sLoc.x-shRad)<0 || (sLoc.x+shRad)>width || (sLoc.y-shRad)<0 || (sLoc.y+shRad)>height){
println("WALL");
return true;
}
PVector temp;
for(int i=0;i<sBits.length;i++){
if(dist(sLoc.x,sLoc.y,sBits[i].sbLoc.x,sBits[i].sbLoc.y)<(shRad+sRad-sRad*.6)){
println("TAIL");
println(sLoc.x+" "+sLoc.y+" "+sBits[i].sbLoc.x+" "+sBits[i].sbLoc.y+" "+dist(sLoc.x,sLoc.y,sBits[i].sbLoc.x,sBits[i].sbLoc.y)+" "+(shRad+sRad-sRad*.6));
return true;
}
}
return false;
}
void updateFBP(){ //Updates the first segment's BendingPoint.
sBits[0].takeNewBP(sLoc,sVel);
sBits[0].hasNewBP(true);
}
void updateBP(){ //Updates the rest of the segments as per the system of receiving new orders once the current orders have been executed.
for(int i=0;i<sBits.length-1;i++){
if(sBits[i].hasBP && !sBits[i+1].hasBP){
sBits[i+1].takeNewBP(sBits[i].newBP,sBits[i].newVel);
sBits[i+1].hasNewBP(true);
}
}
}
}
/*
Each SnakeBit has it's independent movement system. It holds a BendPoint(newBP) variable, a New BP Velocity(newVel) variable and a flag(hasBP) to show whether it has a new Bend Point.
When the SnakeBit already has a BP, it will wait till it reaches that BP and then take on the velocity from newVel. It's flag will be set to false.
In this state it will be open to receiving a new set of orders: a new BP and the velocity to take on for that BP. Thus new BP's are not taken on till the previous BP has been cleared.
*/
class SnakeBits{ //The individual bits of the snake that make up its body.
boolean hasBP;
PVector sbLoc; //Location and Velocity vectors.
PVector sbVel;
float sbRad; //Radius and color of the segment.
color sbCol;
PVector newBP; //This works with the changeDir() method. It holds the position at which the direction will be changed.
PVector newVel; //Stores the new Velocity vector that will be applied when the above position is reached.
SnakeBits(PVector sbLoc,PVector sbVel,float sbRad,color sbCol){
this.sbLoc=sbLoc;
this.sbVel=sbVel;
this.sbRad=sbRad;
this.sbCol=sbCol;
newVel=new PVector(sbVel.x,sbVel.y);
newBP=new PVector(width*2,height*2); //Initialized it as such to avoid problems during first run.
hasBP=false;
}
void render(){
stroke(0);
fill(sbCol);
ellipse(sbLoc.x,sbLoc.y,sbRad*2,sbRad*2);
}
void update(){
sbLoc.add(sbVel); //Both updates the Location and checks if it's time to change direction.
changeDir();
}
void changeDir(){
if(sbLoc.x==newBP.x && sbLoc.y==newBP.y && hasBP){ //As soon as the segment reaches the Location where a change in dir is needed, the Velocity is changed over to the new velocity vector.
println("FTRUE");
hasNewBP(false);
sbVel.x=newVel.x; sbVel.y=newVel.y;
newBP=new PVector(width*2,height*2);
}
}
void takeNewBP(PVector pos,PVector vel){ //Called externally by the Snake class. Takes where last segment changed direction and stores that location as well as the new velocity vector.
newBP.x=pos.x; newBP.y=pos.y;
newVel.x=vel.x; newVel.y=vel.y;
}
void hasNewBP(boolean dat){ //Updates the hasBP state by accepting a boolean and assigning it to hasBP.
hasBP=dat;
}
}
class Food{
PVector fLoc;
float fRad;
color fCol;
Food(PVector fLoc,float fRad,color fCol){
this.fLoc=fLoc;
this.fRad=fRad;
this.fCol=fCol;
}
void render(){
stroke(0);
fill(fCol);
ellipse(fLoc.x,fLoc.y,fRad*2,fRad*2);
}
boolean isEaten(Snake sn){
PVector temp;
temp=PVector.sub(fLoc,sn.sLoc);
if(temp.mag()<(sn.shRad+fRad)){
reset(sn);
return true;
}
return false;
}
void reset(Snake sn){
boolean set=false;
PVector tmp=new PVector();
while(!set){
tmp=new PVector(random(fRad,width-fRad),random(fRad,height-fRad));
set=true;
for(int i=0;i<sn.sBits.length;i++){
if(dist(tmp.x,tmp.y,sn.sBits[i].sbLoc.x,sn.sBits[i].sbLoc.y)<(fRad+sn.sRad) || dist(tmp.x,tmp.y,sn.sLoc.x,sn.sLoc.y)<(fRad+sn.shRad)){
set=false;
break;
}
}
}
fLoc=tmp;
}
}