From ea33e71619d81bf773868ad02264fb990ffb7463 Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Tue, 22 Dec 2020 13:18:10 -0500 Subject: [PATCH] added a working physics system --- src/DataTypes/Interfaces/Descriptors.ts | 12 +- src/DataTypes/Shapes/AABB.ts | 51 +++-- src/Nodes/CanvasNode.ts | 3 +- src/Nodes/GameNode.ts | 26 ++- src/Nodes/Tilemaps/OrthogonalTilemap.ts | 2 +- src/Physics/BasicPhysicsManager.ts | 13 +- src/Physics/TestPhysicsManager.ts | 229 +++++++++++++++++++ src/Scene/Factories/TilemapFactory.ts | 2 +- src/_DemoClasses/PhysicsTesting/TestScene.ts | 70 +++--- src/main.ts | 2 +- 10 files changed, 352 insertions(+), 58 deletions(-) create mode 100644 src/Physics/TestPhysicsManager.ts diff --git a/src/DataTypes/Interfaces/Descriptors.ts b/src/DataTypes/Interfaces/Descriptors.ts index 6db66c6..671a839 100644 --- a/src/DataTypes/Interfaces/Descriptors.ts +++ b/src/DataTypes/Interfaces/Descriptors.ts @@ -55,6 +55,9 @@ export interface Physical { /** The shape of the collider for this physics object. */ collisionShape: Shape; + /** The offset of the collision shape from the center of the node */ + colliderOffset: Vec2; + /** Represents whether this object can move or not. */ isStatic: boolean; @@ -84,6 +87,8 @@ export interface Physical { isPlayer: boolean; + isColliding: boolean; + /*---------- FUNCTIONS ----------*/ /** @@ -104,7 +109,7 @@ export interface Physical { * @param isCollidable Whether this object will be able to collide with other objects * @param isStatic Whether this object will be static or not */ - addPhysics: (collisionShape?: Shape, isCollidable?: boolean, isStatic?: boolean) => void; + addPhysics: (collisionShape?: Shape, colliderOffset?: Vec2, isCollidable?: boolean, isStatic?: boolean) => void; /** * Adds a trigger to this object for a specific group @@ -118,6 +123,11 @@ export interface Physical { * @param layer The name of the layer */ setPhysicsLayer: (layer: String) => void; + + /** + * If used before "move()", it will tell you the velocity of the node after its last movement + */ + getLastVelocity(): Vec2; } /** diff --git a/src/DataTypes/Shapes/AABB.ts b/src/DataTypes/Shapes/AABB.ts index 2c2bee7..f53f845 100644 --- a/src/DataTypes/Shapes/AABB.ts +++ b/src/DataTypes/Shapes/AABB.ts @@ -2,6 +2,7 @@ import Shape from "./Shape"; import Vec2 from "../Vec2"; import MathUtils from "../../Utils/MathUtils"; import Circle from "./Circle"; +import Debug from "../../Debug/Debug"; export default class AABB extends Shape { @@ -109,25 +110,20 @@ export default class AABB extends Shape { * @param paddingX Pads the AABB in the x axis * @param paddingY Pads the AABB in the y axis */ - intersectSegment(point: Vec2, direction: Vec2, distance?: number, paddingX?: number, paddingY?: number): Hit { - // Scale by the distance if it has been provided - if(distance){ - direction = direction.scaled(distance); - } + intersectSegment(point: Vec2, delta: Vec2, padding?: Vec2): Hit { + let paddingX = padding ? padding.x : 0; + let paddingY = padding ? padding.y : 0; - let _paddingX = paddingX ? paddingX : 0; - let _paddingY = paddingY ? paddingY : 0; - - let scaleX = 1/direction.x; - let scaleY = 1/direction.y; + let scaleX = 1/delta.x; + let scaleY = 1/delta.y; let signX = MathUtils.sign(scaleX); let signY = MathUtils.sign(scaleY); - let tnearx = scaleX*(this.x - signX*(this.hw + _paddingX) - point.x); - let tneary = scaleX*(this.y - signY*(this.hh + _paddingY) - point.y); - let tfarx = scaleY*(this.x + signX*(this.hw + _paddingX) - point.x); - let tfary = scaleY*(this.y + signY*(this.hh + _paddingY) - point.y); + let tnearx = scaleX*(this.x - signX*(this.hw + paddingX) - point.x); + let tneary = scaleY*(this.y - signY*(this.hh + paddingY) - point.y); + let tfarx = scaleX*(this.x + signX*(this.hw + paddingX) - point.x); + let tfary = scaleY*(this.y + signY*(this.hh + paddingY) - point.y); if(tnearx > tfary || tneary > tfarx){ // We aren't colliding - we clear one axis before intersecting another @@ -135,15 +131,29 @@ export default class AABB extends Shape { } let tnear = Math.max(tnearx, tneary); + + // Double check for NaNs + if(tnearx !== tnearx){ + tnear = tneary; + } else if (tneary !== tneary){ + tnear = tnearx; + } + let tfar = Math.min(tfarx, tfary); + if(tnear === -Infinity){ + return null; + } + if(tnear >= 1 || tfar <= 0){ return null; } // We are colliding let hit = new Hit(); - hit.t = MathUtils.clamp01(tnear); + hit.time = MathUtils.clamp01(tnear); + hit.nearTimes.x = tnearx; + hit.nearTimes.y = tneary; if(tnearx > tneary){ // We hit on the left or right size @@ -154,10 +164,10 @@ export default class AABB extends Shape { hit.normal.y = -signY; } - hit.delta.x = (1.0 - hit.t) * -direction.x; - hit.delta.y = (1.0 - hit.t) * -direction.y; - hit.pos.x = point.x + direction.x * hit.t; - hit.pos.y = point.y + direction.y * hit.t; + hit.delta.x = (1.0 - hit.time) * -delta.x; + hit.delta.y = (1.0 - hit.time) * -delta.y; + hit.pos.x = point.x + delta.x * hit.time; + hit.pos.y = point.y + delta.y * hit.time; return hit; } @@ -241,7 +251,8 @@ export default class AABB extends Shape { } export class Hit { - t: number; + time: number; + nearTimes: Vec2 = Vec2.ZERO; pos: Vec2 = Vec2.ZERO; delta: Vec2 = Vec2.ZERO; normal: Vec2 = Vec2.ZERO; diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index 93254d8..49f60db 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -94,6 +94,7 @@ export default abstract class CanvasNode extends GameNode implements Region { debugRender(): void { super.debugRender(); - Debug.drawBox(this.relativePosition, this.sizeWithZoom, false, Color.GREEN); + let color = this.isColliding ? Color.RED : Color.GREEN; + Debug.drawBox(this.relativePosition, this.sizeWithZoom, false, color); } } \ No newline at end of file diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index 55f8039..d263192 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -31,6 +31,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable onCeiling: boolean; active: boolean; collisionShape: Shape; + colliderOffset: Vec2; isStatic: boolean; isCollidable: boolean; isTrigger: boolean; @@ -41,6 +42,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable collidedWithTilemap: boolean; physicsLayer: number; isPlayer: boolean; + isColliding: boolean = false; /*---------- ACTOR ----------*/ _ai: AI; @@ -128,7 +130,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable * @param isCollidable Whether this is collidable or not. True by default. * @param isStatic Whether this is static or not. False by default */ - addPhysics = (collisionShape?: Shape, isCollidable: boolean = true, isStatic: boolean = false): void => { + addPhysics = (collisionShape?: Shape, colliderOffset?: Vec2, isCollidable: boolean = true, isStatic: boolean = false): void => { this.hasPhysics = true; this.moving = false; this.onGround = false; @@ -154,6 +156,12 @@ export default abstract class GameNode implements Positioned, Unique, Updateable throw "No collision shape specified for physics object." } + if(colliderOffset){ + this.colliderOffset = colliderOffset; + } else { + this.colliderOffset = Vec2.ZERO; + } + this.sweptRect = this.collisionShape.getBoundingRect(); this.scene.getPhysicsManager().registerObject(this); } @@ -171,6 +179,10 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this.scene.getPhysicsManager().setLayer(this, layer); } + getLastVelocity(): Vec2 { + return this._velocity; + } + /*---------- ACTOR ----------*/ get ai(): AI { return this._ai; @@ -251,7 +263,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable */ protected positionChanged(): void { if(this.hasPhysics){ - this.collisionShape.center = this.position; + this.collisionShape.center = this.position.clone().add(this.colliderOffset); } }; @@ -260,11 +272,17 @@ export default abstract class GameNode implements Positioned, Unique, Updateable } debugRender(): void { - Debug.drawPoint(this.relativePosition, Color.GREEN); + let color = this.isColliding ? Color.RED : Color.GREEN; + Debug.drawPoint(this.relativePosition, color); // If velocity is not zero, draw a vector for it if(this._velocity && !this._velocity.isZero()){ - Debug.drawRay(this.relativePosition, this._velocity.clone().scaleTo(20).add(this.relativePosition), Color.GREEN); + Debug.drawRay(this.relativePosition, this._velocity.clone().scaleTo(20).add(this.relativePosition), color); + } + + // If this has a collider, draw it + if(this.isCollidable && this.collisionShape){ + Debug.drawBox(this.collisionShape.center, this.collisionShape.halfSize, false, Color.RED); } } } diff --git a/src/Nodes/Tilemaps/OrthogonalTilemap.ts b/src/Nodes/Tilemaps/OrthogonalTilemap.ts index 99eb532..a439c15 100644 --- a/src/Nodes/Tilemaps/OrthogonalTilemap.ts +++ b/src/Nodes/Tilemaps/OrthogonalTilemap.ts @@ -27,7 +27,7 @@ export default class OrthogonalTilemap extends Tilemap { // The size of the tilemap on the canvas this.size.set(this.numCols * this.tileSize.x, this.numRows * this.tileSize.y); - this.position.copy(this.size); + this.position.copy(this.size.scaled(0.5)); this.data = layer.data; this.visible = layer.visible; diff --git a/src/Physics/BasicPhysicsManager.ts b/src/Physics/BasicPhysicsManager.ts index a93a286..e50f229 100644 --- a/src/Physics/BasicPhysicsManager.ts +++ b/src/Physics/BasicPhysicsManager.ts @@ -91,6 +91,12 @@ export default class BasicPhysicsManager extends PhysicsManager { resolveCollision(node1: Physical, node2: Physical, firstContact: Vec2, lastContact: Vec2, collidingX: boolean, collidingY: boolean): void { // Handle collision if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ + if(node1.isPlayer){ + node1.isColliding = true; + } else if(node2.isPlayer){ + node2.isColliding = true; + } + // We are colliding. Check for any triggers let group1 = node1.group; let group2 = node2.group; @@ -109,7 +115,7 @@ export default class BasicPhysicsManager extends PhysicsManager { } if(collidingX && collidingY){ - // If we're already intersecting, freak out I guess? Probably should handle this in some way for if nodes get spawned inside of tiles + // If we're already intersecting, resolve the current collision } else if(node1.isCollidable && node2.isCollidable) { // We aren't already colliding, and both nodes can collide, so this is a new collision. @@ -264,6 +270,11 @@ export default class BasicPhysicsManager extends PhysicsManager { node.onCeiling = false; node.onWall = false; node.collidedWithTilemap = false; + node.isColliding = false; + + if(node.isPlayer){ + Debug.log("pvel", "Player Velocity:", node._velocity.toString()); + } // Update the swept shapes of each node if(node.moving){ diff --git a/src/Physics/TestPhysicsManager.ts b/src/Physics/TestPhysicsManager.ts new file mode 100644 index 0000000..a13e102 --- /dev/null +++ b/src/Physics/TestPhysicsManager.ts @@ -0,0 +1,229 @@ +import GameNode from "../Nodes/GameNode"; +import { Physical, Updateable } from "../DataTypes/Interfaces/Descriptors"; +import Tilemap from "../Nodes/Tilemap"; +import PhysicsManager from "./PhysicsManager"; +import Vec2 from "../DataTypes/Vec2"; +import Debug from "../Debug/Debug"; +import Color from "../Utils/Color"; +import AABB from "../DataTypes/Shapes/AABB"; +import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap"; + +export default class TestPhysicsManager extends PhysicsManager { + + /** The array of static nodes */ + protected staticNodes: Array; + + /** The array of dynamic nodes */ + protected dynamicNodes: Array; + + /** The array of tilemaps */ + protected tilemaps: Array; + + constructor(){ + super(); + this.staticNodes = new Array(); + this.dynamicNodes = new Array(); + this.tilemaps = new Array(); + } + + /** + * Add a new physics object to be updated with the physics system + * @param node The node to be added to the physics system + */ + registerObject(node: GameNode): void { + if(node.isStatic){ + // Static and not collidable + this.staticNodes.push(node); + } else { + // Dynamic and not collidable + this.dynamicNodes.push(node); + } + } + + /** + * Registers a tilemap with this physics manager + * @param tilemap + */ + registerTilemap(tilemap: Tilemap): void { + this.tilemaps.push(tilemap); + } + + setLayer(node: GameNode, layer: string): void { + node.physicsLayer = this.layerMap.get(layer); + } + + /** + * Updates the physics + * @param deltaT + */ + update(deltaT: number): void { + /* ALGORITHM: + In an effort to keep things simple and working effectively, each dynamic node will resolve its + collisions considering the rest of the world as static. + + Collision detecting will happen first. This can be considered a broad phase, but it is not especially + efficient, as it does not need to be for this game engine. Every dynamic node is checked against every + other node for collision area. If collision area is non-zero (meaning the current node sweeps into another), + it is added to a list of hits. + + INITIALIZATION: + - Physics constants are reset + - Swept shapes are recalculated. If a node isn't moving, it is skipped. + + COLLISION DETECTION: + - For a node, collision area will be calculated using the swept AABB of the node against every other AABB in a static state + - These collisions will be sorted by area in descending order + + COLLISION RESOLUTION: + - For each hit, time of collision is calculated using a swept line through the AABB of the static node expanded + with minkowski sums (discretely, but the concept is there) + - The collision is resolved based on the near time of the collision (from method of separated axes) + - X is resolved by near x, Y by near y. + - There is some fudging to allow for sliding along walls of separate colliders. Sorting by area also helps with this. + - Corner to corner collisions are resolve to favor x-movement. This is in consideration of platformers, to give + the player some help with jumps + + Pros: + - Everything happens with a consistent time. There is a distinct before and after for each resolution. + - No back-tracking needs to be done. Once we resolve a node, it is definitively resolved. + + Cons: + - Nodes that are processed early have movement priority over other nodes. This can lead to some undesirable interactions. + */ + for(let node of this.dynamicNodes){ + /*---------- INITIALIZATION PHASE ----------*/ + // Clear frame dependent boolean values for each node + node.onGround = false; + node.onCeiling = false; + node.onWall = false; + node.collidedWithTilemap = false; + + // Update the swept shapes of each node + if(node.moving){ + // If moving, reflect that in the swept shape + node.sweptRect.sweep(node._velocity, node.collisionShape.center, node.collisionShape.halfSize); + } else { + // If our node isn't moving, don't bother to check it (other nodes will detect if they run into it) + node._velocity.zero(); + continue; + } + + /*---------- DETECTION PHASE ----------*/ + // Gather a set of overlaps + let overlaps = new Array(); + + // First, check this node against every static node (order doesn't actually matter here, since we sort anyways) + for(let other of this.staticNodes){ + let collider = other.collisionShape.getBoundingRect(); + let area = node.sweptRect.overlapArea(collider); + if(area > 0){ + // We had a collision + overlaps.push(new AreaCollision(area, collider)); + } + } + + // Then, check it against every dynamic node + for(let other of this.dynamicNodes){ + let collider = other.collisionShape.getBoundingRect(); + let area = node.sweptRect.overlapArea(collider); + if(area > 0){ + // We had a collision + overlaps.push(new AreaCollision(area, collider)); + } + } + + // Lastly, gather a set of AABBs from the tilemap. + // This step involves the most extra work, so it is abstracted into a method + for(let tilemap of this.tilemaps){ + if(tilemap instanceof OrthogonalTilemap){ + this.collideWithOrthogonalTilemap(node, tilemap, overlaps); + } + } + + // Sort the overlaps by area + overlaps = overlaps.sort((a, b) => b.area - a.area); + + + /*---------- RESOLUTION PHASE ----------*/ + // For every overlap, determine if we need to collide with it and when + for(let other of overlaps){ + // Do a swept line test on the static AABB with this AABB size as padding (this is basically using a minkowski sum!) + // Start the sweep at the position of this node with a delta of _velocity + const point = node.collisionShape.center; + const delta = node._velocity; + const padding = node.collisionShape.halfSize; + const otherAABB = other.collider; + + + const hit = otherAABB.intersectSegment(node.collisionShape.center, node._velocity, node.collisionShape.halfSize); + + if(hit !== null){ + // We got a hit, resolve with the time inside of the hit + let tnearx = hit.nearTimes.x; + let tneary = hit.nearTimes.y; + + // Allow edge clipping (edge overlaps don't count, only area overlaps) + // Importantly don't allow both cases to be true. Then we clip through corners. Favor x to help players land jumps + if(tnearx < 1.0 && (point.y === otherAABB.top - padding.y || point.y === otherAABB.bottom + padding.y) && delta.x !== 0) { + tnearx = 1.0; + } else if(tneary < 1.0 && (point.x === otherAABB.left - padding.x || point.x === otherAABB.right + padding.x) && delta.y !== 0) { + tneary = 1.0; + } + + + if(hit.nearTimes.x >= 0 && hit.nearTimes.x < 1){ + node._velocity.x = node._velocity.x * tnearx; + } + + if(hit.nearTimes.y >= 0 && hit.nearTimes.y < 1){ + node._velocity.y = node._velocity.y * tneary; + } + } + } + + // Resolve the collision with the node, and move it + node.finishMove(); + } + } + + collideWithOrthogonalTilemap(node: Physical, tilemap: OrthogonalTilemap, overlaps: Array): void { + // Get the min and max x and y coordinates of the moving node + let min = new Vec2(node.sweptRect.left, node.sweptRect.top); + let max = new Vec2(node.sweptRect.right, node.sweptRect.bottom); + + // Convert the min/max x/y to the min and max row/col in the tilemap array + let minIndex = tilemap.getColRowAt(min); + let maxIndex = tilemap.getColRowAt(max); + + let tileSize = tilemap.getTileSize(); + + // Loop over all possible tiles (which isn't many in the scope of the velocity per frame) + for(let col = minIndex.x; col <= maxIndex.x; col++){ + for(let row = minIndex.y; row <= maxIndex.y; row++){ + if(tilemap.isTileCollidable(col, row)){ + // Get the position of this tile + let tilePos = new Vec2(col * tileSize.x + tileSize.x/2, row * tileSize.y + tileSize.y/2); + + // Create a new collider for this tile + let collider = new AABB(tilePos, tileSize.scaled(1/2)); + + // Calculate collision area between the node and the tile + let area = node.sweptRect.overlapArea(collider); + if(area > 0){ + // We had a collision + overlaps.push(new AreaCollision(area, collider)); + } + } + } + } + } +} + +class AreaCollision { + area: number; + collider: AABB; + constructor(area: number, collider: AABB){ + this.area = area; + this.collider = collider; + } +} \ No newline at end of file diff --git a/src/Scene/Factories/TilemapFactory.ts b/src/Scene/Factories/TilemapFactory.ts index f7bcf10..2311bb5 100644 --- a/src/Scene/Factories/TilemapFactory.ts +++ b/src/Scene/Factories/TilemapFactory.ts @@ -187,7 +187,7 @@ export default class TilemapFactory { // Now we have sprite. Associate it with our physics object if there is one if(hasPhysics){ - sprite.addPhysics(sprite.boundary.clone(), isCollidable, isStatic); + sprite.addPhysics(sprite.boundary.clone(), Vec2.ZERO, isCollidable, isStatic); sprite.group = group; } } diff --git a/src/_DemoClasses/PhysicsTesting/TestScene.ts b/src/_DemoClasses/PhysicsTesting/TestScene.ts index e85ab4b..b49e96c 100644 --- a/src/_DemoClasses/PhysicsTesting/TestScene.ts +++ b/src/_DemoClasses/PhysicsTesting/TestScene.ts @@ -1,55 +1,69 @@ +import AABB from "../../DataTypes/Shapes/AABB"; +import { TiledTilemapData } from "../../DataTypes/Tilesets/TiledData"; import Vec2 from "../../DataTypes/Vec2"; +import Debug from "../../Debug/Debug"; import InputHandler from "../../Input/InputHandler"; import InputReceiver from "../../Input/InputReceiver"; +import CanvasNode from "../../Nodes/CanvasNode"; import { GraphicType } from "../../Nodes/Graphics/GraphicTypes"; -import BasicPhysicsManager from "../../Physics/BasicPhysicsManager"; +import TestPhysicsManager from "../../Physics/TestPhysicsManager"; import Scene from "../../Scene/Scene"; import Color from "../../Utils/Color"; export default class TestScene extends Scene { + loadScene(){ + this.load.tilemap("test", "assets/tilemaps/PhysicsTest.json"); + } + startScene(){ // Opt into a custom physics manager - this.physicsManager = new BasicPhysicsManager(this.sceneOptions.physics); + this.physicsManager = new TestPhysicsManager(); + + let tilemap = this.add.tilemap("test")[0].getItems()[0]; + + let layer = this.getLayer("MovingObject"); + layer.getItems().forEach(item => { + let timer = 0; + let dir = new Vec2(-1, 0); + item.update = (deltaT: number) => { + if(timer > 2){ + timer = 0; + dir.scale(-1); + } + + item.move(dir.scaled(100*deltaT)); + + timer += deltaT; + } + }) this.addLayer("main"); - let player = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(100, 100), size: new Vec2(100, 100)}); - player.addPhysics(); + let player = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(50, 100), size: new Vec2(45, 45)}); + player.color = Color.ORANGE; + player.addPhysics(new AABB(new Vec2(0, 0), new Vec2(15, 15)), new Vec2(0, 7.5)); player.update = (deltaT: number) => { const input = InputReceiver.getInstance() let xDir = (input.isPressed("a") ? -1 : 0) + (input.isPressed("d") ? 1 : 0); - let yDir = (input.isPressed("w") ? -1 : 0) + (input.isPressed("s") ? 1 : 0); + let yDir = input.isJustPressed("space") ? -1 : 0; - let dir = new Vec2(xDir, yDir); - dir.normalize(); + let dir = new Vec2(xDir * 300 * deltaT, yDir*1000 * deltaT); + + // Gravity + if(dir.y === 0){ + dir.y = player.getLastVelocity().y + 50 * deltaT; + } + + Debug.log("pvel", player.getLastVelocity()); if(!dir.isZero()){ - player.move(dir.scale(deltaT * 300)); + player.move(dir); } } - let block = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(300, 500), size: new Vec2(100, 100)}); - block.color = Color.CYAN; - block.addPhysics(block.boundary, true, true); - - let movingBlock = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(500, 200), size: new Vec2(100, 100)}); - movingBlock.color = Color.CYAN; - movingBlock.addPhysics(); - - let timer = 0; - let dir = new Vec2(1, 0); - movingBlock.update = (deltaT: number) => { - if(timer > 0.5){ - timer = 0; - dir.scale(-1); - } - - movingBlock.move(dir.scaled(200*deltaT)); - - timer += deltaT; - } + player.isPlayer = true; } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index cda1856..d28a736 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,7 +15,7 @@ function main(){ game.start(); let sm = game.getSceneManager(); - sm.addScene(MainMenu, {}); + sm.addScene(TestScene, {}); } CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {