Обнаружение столкновения работает только тогда, когда объект попадает прямо в центр - PullRequest
1 голос
/ 25 марта 2020

Привет, у меня здесь игра в стиле «падающий», где у меня голова Гомера Симпсона, падающая в небо, избегая салатов и собирая гамбургеры, чтобы увеличить счет. Я реализовал обнаружение столкновений, поэтому, когда Гомер попадает в салат, он должен вновь появиться и получить вычтенную жизнь, которая работает отлично. Однако, если голова Гомера ударяет в сторону салата, а не прямо в его центре, игра на мгновение останавливается, а затем продолжается, как будто ничего не произошло. Я не уверен, почему это происходит, и мне интересно, есть ли проблема с тем, как я это делаю. Вот мой код ниже:

Является ли обнаружение столкновений недостаточно точным или есть еще одна проблема, которую мне не хватает?

PImage background;
PImage MenuBackground;
int y=0;//global variable background location
final int End = 0;
final int Active = 1;
final int Menu = 2;
int gameMode = Menu;
int score = 0;
int lives = 3;
Boolean BurgerCollisionInProgress = false;
Boolean BurgerCollisionInProgress2 = false;


Salad salad1;
Salad salad2;
Salad salad3;
Homer user1;
Burger Burger;


public void settings()
{
 size(500,1000); //setup size of canvas
}

void menu()
{
 background = loadImage("spaceBackground.jpg"); //image used for background
 background.resize(500,1000); //resizes the background
 gameMode = Active; 

float rand = random(25,475);
int intRand = int(rand);

float rand2 = random(25,475);
int intRand2 = int(rand2); 

float rand3 = random(25,475);
int intRand3 = int(rand3); 

float rand4 = random(25,475);
int intRand4 = int(rand4); 


 user1 = new Homer(250,100);  //declares new defender as user1
 Burger = new Burger(intRand,900,2);
 salad1 = new Salad(intRand2,900,3);
 salad2 = new Salad(intRand3,900,3);
 salad3 = new Salad(intRand4,900,3); //3 aliens declared with their x and y position and their speed they move at
 draw();
}


void setup()
{
  if(gameMode == 2)
  {    
    MenuBackground = loadImage("simpMenu.png");
    MenuBackground.resize(540,1000);
    image(MenuBackground, 0, y);
    textAlign(CENTER);
    textSize(40);
    fill(252, 3, 3);
    text("Press 'p' to play", 250,500);     
  } 
}


void draw ()
{  
  if (gameMode == Active)
  {        
    if(crash() == false)
    {
      drawBackground();//calls the drawBackground method
      textSize(32);
      fill(22,100,8);
      text("Score: " + score,75,40); 
      text("Lives: " + lives,75,80);
      salad1.update();//calls the update method which holds the move and render methods for alien
      salad2.update();
      salad3.update();
      user1.render();//calls the update method which holds the move and render methods for user
      Burger.update();//calls the update method which holds the move and render methods for burger

      if(Bcrash() == true && BurgerCollisionInProgress == false)
      {
      score = score+1;
      BurgerCollisionInProgress = true;
      Burger.y = 900;
      float rand = random(25,475);
      int intRand = int(rand);
      Burger.x = intRand;
      }

      if(Bcrash() == false)
      {
      BurgerCollisionInProgress = false;
      }


      if(crash() == true && BurgerCollisionInProgress2 == false)
      {
        if (lives < 1)
        {   gameMode = End;
            textSize(28);
            fill(22,100,8);
            text("Game Over, press 'r' to restart",200,200);
        }
        else
        {
          lives = lives - 1;
          BurgerCollisionInProgress2 = true;
          menu();
        }
        if(crash() == false)
      {
      BurgerCollisionInProgress2 = false;
      }
     }      
    }
  }
}

void drawBackground()
{
 image(background, 0, y); //draw background twice adjacent
 image(background, 0, y-background.width);
 y -=2;
 if(y == -background.width)
 y=0; //wrap background
}


  boolean crash()
  {
    if(user1.crash(salad1))
    {
      return true;
    }
    if(user1.crash(salad2))
    {
      return true;
    }
    if(user1.crash(salad3))
    {     
      return true;
    }
    return false;
  }


  boolean Bcrash()
  {
    if(user1.crash(Burger))
    {      
      return true;
    }
    return false;
  }

Класс Гомера:

class Homer
{
  PImage UserImage;
  int x,y;  //declaring variables

  Homer(int x, int y)
  {
   this.x = x;
   this.y = y;
   UserImage = loadImage("homer.png");
   UserImage.resize (60, 52);   
  } // end of Homer

