You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实现从定点向鼠标位置发射抛射物的JavaScript技术求助

实现鼠标精准控制炮弹发射目标位置的解决方案

我正在开发一个大炮发射炮弹的项目,目前已完成具备重力效果和地面弹跳功能的炮弹绘制代码,现希望实现通过鼠标精准控制炮弹的发射目标位置。以下是我正在使用的JavaScript代码:

const canvas = document.querySelector('canvas')
const c = canvas.getContext('2d')

canvas.width = innerWidth
canvas.height = innerHeight

// const mouse = {
//   x: 0,
//   y: 0
// }
const mouse = {
x: innerWidth / 2,
y: innerHeight / 2
}
const center = {x: canvas.width / 2,
y: canvas.height / 2}

// Event Listeners
addEventListener('mousemove', (event) => {
mouse.x = event.clientX
mouse.y = event.clientY - canvas.height

angle = Math.atan2(mouse.y, mouse.x)

//let distBetween = function(p1, p2){
//return Math.sqrt(Math.pow((p2.x - p1.x), 2) + Math.pow((p2.y - p1.y), 2));
//}

//let angleBetween = function(p1, p2){
//return Math.atan2(p2.y - p1.y, p2.x - p1.x)
//}
//})

addEventListener('resize', () => {
canvas.width = innerWidth
canvas.height = innerHeight

init()
})
let angle = 0

// Objects
class Particle {
constructor(x, y, radius, color, distanceFromCenter) {
this.x = x
this.y = y
this.radius = radius
this.color = color
this.distanceFromCenter = distanceFromCenter
}

    draw() {
        c.beginPath()
        c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false)
        c.fillStyle = this.color
        c.fill()
        c.closePath()
    }
    
    
    update() {
        this.draw()
        this.x = (0) + (this.distanceFromCenter + 30) * Math.cos(angle)
        this.y = canvas.height + (this.distanceFromCenter +30) * Math.sin(angle)
    }

}
class Cannonball {
constructor(x, y, radius, color){
this.x = x
this.y = y
this.radius = radius
this.color = color
this.dx = Math.cos(angle) * 2;
this.dy = Math.sin(angle) * 2;
this.velocity = 2
this.gravity = 1
this.friction = -0.75
}
draw(){
c.beginPath()
c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false)
c.fillStyle = 'blue'
c.fill()
c.closePath()
}

    update(){
        if (this.y + this.radius + 5 > canvas.height){
            this.velocity = -this.velocity * this.gravity
        } else {
          this.velocity += this.gravity
        }
        
        this.y += this.velocity
        this.velocity += this.friction 
        this.draw()
    }

}

// let cannonball = new Cannonball(center.x, center.y, 30, 'blue')

// particle Implementation
let particleCount = 400
let particles;

// let cannonballCount = 400
let cannonballs;

function init() {
particles = []
for (let i = 0; i < particleCount; i++) {
const x = 0 + i * Math.cos(1)
const y = canvas.height + this.distanceFromCenter * Math.sin()
particles.push(new Particle(x, y, 30, 'red', i))
}

cannonballs = []
}

// Animation Loop
function animate() {
requestAnimationFrame(animate)

    c.clearRect(0, 0, canvas.width, canvas.height)
    
    particles.forEach((particle) => {
        particle.update()
    })
    cannonballs.forEach(cannonball => {
        cannonball.update()
       })

}

init()
animate()

