Search code examples
javascripttypescriptcollision-detectionjoystickpythagorean

JavaScript: Detecting Region of a Circle


I am working on a software joystick implementation for a game client written in JavaScript/TypeScript. I need to detect the different regions within a circle radius where an event occurs for 4-directional movement (up, down, left, right). Currently my mapping looks something like the following (because I only know how to work with four-sided polygons):

current

But what I would like to achieve is something like this:

desired

The violet represents areas for lateral movement. The green for vertical. The gray and black are dead zones (play) where no directional movement should be applied.

The radius of the circle I am working with is 75 pixels. Inner gray dead zone radius is 24 pixels.

The following code is an example (not exact as I am only including what is relevant to this question) of how I am currently detecting direction:

    /**
     * Retrieves the interpreted direction of joystick.
     *
     * Returned directions are represented using the following integer values:
     * - 0: no movement (stop)
     * - 1: up
     * - 2: right
     * - 3: down
     * - 4: left
     *
     * @param relX {number}
     *   Point of event on X axis relative to joystick center.
     * @param relY {number}
     *   Point of event on Y axis relative to joystick center.
     * @return {number}
     *   Facing direction the joystick's state represents.
     */
    private getPressedDirection(relX: number, relY: number): number {
        let dir = 0;
        const playThreshold = 24;
        if (relX < -playThreshold) {
            dir = 4;
        } else if (relX > playThreshold) {
            dir = 2;
        } else if (relY < -playThreshold) {
            dir = 1;
        } else if (relY > playThreshold) {
            dir = 3;
        }
        return dir;
    }

As you can probably see, there are multiple issues with it. The regions of detection are not equal in size. The dead zone area is square instead of circular. And it allows direction to be detected outside the radius of the main circle.

If I understand correctly, I need to use the Pythagorean theorem to detect the boundaries of the circle radius, as is suggested in https://stackoverflow.com/a/7227057/4677917 and many other answers I have found, to do something like the following:

    private getPressedDirection(relX: number, relY: number): number {
        let dir = 0;
        const radius = 75;
        const playThreshold = 24;
        const eventResult = Math.pow(relX, 2) + Math.pow(relY, 2);
        const hit = eventResult < Math.pow(radius, 2) && eventResult > Math.pow(playThreshold, 2);
        ...

If that is correct, it tells me whether an event occurred within the boundaries of the joystick's circle. But I'm still unsure how to detect which region the event occurred.


Solution

  • Thank you to @CBroe for pointing out what I needed to be looking for.

    I'm able to find the angle relative to the center of the joystick with a function like this:

        private pointToDeg(relX: number, relY: number): number {
            // convert coordinates to radians
            const rad = Math.atan2(relY, relX);
            // convert radians to angle of degrees
            return rad * 180 / Math.PI;
        }
    

    I map the angle regions to the joystick:

        // format: [direction, angleMin, angleMax]
        private readonly sectors: number[][] = [
            // NOTE: 0 degrees represents right on joystick plane
            [1, 225, 314.9],
            [2, 315, 44.9],
            [3, 45, 134.9],
            [4, 135, 224.9]
        ];
    

    Then I can iterate the mappings:

        private getPressedDirection(relX: number, relY: number): number {
            const angle = this.pointToDeg(relX, relY);
            for (const s of this.sectors) {
                if (this.sectorContains(s[1], s[2], angle)) {
                    return s[0];
                }
            }
            return 0;
        }
    
        /**
         * Checks if an angle is within a specified sector region.
         */
        private sectorContains(min: number, max: number, angle: number): boolean {
            // ensure values are within boundaries of 360 degrees
            min %= 360;
            max %= 360;
            angle %= 360;
            // normalize to positive degrees values
            min = min < 0 ? min + 360 : min;
            max = max < 0 ? max + 360 : max;
            angle = angle < 0 ? angle + 360 : angle;
            // check if within range
            if (min > max) {
                return angle >= min || angle <= max;
            }
            return angle >= min && angle <= max;
        }
    

    The second issue of checking if a point is within the non-dead zone area is solved with a function like this:

        private isHit(relX: number, relY: number): boolean {
            const dist = Math.pow(Math.abs(relX), 2) + Math.pow(Math.abs(relY), 2);
            return dist < Math.pow(this.radius, 2) && dist > Math.pow(this.playThreshold, 2);
        }
    
        private getPressedDirection(relX: number, relY: number): number {
            if (!this.isHit(relX, relY)) {
                    return 0;
            }
            const angle = this.pointToDeg(relX, relY);
            for (const s of this.sectors) {
                if (this.sectorContains(s[1], s[2], angle)) {
                    return s[0];
                }
            }
            return 0;
        }
    

    If I made any mistakes I will try to correct it. It's not copied exactly from my actual code.

    I also want to reference this answer as it helped with another issue related to my joystick.