  void render()
  {
    //draw a Homer
 image(UserImage,x,y);

  } //end of void render

  boolean crash(Salad A)  
  {   
    if((abs(x-A.x)<=30) && abs(y-A.y)<=30)
    {
    return true;
    }
    return false;
  }// end of crash


  boolean crash(Burger A)
  {
    if((abs(x-A.x)<=30) && abs(y-A.y)<=30)
    {
    return true;
    }
    return false;
  }
} // end of class

Бургер Класс:

class Burger
{
  PImage burgerImage;
  int x,y, speedX;
  int speedY = 0;

  Burger(int x, int y, int speedY)
  {     
    this.x = x;
    this.y = y;
    this.speedY= speedY;
    burgerImage = loadImage("food.png");
    burgerImage.resize (60, 52);    
  }  

  void render()
  {
    image(burgerImage,x,y);
  }


   void move()
 {
  y = y - speedY;  
  float rand = random(25,475);
  int intRand = int(rand);
   if(this.y < 0)
   {
    this.y = 900;
    this.x = intRand;
   }  
}

void update()
{
 move();
 render();
}
 }

Салат Класс:

class Salad
{
 float x,y;
 float speedX, speedY; //declaring variables
 PImage saladImage;

 Salad(int x, int y, int speedY)
 {
  this.x = x;
  this.y = y;
  this.speedY = speedY;
  saladImage = loadImage("salad.png");
  saladImage.resize (60, 52);  
 } //end of salad

 void move()
 {
   y=y-speedY;
   float stepY = random(-5,5);
   y = y + (int)stepY;

   float rand = random(25,475);
   int intRand = int(rand);   

   if(this.y < 0)
   {
    this.y = 900; // once the salads y is less than 0 they restart at 900
    this.x = intRand;
    speedY = speedY + 0.5;
   }
 } //end of void move

 //draw a salad
 void render()
 {
image(saladImage,x,y);

 } //end of void render 

 void update()
 {
  move();
  render();
 }
}// end of alien class

1 Ответ

2 голосов
/ 26 марта 2020

Есть несколько мелочей, которые делают это сложнее, чем могло бы быть. Во-первых, ваш метод пересечения не совсем правильный. Затем способ обработки координат может быть улучшен.

Сначала я покажу вам, как пересекать прямоугольники. После этого я покажу вам, как бы я справлялся с нарисованными объектами, чтобы ими было легко манипулировать. Затем я покажу вам скелетный код для короткой и простой игры, в которой вещи падают и сталкиваются, и просто для вас я добавлю некоторую помощь, чтобы вы могли реализовать эти предложения в контексте вашей игры.



1. Столкновения

Существует много способов обработки столкновений. Большинство из них - прикладная математика, некоторые из них - умные алгоритмы, использующие цвета или невидимые спрайты. Вероятно, есть и методы, о которых я забыл.

Мы будем делать столкновения только между прямоугольниками, так как ваша программа выглядит достаточно удобной для прямоугольников, и это более простой метод. Поэтому мы напишем алгоритм обнаружения пересечений.

Первое, что нужно сделать при написании алгоритма, это псевдокод. Я не шучу. * * * * * * * * * * * * * * * * * * * * * * * * - все «клацки-клацкать» с вашей клавиатурой и нажмите «compile». Это работает большую часть времени ... но это более интуитивная логика c, чем применение вашего мозга к проблеме.

Возможность псевдокода подобна сверхдержаве для программистов. Никогда не стоит недооценивать.

Теперь, как вы узнаете, пересекаются ли два прямоугольника? Ответ:

  1. Существует 4 способа пересечения двух прямоугольников по горизонтали или по вертикали.
  2. Они должны пересекаться как по горизонтали, так и по вертикали, чтобы перекрываться по-настоящему.

Это возможности, которые вы должны искать:

4 ways two rectangles may intersect

  1. Красный прямоугольник больше черного прямоугольника, а черный прямоугольник полностью внутри него.
  2. Оба прямоугольника перекрываются с левой стороны (по горизонтали) или с верхней стороны (по вертикали).
  3. Красный прямоугольник достаточно мал, чтобы находиться внутри черного прямоугольника.
  4. Оба прямоугольника перекрываются с правой стороны (по горизонтали) или с нижней стороны (по вертикали).

Поскольку этот код можно использовать во многих местах, я вынул его из контекста и поместил внутри Функция, которая принимает координаты и возвращает логическое значение (true, если действительно есть столкновение):