addEventListener('click', (event) => {
mouse.x = event.clientX
mouse.y = event.clientY

    cannonballs.push(new Cannonball(mouse.x, mouse.y, 30, 'blue',  {
        x: Math.cos(angleIncrement * i),
        y: Math.sin(angleIncrement * i)
    })

}

核心修改方案

1. 修正鼠标位置与角度计算

原代码中鼠标Y轴计算错误,且角度未基于大炮位置计算,导致指向偏差。调整后:

  • 鼠标位置直接取event.clientX和event.clientY
  • 基于大炮固定位置(画布底部中心)到鼠标的向量计算发射角度

2. 重构炮弹运动逻辑

原炮弹类的velocity与dx/dy混用,运动逻辑混乱。改为拆分水平/垂直速度分量,重力仅作用于垂直方向,同时添加合理的弹跳摩擦力和空气阻力。

3. 调整发射触发逻辑

点击时从大炮位置发射炮弹,而非鼠标位置,初速度由计算出的角度和固定速度(可调整)生成。

修改后的完整代码

const canvas = document.querySelector('canvas')
const c = canvas.getContext('2d')

canvas.width = innerWidth
canvas.height = innerHeight

const mouse = {
  x: innerWidth / 2,
  y: innerHeight / 2
}
// 大炮固定在画布底部中心
const cannonPos = {
  x: canvas.width / 2,
  y: canvas.height - 30
}

let angle = 0

// 事件监听
addEventListener('mousemove', (event) => {
  mouse.x = event.clientX
  mouse.y = event.clientY
  // 计算大炮到鼠标的角度(画布坐标系Y轴向下为正)
  angle = Math.atan2(mouse.y - cannonPos.y, mouse.x - cannonPos.x)
})

addEventListener('resize', () => {
  canvas.width = innerWidth
  canvas.height = innerHeight
  // 窗口 resize 时重新设置大炮位置
  cannonPos.x = canvas.width / 2
  cannonPos.y = canvas.height - 30
  init()
})

// 粒子类(作为大炮的视觉指示器)
class Particle {
  constructor(x, y, radius, color, distanceFromCenter) {
    this.x = x
    this.y = y
    this.radius = radius
    this.color = color
    this.distanceFromCenter = distanceFromCenter
  }

  draw() {
    c.beginPath()
    c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false)
    c.fillStyle = this.color
    c.fill()
    c.closePath()
  }

  update() {
    this.draw()
    this.x = cannonPos.x + (this.distanceFromCenter + 30) * Math.cos(angle)
    this.y = cannonPos.y + (this.distanceFromCenter + 30) * Math.sin(angle)
  }
}

// 重写炮弹类,优化运动逻辑
class Cannonball {
  constructor(x, y, radius, color, angle, speed) {
    this.x = x
    this.y = y
    this.radius = radius
    this.color = color
    // 根据发射角度计算初速度分量
    this.dx = Math.cos(angle) * speed
    this.dy = Math.sin(angle) * speed
    this.gravity = 0.5
    this.bounceFriction = 0.7 // 弹跳时的垂直速度衰减
    this.airResistance = 0.995 // 水平方向空气阻力
  }

  draw() {
    c.beginPath()
    c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false)
    c.fillStyle = this.color
    c.fill()
    c.closePath()
  }

  update() {
    // 应用空气阻力
    this.dx *= this.airResistance
    // 应用重力
    this.dy += this.gravity

    // 地面碰撞检测与弹跳处理
    if (this.y + this.radius >= canvas.height) {
      this.y = canvas.height - this.radius
      this.dy = -this.dy * this.bounceFriction
    }

    // 更新炮弹位置
    this.x += this.dx
    this.y += this.dy

    this.draw()
  }
}

let particleCount = 400
let particles;
let cannonballs;

function init() {
  particles = []
  for (let i = 0; i < particleCount; i++) {
    particles.push(new Particle(cannonPos.x, cannonPos.y, 3, 'red', i))
  }
  cannonballs = []
}

// 动画循环
function animate() {
  requestAnimationFrame(animate)
  c.clearRect(0, 0, canvas.width, canvas.height)
  
  // 绘制大炮底座
  c.fillStyle = 'gray'
  c.fillRect(cannonPos.x - 50, cannonPos.y, 100, 20)
  
  particles.forEach((particle) => {
    particle.update()
  })
  cannonballs.forEach(cannonball => {
    cannonball.update()
  })
}

init()
animate()

// 点击鼠标发射炮弹,速度设为20,可根据需求调整
addEventListener('click', () => {
  cannonballs.push(new Cannonball(cannonPos.x, cannonPos.y, 10, 'blue', angle, 20))
})

关键说明

  • 大炮固定在画布底部中心,添加了灰色底座增强视觉效果
  • 鼠标移动时,粒子组件会跟随角度变化,直观显示发射方向
  • 炮弹运动逻辑更符合物理规律,弹跳和减速效果自然
  • 点击鼠标即可从大炮位置向鼠标指向发射炮弹,精准度完全匹配鼠标位置

内容的提问来源于stack exchange,提问作者bg560581

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 13:50:18