Search code examples
pythonpygamegame-physicsgame-development

Calculate X and Y velocity to hit the target on flat 2d surface in pygame


I am creating a game using Python and Pygame.

I have used this function before using JS with p5.js and Java with libwjgl, but for some reason, it does not work with Pygame.

I'm trying to hit a static target shot from a moving object. Now I'm missing the target. You can see it in the gif. Every projectile should hit the target (now everything misses, but shots in the right direction)

The bullet is fired once player clicks mouse button.

This is how I am doing this:

def shoot(self):
  posX, posY = 100, 100 # Static target coordinates

  diffX = self.player.x - posX               
  diffY = self.player.y - posY                 

  distance = math.sqrt((diffX * diffX) + (diffY * diffY)) # Calculate the distance

  velX = float((-1 / distance * diffX * 6)) # Calculate volocity required to hit the target
  velY = float((-1 / distance * diffY * 6)) # Calculate volocity required to hit the target 

  # Bullet(x, y, width, height, velocityX, velocityY)
  # Will be rendered on the screen
  self.bullets.append(Bullet(self.player.x, self.player.y, 10, 10,  velX, velY)) # The bullet is added to array, which later gets rendered on the screen

Bullet class:

import pygame

class Bullet(object):
  def __init__(self, x, y, width, height, velY, velX):
    self.width = width
    self.height = height
    self.x = x
    self.y = y
    self.velX = velX
    self.velY  = velY
    self.bullet = pygame.Rect(self.x, self.y, self.width, self.height)


  def render(self, screen):
    pygame.draw.rect(screen, (255, 255, 255), self.bullet)

  def update(self):
    self.bullet.x += self.velX
    self.bullet.y += self.velY

This works perfectly with other languages I have mentioned, but in Python the launched projectile is off...

Here is how it looks. The red square is the target:

THANK YOU, EVERYONE, FOR THE HELP. I really appreciate it :)

EDIT: FULL GAME CODE

import pygame
from PodSixNet.Connection import connection, ConnectionListener
from objects.Button import Button
from time import sleep
from STATE import STATE
import sys, os
from objects.Text import Text
from resources.hubColours import ColoursClass
from pathlib import Path
from objects.Bullet import Bullet
import math

class DefenseGame(ConnectionListener):
def __init__(self, hub, width, height, soundController, fontController, fps=60):
    #Hub
    self.hub = hub

    #COLOURS
    self.color = ColoursClass("alt")

    #SOUNDS
    self.soundController = soundController

    #SCREEN
    self.width = width
    self.height = height
    self.background = pygame.Surface((self.width, self.height), pygame.HWSURFACE | pygame.DOUBLEBUF).convert()  
    self.background.fill(self.color.getColour(7)) # fill background white

    #INFO
    self.fps = fps
    self.playtime = 0.0

    #FONTS
    self.fontController = fontController

    #STATE
    # All player related stuff is stored in the HUB.
    # TODO:
    # Player class
    self.isRunning = True


    self.moveLeft = False
    self.moveRight = False
    self.moveUp = False
    self.moveDown = False

    self.bullets = []

    self.moveAmnt = 5

    self.timeLeft = "Waiting for players..."


    self.clock = pygame.time.Clock()

    #OBJECTS
    self.on_init()

#Initialize game objects
def on_init(self):
  self.char = pygame.transform.smoothscale(pygame.image.load(str(Path("character.png"))), (50, 50))

# Draws on screen
def render(self, screen):                               # Update all objects (through object handler)                                            # This will update the contents of the entire display
    screen.blit(self.background, (0, 0))
    self.drawInfoText("Color: " + self.hub.playerColorName, 10, 10, self.fontController.tiny, screen)
    self.drawInfoText("Player Count: " + str(self.hub.getInGamePlayerCount()), 10, 20, self.fontController.tiny, screen)
    self.drawInfoText("FPS: " + str(round(self.getFPS(), 2)), 10, 30, self.fontController.tiny, screen)
    self.drawInfoText("Network Status: " + self.hub.statusLabel, 10, 40, self.fontController.tiny, screen)
    self.player.render(screen)
    self.player2.render(screen)
    # screen.blit(self.char, (self.posX, self.posY))

    if len(self.bullets) > 0:
      for b in self.bullets:
        b.render(screen)