//  INTERSECT RECTs
boolean intersect(float x1, float y1, float w1, float h1, float x2, float y2, float w2, float h2)
{
    boolean checkX = x1 < x2 && x1+w1 > x2 || x1 < x2+w2 && x1+w1 > x2+w2 || x1 > x2 && x1+w1 < x2+w2 || x1 < x2 && x1+w1 > x2+w2; 
    boolean checkY = y1 < y2 && y1+h1 > y2 || y1 < y2+h2 && y1+h1 > y2+h2 || y1 > y2 && y1+h1 < y2+h2 || y1 < y2 && y1+h1 > y2+h2;

    return checkX && checkY;
}

Это один из способов обработки столкновений между прямоугольниками. Вы можете взять эту информацию и применить ее к своей игре, и она будет раскачиваться.

Тем не менее, вы также можете улучшить свой код с помощью Inheritance ...



2. Наследование (в данном случае: для графических объектов)

Наследование в информатике - это способ заставить класс получить свойства другого. Большинство людей объясняет это термином «семья»: есть родительский класс и есть дочерний класс, который наследует свойства родительского класса.

Наследование особенно полезно, когда несколько ваших классов имеют одинаковые свойства или методы. Рисованные объекты являются отличным примером, потому что все они нуждаются в координатах. Всем им нужен метод для рисования.

Как вы увидите позже в примере с игрой, я заметил, что все мои прямоугольники нуждались в следующих модальных переменных:

protected float x, y, w, h; // x and y coordinate, width and height of the square
protected color fill, stroke;
protected float strokeWeight = 1;

Итак, я создал базовый класс с именем «Drawable». В более крупном проекте это может быть базовый класс целого дерева классов, например:

Inheritance!

Так что в этом примере Rat будет дитя Уокера, дитя врага, дитя актера, дитя Drawable.

Преимущество состоит в том, что каждый дитя наследует все от своего родителя. Это позволяет вам писать меньше кода и исправлять ошибки только в одном месте, а не везде. Например, если есть ошибка в том, как вы используете координаты ваших объектов, вы хотите исправить это в классе, где написано это логическое c, а не в каждом классе .

У Наследования есть много других преимуществ, но пока давайте сделаем все просто, хорошо?



3. Пример программы

Это очень просто: в этом примере используются как наследование, так и коллизии. Вы можете скопировать и вставить его в IDE обработки, и он запустится. Потратьте некоторое время, чтобы увидеть, как эти 3 класса связаны друг с другом и как каждый дочерний класс имеет модальные переменные и функции своего родителя.

Hero hero;
ArrayList<Bomb> bombs = new ArrayList<Bomb>();
int numberOfBombs = 20; // if you change this number the number of bombs will change too. Try it!
int hitCount = 0;

public void settings()
{
  size(800, 600); //setup size of canvas
}

public void setup() {
  hero = new Hero();

  for (int i = 0; i < numberOfBombs; i++) {
    bombs.add(new Bomb(random(20, width-20), random(1, 10)));
  }

  // This part serves no purpose but to demonstrate that you can gather objets which share a parent class together
  ArrayList<Drawable> myDrawables = new ArrayList<Drawable>();
  for (Bomb b : bombs) {
    myDrawables.add(b);
  }
  myDrawables.add(hero);

  for (Drawable d : myDrawables) {
    d.Render();
    // Even though hero and the bombs are different classes, they are in the same ArrayList because they share the Drawable parent class.
    // Drawable has the Render() function, which may be called, but the child class will overshadow the Drawable's method.
    // Proof is that the error message "Drawable child: Render() was not overshadowed." will not appear in the console.
  }
}


public void draw() {
  DrawBackground();

  hero.Update();
  hero.Render();

  for (Bomb b : bombs) {
    b.Update();
    b.Render();
  }

  ShowHitCount();
}


public void DrawBackground() {
  fill(0);
  stroke(0);
  rect(0, 0, width, height, 0); // dark background 
}

public void ShowHitCount() {
  textAlign (RIGHT);
  textSize(height/20);
  fill(color(200, 200, 0));
  text(hitCount, width-20, height/20 + 20);
}


class Drawable {
  protected float x, y, w, h; // 'protected' is like 'private', but child class retain access
  protected color fill, stroke;
  protected float strokeWeight = 1;

  Drawable() {
    this(0, 0, 0, 0);
  }

  Drawable(float x, float y, float w, float h) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
  }

  public void Render() { print("Drawable child: Render() was not overshadowed."); } // nothing to see here: this exists so we can overshadow it in the childs
  public void Update() { print("Drawable child: Update() was not overshadowed."); } // same thing
}


