Ping Pong Reflection example
반사판의 각도에 따른 충돌체의 반사각도를 계산하는 예
이동 방향과 속도(크기)를 가진 물체의 궤적을 그리거나 충돌반응을 계산할 때는 수학적 도구인 벡터를 사용하는 것이 일반적일 것이다. 여기서는 명시적으로 벡터를 사용하지는 않으며 방향과 속도를 제어하기 위해 삼각함수를 사용하려고 한다.
JAVA에서 삼각함수를 이용할 때의 각도 체계는 아래의 그림과 같다 (3시 방향에서 시작하여 시계방향으로 각도가 증가한다)
위의 그림이 의미하는 것을 이해하기 위한 좋은 방법은, 화면의 중앙( JPanel 등의 중앙 )에서 45도 각도로 직선을 그리는 프로그램을 작성해보는 것이다. 위의 그림에서 중심점은 직선을 그리기 시작하는 원점에 해당하고 만약 원점으로부터 45도 직선을 그린다면 그 직선은 원점으로부터 우측 아래 방향을 향하여 나아가는 직선이 될 것이다.
아래의 그림은 탁구공이 반사판에 충돌할 때 입사각에 따른 반사각을 계산할 때 이해를 돕기위한 것이다
공은 수직으로 내려오므로 자바의 삼각함수 각도체계에 따르면 90도 방향으로 이동하여 아랫쪽의 반사판에 충돌할 것이다. 만약 반사판의 각도가 수평이라면 공은 수직으로 내려와서 충돌하고 다시 수직(270도)으로 반사되어 올라 갈 것이다. 그러나 반사판이 수평으로부터 기울어져 있으므로 공이 반사판에 충돌하는 각도(입사각)와 동일한 반사각을 따라 튕겨져 나갈 것이다.
위의 그림에서 반사판은 수평으로부터 20도 경사를 이루고 있다
공은 수직으로 내려오지만 반사판에는 70도 각도로 충돌하므로 입사각은 70도이다
그러므로 반사각은 반사판의 반대편으로부터 70도가 되어야 한다.
이 때 반사각은 반사판에 상대적인 각도(70도)이므로 절대각도로 변환해야 삼각함수를 적용하여 기울기를 구할 수 있다
위에서 반사를 위한 절대각도는 반사판 우측에서 위쪽으로 70도이어야 하므로 20도 기울어져 있는 반사판을 고려하면 0도(360도)로부터 위쪽으로 50도를 이루어야 한다. 그러므로 자바의 삼각함수 각도체계에 따르면 360-50=310도가 된다.
우측으로 20도 기울어진 반사판의 각도를 절대각도록 변환하면 360+20=380도이며 이 각도로부터 70도 위에 반사각이 있으므로 결국 380-70=310도가 반사각이 된다
입사각 구하기
반사판의 각도와 입사각(절대각)을 알면 반사판에 상대적인 입사각을 구할 수 있다
반사판은 키보드에 의해 각도가 조절된다고 가정하면 반사판의 각도는 이미 계산되어 있는 상태일 것이다
반사판을 향해 내려오는 공의 이동벡터(방향과 속도를 표현하는 2개의 숫자)를 이용하여 기울기를 계산할 수 있고 기울기를 아크 탄젠트함수에 대입하면 라디안 입사각(절대각)을 구할 수 있다.
double deg = 20; // 반사판의 경사각도
double inboundRad = Math.atan2(ySpeed, xSpeed); // 절대적 입사각
double inboundRelative = inboundRad - deg; // 반사판에 상대적 입사각
반사각을 절대각도로 변환하기
m : 반사판의 경사각도
a : 입사각(반사판에 상대적인 각도)
r : 반사각(반사판에 상대적인 각도가 아닌 절대각도)
r = 360+m-a
아래의 코드는 버그가 존재하고 완전한 내용은 아니지만 반사체의 입사각을 이용하여 반사각을 구하는 문제에 대한 힌트로 사용될 수 있다
프로그램을 시작하면 A, D 키를 이용하여 반사판의 경사각을 조정할 수 있고 경사각에 따라서 충돌체의 반사 방향을 변경할 수 있다
JPanel에서 KeyEvent가 작동하게 하려면 모든 코드가 EDT(Event Dispatched Thread)안에서 실행되어야 하고 윈도우가 화면에 출력된 후에 JPanel.requestFocusInWindow()을 호출하면 키리스너가 제대로 작동한다
package pong; import java.awt.*; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.image.BufferedImage; import javax.swing.*; public class GameWindow extends JFrame { public GameWindow(){ setTitle("반사벡터 테스트"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Container c = this.getContentPane(); c.setLayout(null); setBounds(100, 50, 640, 480); GamePanel gp = new GamePanel(); c.add(gp); setVisible(true); gp.requestFocusInWindow(); } public static void main(String[] args) { //JPanel에서 키리스너가 작동하려면 모든 코드가 EDT(Event Dispatched Thread)안에서 실행되어야 한다 EventQueue.invokeLater(new Runnable() { @Override public void run() { new GameWindow(); } }); } } class GamePanel extends JPanel { BufferedImage reflBar; BufferedImage ball; int barX, barY; double ballX, ballY; double xSpeed = 0.4; double ySpeed = 1; double deg = 30; // 반사판의 경사각도 public GamePanel() { setLayout(null); setBounds(10, 10, 600, 420); this.setBackground(Color.WHITE); this.setDoubleBuffered(true); createReflBar(); createBall(); init(); //JPanel에서 KeyListener가 작동하려면 모든 코드가 EDT(Event Dispatched Thread)안에서 실행되어야 한다 addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if(keyCode==KeyEvent.VK_A){ deg-=2; }else if(keyCode==KeyEvent.VK_D){ deg+=2; } repaint(); } }); new Thread() { @Override public void run() { while(true){ ballX += xSpeed; ballY += ySpeed; if(ballY+20 >= barY){// 충돌탐지 및 충돌처리 collisionProcess(); } repaint(); try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } private void init() { barX = this.getWidth()/2-25; barY = this.getHeight()-50; ballX = this.getWidth()/2-60; ballY = this.getHeight()/2; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g; g2d.drawImage(ball, (int)ballX, (int)ballY, null); g2d.rotate(Math.toRadians(deg), barX+25, barY); g2d.drawImage(reflBar, barX, barY, null); } void createReflBar(){ reflBar = new BufferedImage(50, 20, BufferedImage.TYPE_BYTE_GRAY); Graphics2D g2d = (Graphics2D)reflBar.getGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setColor(Color.RED); g2d.fillRect(0,0, 50, 20); } void createBall() { ball = new BufferedImage(20, 20, BufferedImage.TYPE_BYTE_GRAY); Graphics2D g2d = (Graphics2D)ball.getGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setColor(Color.WHITE); g2d.fillRect(0, 0, 20, 29); g2d.setColor(Color.BLACK); g2d.fillOval(0,0, 20, 20); } void collisionProcess(){ double inAng = Math.toDegrees(Math.atan2(ySpeed, xSpeed)); //입사각(절대각도) double inAngRelative = inAng - deg; // 반사판에 상대적인 입사각 System.out.println("상대적 입사각:"+inAngRelative); double rad = Math.toRadians(360+deg-inAngRelative);//반사각(절대각도) double cosVal = Math.cos(rad); double sinVal = Math.sin(rad); xSpeed = cosVal; ySpeed = sinVal; } }