def onMove(self):
  connection.Send({"action": "playerMove", 'x': self.player.x , 'y': self.player.y, 'id': self.hub.playerId})


def shoot(self):
  posX, posY = pygame.mouse.get_pos()

  diffX = self.player.x - posX               
  diffY = self.player.y - posY                 

  distance = math.sqrt((diffX * diffX) + (diffY * diffY))
  print("DISTANCE: ", distance)

  velX = float((-1 / distance * diffX * 20))
  velY = float((-1 / distance * diffY * 20))  

  print(velX, velY)

  self.bullets.append(Bullet(self.player.x, self.player.y, 10, 10,  velX, velY))

# Looks for events
def handle_events(self, events):
    for event in events:
        if event.type == pygame.QUIT: 
            self.exitGame(event)
            self.hub.setState(STATE.Home)
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                self.exitGame(event)
                self.hub.setState(STATE.SelectGame)

            if event.key == pygame.K_w:
              self.moveUp = True
            if event.key == pygame.K_a:
              self.moveLeft = True
            if event.key == pygame.K_s:
              self.moveDown = True
            if event.key == pygame.K_d:
              self.moveRight = True

        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_w:
              self.moveUp = False
            if event.key == pygame.K_a:
              self.moveLeft = False
            if event.key == pygame.K_s:
              self.moveDown = False
            if event.key == pygame.K_d:
              self.moveRight = False

        elif event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:          # Mouse first button
                self.shoot()

# Update stuff (loop)
def update(self):
    self.Pump()                 # Connection
    connection.Pump()           # Connection

    timer = self.clock.tick(self.fps)
    if self.moveLeft:
      self.player.x -= timer
      self.onMove()
    if self.moveRight:
      self.player.x += timer
      self.onMove()
    if self.moveUp:
      self.player.y -= timer
      self.onMove()
    if self.moveDown:
      self.player.y += timer
      self.onMove()

    if len(self.bullets) > 0:
      for b in self.bullets:
        b.update()
    # sleep(0.001)


# returns FPS
def getFPS(self):
    self.clock.tick(self.fps)
    return  self.clock.get_fps()

# Helper for quickly drawing on screen
def drawInfoText(self, text, x, y, font, screen):
    surface = font.render(str(text), 1, self.color.getColour(8))
    screen.blit(surface, (x, y))

# Exits the game to home
def exitGame(self, e):
    self.soundController.stopSoundEffects()
    connection.Send({"action":"exitGame", "isInGame": False})
    self.hub.isInGame = False

Solution

  • self.bullet is a pygame.Rect object. The position of the bullet is tracked by the position of the rectangle (self.bullet.x, self.bullet.y). Since the rectangle position is integral this causes an inaccuracy each time when the position is updated.

    Use floating point data to keep track of the position. Use c.

    Store the position of the bullet and the velocity to a pygame.math.Vector2 object.

    class Bullet(object):
        def __init__(self, x, y, width, height, velX, velY):
            self.width = width
            self.height = height
            self.x = x
            self.y = y
            self.bullet = pygame.Rect(self.x, self.y, self.width, self.height)
            self.pos = pygame.math.Vector2(self.bullet.center)
            self.vel = pygame.math.Vector2(velX, velY)
    

    Update the position attribute instead of the rectangle position:

    class Bullet(object):
    
        # [...]
    
        def update(self):
            self.pos = self.pos + self.vel
    

    Update the position of the rectangle before drawing the bullet:

    class Bullet(object):
    
        # [...]
    
        def render(self, screen):
            self.bullet.center = (int(self.pos[0]), int(self.pos[1]))
            pygame.draw.rect(screen, (255, 255, 255), self.bullet)
    

    Use pygame.math.Vector2 to calculate the velocity:

    def shoot(self):
        posX, posY = 100, 100 # Static target coordinates
    
        traget = pygame.math.Vector2(posX, posY)
        start  = pygame.math.Vector2(self.player.x, self.player.y)
    
        delta = traget - start
        distance = delta.length() 
        direction = delta.normalize()
    
        vel = direction * 6
        self.bullets.append(Bullet(self.player.x, self.player.y, 10, 10, vel[0], vel[1]))