class Hero extends Drawable { // 'extends' is keyword for 'will inherit from'
  Hero() {
    // 'super()' calls the parent's constructor
    // in this example, I decided that the hero would be a red 40x60 rectangle that follows the mouse X position
    super(mouseX - 20, height - 80, 40, 60);

    fill = color(200, 0, 0);
    stroke = color(250);
  }

  public void Update() { // when both parents and child have the same function (type and signature), the child's one prevail. That's overshadowing.
    x = mouseX - w/2;
  }

  public void Render() {
    fill(fill);
    stroke(stroke);
    strokeWeight(strokeWeight);
    rect(x, y, w, h);
  }
}


class Bomb extends Drawable {
  protected float fallSpeed;

  Bomb(float xPosition, float fallSpeed) {
    // Bombs will be small blue squares falling from the sky
    super(xPosition, -20, 20, 20);
    this.fallSpeed = fallSpeed;

    fill = color(0, 0, 200);
    stroke = fill;
  }

  private void FallAgain() {
    x = random(20, width-20);
    fallSpeed = random(1, 10);
    y = 0 - random(20, 100);
  }

  public void Update() {
    y += fallSpeed;

    // check for collision with the Hero
    if (intersect(x, y, w, h, hero.x, hero.y, hero.w, hero.h)) {
      hitCount++;
      FallAgain();
    }

    // check if it fell lower than the screen
    if (y > height) {
      FallAgain();
    }
  }

  public void Render() {
    fill(fill);
    stroke(stroke);
    strokeWeight(strokeWeight);
    rect(x, y, w, h);
  }
}


//  INTERSECT RECTs
boolean intersect(float x1, float y1, float w1, float h1, float x2, float y2, float w2, float h2)
{
    boolean checkX = x1 < x2 && x1+w1 > x2 || x1 < x2+w2 && x1+w1 > x2+w2 || x1 > x2 && x1+w1 < x2+w2 || x1 < x2 && x1+w1 > x2+w2; 
    boolean checkY = y1 < y2 && y1+h1 > y2 || y1 < y2+h2 && y1+h1 > y2+h2 || y1 > y2 && y1+h1 < y2+h2 || x1 < y2 && y1+h1 > y2+h2;

    return checkX && checkY;
}


4. Бонус: помощь в реализации

Итак ... вы видите это, и это заставляет вас хотеть улучшить свою программу. Это хорошо. Может быть, вы хотите реализовать наследование, может быть, просто коллизии. Оба могут быть хитрыми, и ни один из них не должен влиять на пользователя.

Это то, что называется рефакторингом.

Давайте сначала реализуем класс Drawable. Остальное будет легче.

Первый шаг: найти общий язык с Бургером, Гомером и Салатом. Из кода, который вы разместили, я вижу, что им нужны эти вещи:

int x, y;
int speedX, speedY;
PImage img;
// To which I would add:
int w, h;
boolean isVisible;

Я заметил, что вы используете целые числа. Это нормально, но я настоятельно рекомендую использовать float для координат. Я делал то же самое, когда учился программировать, и в итоге пожалел, что раньше не использовал float. И integer, и float, вероятно, справятся с этим проектом (с некоторым приведением при необходимости).

Кроме того, здесь есть несколько общих функций:

void Render()
void Update()
void Move()
// To which I would add:
void SetPosition()
void SetIsVisible()
boolean Crash() // so we can check if it intersect with given coordinates

Пока что ваш Класс Drawable может выглядеть так:

class Drawable {
  public float x, y, w, h; // Making variables public while you could avoid it is bad practice, I'm doing it to avoid writing Get functions. Avoid doing this as much as possible, but bear with me for now.
  protected float speedX, speedY;
  protected PImage img;
  protected boolean isVisible = true;

  Drawable(float x, float y, float w, float h, String imagePath) {
    this.x = x; // starting x position
    this.y = y; // starting y position
    this.w = w; // width if the object (your image in this case)
    this.h = h; // height of the object (height of your image)

    if (imagePath.length() > 0) { // if there is nothing in the string it won't try to load an image
      img = loadImage(imagePath);
    }
  }

  public void Render() {
    if (isVisible && img != null) {
      image(img, x, y);
    }
  }

  public void Update() {
    Move(); // I kept Move() out of Update() so you can overshadow Update() without having to re-code Move() later
  }

  protected void Move() {
    // The 'normal' behavior of a Drawable would then to move according to it's speed.
    // You can then change how they move by changing their speed values.
    // Or... you can overshadow this function in a child class and write your own!
    x += speedX;
    y += speedY;
  }

  public void SetPosition(float x, float y) {
    this.x = x;
    this.y = y;
  }

  public void SetIsVisible(boolean isVisible) {
    this.isVisible = isVisible;
  }

