Search code examples
phpmysqlmathgame-physics

Reflection Inside Circle Over Time


This is for a game that I am considering.

I have a point that is noted as moving inside a (2D) circle from an arbitrary point, at an arbitrary direction and at a particular time. The point will bounce off the interior wall of the circle when it intersects.

For this example let's say the circle has a diameter of 100 kilometers with it's center at (0,0) and 10 hours ago the point was at location (20,30) with a heading of 40 degrees at a speed of 50kph .

What is the best way to determine where that point currently is and at what direction it's traveling?

I will be implementing this in PHP with the point and circle data stored in MySQL. Since it is a web page there will be no constantly running host process to keep things up to date and the data will need to be refreshed upon a page load.

I'm certainly not looking for anyone to write the code for me but am hoping someone can help me with a somewhat efficient way to approach this.


Solution

  • Your point-object will travel along what is called chords in geometry.

    As the object hits the circle boundary, it will reflect from the circle's tangent at that point, and go along a next chord that has the same length. The next hit will be at the same angle (with the tangent at that hit point) as the previous hit, and so it will continue. At a constant speed, the time between hits will be a constant time.

    Given the start time and the current time, one can calculate the number of chords that have been completed, and how much of the current chord has been completed. Calculating the position from that is easy when you know the previous and next hit positions. As these hit positions are at equal distances along the circle boundary, that is a matter of converting polar coordinates (an angle, and a distance of 1 radian) to Cartesian coordinates.

    I will demonstrate this with JavaScript code. It will not be a great effort to wrap this in PHP:

    var canvas = document.getElementById('myCanvas');
    var context = canvas.getContext('2d');
    var radius = canvas.height / 2 - 5;
    var pixel = 1/radius;
    // transform the canvas so that 0,0 is in the center of the canvas,
    // and a unit circle would cover most of the height of the canvas: 
    context.setTransform(radius, 0, 0, radius, radius+2, radius+2);
    
    // draw unit circle
    context.beginPath();
    context.arc(0, 0, 1, 0, 2 * Math.PI, false);
    context.lineWidth = pixel;
    context.strokeStyle = 'black';
    context.stroke();
    
    function drawPoint(point) {
        // use a different color every 30 seconds:
        context.fillStyle = Date.now() % 60000 > 30000 ? 'red' : 'blue';
        context.fillRect(point.x-2*pixel, point.y-2*pixel, 4*pixel, 4*pixel);
    }
    function polarToCartesian(rad, dist) {
        return {
            x: Math.cos(rad) * dist,
            y: Math.sin(rad) * dist
        }
    }
    function pointBetween(a, b, fractionTravelled) {
        return {
            x: a.x + (b.x-a.x)*fractionTravelled,
            y: a.y + (b.y-a.y)*fractionTravelled
        }
    }
    
    // 4 parameters are needed:
    var startRadians = 0;            // distance along circle boundary from (0,1)
    var hitAngle = Math.PI/2.931;    // PI/2 would be head-on impact along diagonal
    var speed = 0.4;                 // radians per second
    var startTime = Date.now()/1000; // seconds
    //
    
    // Calculate some derived values which remain constant:
    // - theta as used on https://en.wikipedia.org/wiki/Chord_%28geometry%29
    // - chordSize formula comes from that wiki article. 
    var theta = 2 * hitAngle;
    var chordSize = 2 * Math.sin(theta/2); // in radians
    
    function drawCurrentPosition() {
        // Note that this calculation does not look at the previous result,
        // but uses the original parameters and time passed to calculate
        // the objects current position.
        var elapsedTime = Date.now()/1000 - startTime;   // in secs
        var distanceTravelled = speed * elapsedTime;     // in radians
        var chordsTravelled = distanceTravelled / chordSize; // in number of chords
        var chordsTravelledComplete = Math.floor(chordsTravelled);
        var fractionOnChord = chordsTravelled - chordsTravelledComplete; // 0<=f<1
        var lastHitRadians = startRadians + chordsTravelledComplete * theta; // rad
        var nextHitRadians = lastHitRadians + theta;
        var lastHitPos = polarToCartesian(lastHitRadians, 1); // (x,y)
        var nextHitPos = polarToCartesian(nextHitRadians, 1);
        var currentPos = pointBetween(lastHitPos, nextHitPos, fractionOnChord);
        drawPoint(currentPos);
    }
    
    // Demo: keep drawing the object's position every 0.1 second:
    setInterval(drawCurrentPosition, 100);
    <canvas id="myCanvas" width="200" height="200"></canvas>

    Addendum: PHP code

    Here is some code that could be useful for use in PHP. It uses the same calculations as the above JavaScript code, but does not keep running. Instead it first checks if there is a started game in the session scope, if not, it starts the "clock". At every request (reload of the page), the new position is calculated and printed on the page as an X,Y pair.

    The coordinates are normalised, based on a unit circle (radius 1). The game parameters are hard-coded, but you could easily let them be passed via POST/GET parameters:

    session_start(); // needed to persist game data for this user session
    
    function getNewGame($startRadians, $hitAngle, $speed) {
        $game = array();
        $game["startTime"] = microtime(true);
        $game["startRadians"] = $startRadians;
        $game["theta"] = 2 * $hitAngle;
        $game["chordSize"] = 2 * sin($hitAngle);
        $game["speed"] = $speed;
        return (object) $game;
    }
    
    function polarToCartesian($rad, $dist) {
        return (object) array(
            "x" => cos($rad) * $dist,
            "y" => sin($rad) * $dist
        );
    }
    
    function pointBetween($a, $b, $fractionTravelled) {
        return (object) array(
            "x" => $a->x + ($b->x-$a->x)*$fractionTravelled,
            "y" => $a->y + ($b->y-$a->y)*$fractionTravelled
        );
    }
    
    function getCurrentPosition($game) {
        // Note that this calculation does not look at the previous result,
        // but uses the original parameters and time passed to calculate
        // the objects current position.
        $elapsedTime = microtime(true) - $game->startTime;   // in secs
        $distanceTravelled = $game->speed * $elapsedTime;     // in radians
        $chordsTravelled = $distanceTravelled / $game->chordSize; //number of chords
        $chordsTravelledComplete = floor($chordsTravelled);
        $fractionOnChord = $chordsTravelled - $chordsTravelledComplete; // 0<=f<1
        $lastHitRadians = $game->startRadians
              + $chordsTravelledComplete * $game->theta; // in radians on circle
        $nextHitRadians = $lastHitRadians + $game->theta;
        $lastHitPos = polarToCartesian($lastHitRadians, 1); // (x,y)
        $nextHitPos = polarToCartesian($nextHitRadians, 1);
        $currentPos = pointBetween($lastHitPos, $nextHitPos, $fractionOnChord);
        return $currentPos;
    }
    
    // check if this is the first time the user loads this page:
    if (!isset($_SESSION["game"])) {
        // start game with some game parameters:
        $_SESSION["game"] = getNewGame(0, pi()/2.931, 0.4);
    }
    
    // calculate the position based on game info and current time:
    $pos = getCurrentPosition($_SESSION["game"]);
    // print the result:
    echo "Current position: {$pos->x}, {$pos->y}<br>";