  public boolean Crash(float x, float y, float w, float h) {
    // this function uses the 'intersect' function I wrote earlier, so it would have to be included in the project
    return intersect(this.x, this.y, this.w, this.h, x, y, w, h);
  }
}

Не так уж и плохо, не так ли? Это станет прочной основой для всех ваших объектов. Теперь давайте посмотрим, как реализовать это в существующем классе:

Гомер:

class Homer extends Drawable // give Homer the power of the Drawable class!
{
  Homer(float x, float y)
  {
    // I can read in the code that your image will be (60, 52), but you have to write the manipulation here
    super(x, y, 60, 52, "homer.png");
    img.resize (60, 52);   
  }

  public void Update() {
    // do Update stuff so Homer can move around
  }
}

Обратите внимание, насколько меньше этот класс сейчас, когда все вещи Drawable сдаются в другом месте.

Теперь, для класса Salad:

Сначала вы можете удалить глобальные переменные salad1, salad2, salad3. Мы поместим их в список, и вы сможете иметь больше или меньше их, если хотите (вы можете думать об этом как о возможности изменить настройку сложности):

int numberOfSalads = 3;
ArrayList<Salad> salads = new ArrayList<Salad>();

В том месте, где вы инициализируете салаты, вы можете инициализировать их в al oop:

for (int i=0; i<numberOfSalads; i++) {
  salads.add(new Salad(random(25,475), 900, 3);
}

Конечно, в класс Salad также будут внесены некоторые изменения:

class Salad extends Drawable {
  Salad(float x, float y, float speedY)
  {
    super(x, y, 60, 52, "salad.png");
    this.speedY = speedY; // Drawable will take it from here
    img.resize (60, 52);
  }

  protected void Move() // I knew this would come in handy!
  {
    // I have no idea what's going on, just re-writing your stuff
    y = y - speedY;
    y = y + random(-5, 5);

    if (this.y < 0)
    {
      this.y = 900; // once the salads y is less than 0 they restart at 900
      this.x = random(25, 475);
      speedY = speedY + 0.5;
    }
  }
}

Пока все хорошо. Есть много других мест, где вам придется адаптировать код, но вы должны заметить, что пока вы удалили больше строк, которые вы добавили. Это хорошая вещь. Поскольку ваш код легко читается, его укорочение означает, что меньше мест для поиска неприятных ошибок, которые нужно исправить.

Кроме того, когда вы избегаете повторять одни и те же строки (как и все эти идентичные функции рендеринга), располагая их все в одном месте (в данном случае класс Drawable), вы также избегаете необходимости выслеживать каждую итерацию вашего код, если вы хотите сделать одно изменение. Это называется DRY код. DRY (для Dont Repeat Yourself) код проще отладить и поддерживать. Как правило, каждый раз, когда вы копируете и вставляете код без каких-либо изменений, вы должны спросить себя, можете ли вы просто хранить эти строки в одном централизованном месте, будь то переменная, или функция, или класс.

Я дам вам код класса Бургер. Я думаю, что вы справитесь с этим сейчас, когда вы узнали, как обращаться с другими.

Теперь давайте посмотрим, как обновить ваш основной l oop, draw():

void draw ()
{
  // As a general rule, all your game states should be dealt in the game loop.
  // I like 'switch' statements for this kind of operations
  // Also, try not to clutter the game loop. If you have a lot of code here, you should probably put them into functions
  // it will make it easier to read and the game loop can very easily become a spaghetti nightmare if you're not careful.
  switch(gameMode) {
  case Menu:
    // Do Menu stuff
    break;
  case Active:
    drawBackground();  // Maybe this should be before the switch, I'm not sure how you want to deal with this

    // Updates
    user1.Update();
    burger.Update();
    for (Salad s : salads) {
      s.Update();
    }

    // Check for collisions
    // I may be mistaken but I think only the Homer can collide with stuff
    if (burger.Crash(user1.x, user1.y, user1.w, user1.h)) {
      // Do burger crash stuff
    }
    for (Salad s : salads) {
      if (s.Crash(user1.x, user1.y, user1.w, user1.h)) {
        // Do Salad crash stuff
      }
    }

    // Render
    user1.Render();
    burger.Render();
    for (Salad s : salads) {
      s.Render();
    }
    break;
  case End:
    // Do End stuff
    break;
  }
}

Это должно поставить вас на путь.

Если по какой-то причине вы хотите использовать только метод пересечения: помните, что ширина и высота ваших объектов - это та, которую вы используете для своих изображений.


У вас, вероятно, есть вопросы, не стесняйтесь спрашивать. И веселиться!

...