{"author_link":"\/users\/cottontshirtz","author_name":"cottontshirtz","author_uid":"cottontshirtz","comments":[],"epoch":1493591222,"event":"LD38","format":"md","ldjam_node_id":29017,"likes":6,"metadata":{"p_key":"91916","p_author":"cottontshirtz","p_authorkey":"1011109","p_urlkey":"304834","p_title":"P L A N E T O R U S   Retrospective","p_cat":"LDJam ","p_event":"LD38","p_time":"1493591222","p_likes":"6","p_comments":"0","p_status":"WAYBACK","us_key":"1011109","us_name":"cottontshirtz","us_username":"cottontshirtz","event_start":"1492732800","event_key":"69","event_name":"LD 38"},"node":{"_collation":{"body_sanitizer":"TextUtils::SanitizeHTML via existing importer","event":"LD38","removed_author":false},"_superparent":9405,"_trust":6,"author":11109,"body":"# \"Wait, how did you ...?\"\n\nMy approach to making games has always been focused on solving problems I consider interesting. In that same vein, this post is focused on those aspects of my entry **P L A N E T O R U S** that I found interesting and enjoyed solving. As a disclaimer, a lot of the code in this post can no doubt be optimized or in some cases replaced with analytic solutions; but that's how game jams go.\n\nBefore reading on, I suggest you take a chance to play my entry so this post has context.\n\nhttps:\/\/ldjam.com\/events\/ludum-dare\/38\/planetorus\n\n### How to Address the Torus Surface\nThe surface of a torus can be represented as a 2D surface which wraps around at the seams. Towards this end we can define a rectangular map with an arbitrary `MAP_WIDTH` and `MAP_HEIGHT`. These lead to a `MAJOR_RADIUS` and `MINOR_RADIUS` for the torus as calculated below.\n```\nMAJOR_RADIUS = MAP_WIDTH  * Mathf.PI * 2\nMINOR_RADIUS = MAP_HEIGHT * Mathf.PI * 2\n```\nA 2D location on this map can be converted to a location in 3D on the surface of the torus with the following procedure.\n\n```\npublic static Vector3 get3DLocationFor2DLocation (Vector2 mapLocation) {\n\n\t\tfloat vertAsRad = (mapLocation.y \/ MAP_HEIGHT) * (2 * Mathf.PI);\n\t\tVector2 minorCircle = new Vector2 (Mathf.Cos(vertAsRad), Mathf.Sin(vertAsRad));\n\t\tminorCircle *= MINOR_RADIUS;\n\n\t\tfloat horzAsRad = (mapLocation.x \/ MAP_WIDTH) * (2 * Mathf.PI);\n\t\tVector2 majorCircle = new Vector2 (Mathf.Cos (horzAsRad), Mathf.Sin (horzAsRad));\n\t\tmajorCircle *= MAJOR_RADIUS;\n\n\t\tVector3 result = new Vector3 \n\t\t\t( majorCircle.x + (Mathf.Cos(horzAsRad) * minorCircle.x),\n\t\t\t  minorCircle.y,\n\t\t\t  majorCircle.y + (Mathf.Sin(horzAsRad) * minorCircle.x)\n\t\t\t);\n\n\t\treturn result;\n}\n```\n\nEssentially, we find the `x,y` map location as a ratio of the map width and height; which are then converted to radians, `horzAsRad` and `vertAsRad`. These two ratios can then be turned into separate 2D locations on the major circle and minor circle respectively. These two 2D circle locations are then added together. The major circle is treated as laying flat in the **x**&#10799;**y** plane. Note that the **x** component of the minor circle is transformed before addition, as the position *along* the major circle determines its orientation in 3D space.\n\n![Torus_Diagram.PNG](\/\/\/raw\/56b\/2\/z\/2c05.png)\n\nTo correctly orient objects along the surface of the torus we also need to know the normal vector for a given location on the torus map. This was achieved by normalizing the offset from the 3D location retrieved from the function above and a 3D location on the major circle; though in retrospect there are more efficient methods by which to calculate the surface normal. \n\nMoving objects along the surface of the was achieved using a simple 2D RK4 integrator; the details of which I won\u2019t include here, but see the link below for more details.\n\nhttp:\/\/gafferongames.com\/game-physics\/integration-basics\/\n\nThis however lead to an issue: moving horizontally on the torus map would give different 3D velocities depending on the vertical component of the 2D location. Mapping from the 2D rectangle to the 3D torus introduced distortion which affected the horizontal components of distances; luckily this distortion could be represented and corrected in the evaluation step of the RK4 integrator by scaling the **x** component of the entity\u2019s torus map velocity by a factor given by the following function:\n\n```\npublic static float getHorizontalMapDistortion (float verticalPosition) {\n\tfloat vertAsRad = (verticalPosition \/ MAP_HEIGHT) * (2 * Mathf.PI);\n\tfloat offset = Mathf.Cos(vertAsRad);\n\n    return 1 - (offset * (MINOR_RADIUS \/ MAJOR_RADIUS)); \n}\n```\n\nIn effect, this function returns the ratio between the radius of the major circle and the radius of the circle created by a horizontal line placed at the given vertical position on the torus map. \n\nWith these functions available it became easy to orient entities on the torus surface along their direction of travel. This was handled by the inbuilt Unity function `Transform.LookAt(Transform target, Vector3 worldUp)`, which rotates the operand transform so its *forward* vector aims at the `target` transform and then rotates the operand transform so its *up* vector aims in the direction given by the `worldUp` parameter. The `target` transform was an empty object whose position was set by the `get3DLocationFor2DLocation` function; the parameter to which was the entity's 2D position plus its normalized velocity. The value of the `worldUp` parameter is the surface normal for the entity's position on the torus surface.\n\nPositioning the camera above the player object was done by directly setting the camera\u2019s position to be some distance along the normal vector from the player\u2019s current position. The camera\u2019s orientation was set by  the `Transform.LookAt()` function; though the camera\u2019s own *up* vector, transformed into world space, was used as the `worldUp` parameter. This allows the camera to smoothly follow the player around the surface without any jarring discontinuities in orientation or position.\n\n### Controlling Entities on the Torus Surface\n\nOne problem which gave me considerable trouble was that of relating camera local directions to directions on the torus surface. After a few dead ends, the procedure that is present in the final game is as follows: find a *local* 3D basis for the torus surface in world space, take a camera local direction and transform it into world space, and find the projection of this camera local direction onto the local 3D basis for the torus surface. \nThis local 3D basis for the torus surface is constructed for a given location on the torus map and is comprised of the surface normal at the given location, and two 3D offsets found by adding to the horizontal and vertical components of the given torus map location. This set of vectors is then orthonormalized with the inbuilt `Vector3.Orthonormalize()` function. In effect, this set is used to describe the 3D directions which correspond to the *up* and *right* directions on the torus map.\n\nThere is a useful assumption that can be made; the camera is positioned along the surface normal and is oriented so that its *forward* vector is parallel with the surface normal. Due to this assumption, we can ignore the **z** direction of the camera and the vector in the local 3D torus surface basis which corresponds to the surface normal. This means that our directional input is constrained to the camera local **x**&#10799;**y** plane. We use the inbuilt `Transform.TransformDirection()` function, which maps the input direction from camera local space to world space. From here we can project this direction onto the two remaining vectors in the local 3D torus surface basis by taking the dot products of this world space vector against the *right* and *up* vectors respectively. \n\n```\n\tVector3 torusHorz = ToroidMap.getTorusMapRightDirection (this.torusMapPos);\n\tVector3 torusVert = ToroidMap.getTorusMapUpDirection (this.torusMapPos);\n\tVector3 torusNorm = ToroidMap.getNormalVectorForMapLocation (this.torusMapPos);\n\n\tVector3.OrthoNormalize (ref torusNorm, ref torusVert, ref torusHorz);\n\n\tVector2 torusMapDirection = new Vector2 \n\t(\tVector3.Dot (worldSpaceDirection, torusHorz), \n\t\t Vector3.Dot (worldSpaceDirection, torusVert));\n\t\t\t\n```\n\nThis procedure allows the player to indicate directions for movement and weapons fire in 2D intuitively. In retrospect, there is an analytic solution to finding the local 3D surface basis.\n\n### Summary\n\nI very much enjoyed LD 38, and I was pleased with the end result of my entry. As with any game jam, there are always things I would have approached differently if I were to create this again. I hope this post has answered any questions about the implementation of this game you may have had.","comments":0,"created":"2017-04-29T19:39:05Z","files":[],"files-timestamp":0,"id":29017,"love":6,"love-timestamp":"2017-05-01T00:29:38Z","meta":[],"modified":"2017-05-01T00:29:38Z","name":"P L A N E T O R U S   Retrospective","node-timestamp":"2017-04-30T22:57:59Z","parent":19868,"parents":[1,5,9,9405,19868],"path":"\/events\/ludum-dare\/38\/planetorus\/p-l-a-n-e-t-o-r-u-s-retrospectivve","published":"2017-04-30T22:27:02Z","scope":"public","slug":"p-l-a-n-e-t-o-r-u-s-retrospectivve","subsubtype":"","subtype":"","type":"post","version":72535},"node_metadata":{"n_key":"29017","n_urlkey":"304834","n_parent":"19868","n_path":"\/events\/ludum-dare\/38\/planetorus\/p-l-a-n-e-t-o-r-u-s-retrospectivve","n_slug":"p-l-a-n-e-t-o-r-u-s-retrospectiv","n_type":"post","n_subtype":"","n_subsubtype":"","n_author":"11109","n_created":"1493494745","n_modified":"1493598578","n_version":"72535","n_status":"WAYBACK"},"source_url":"https:\/\/ldjam.com\/events\/ludum-dare\/38\/planetorus\/p-l-a-n-e-t-o-r-u-s-retrospectivve","text":"# \"Wait, how did you ...?\"\n\nMy approach to making games has always been focused on solving problems I consider interesting. In that same vein, this post is focused on those aspects of my entry **P L A N E T O R U S** that I found interesting and enjoyed solving. As a disclaimer, a lot of the code in this post can no doubt be optimized or in some cases replaced with analytic solutions; but that's how game jams go.\n\nBefore reading on, I suggest you take a chance to play my entry so this post has context.\n\nhttps:\/\/ldjam.com\/events\/ludum-dare\/38\/planetorus\n\n### How to Address the Torus Surface\nThe surface of a torus can be represented as a 2D surface which wraps around at the seams. Towards this end we can define a rectangular map with an arbitrary `MAP_WIDTH` and `MAP_HEIGHT`. These lead to a `MAJOR_RADIUS` and `MINOR_RADIUS` for the torus as calculated below.\n```\nMAJOR_RADIUS = MAP_WIDTH  * Mathf.PI * 2\nMINOR_RADIUS = MAP_HEIGHT * Mathf.PI * 2\n```\nA 2D location on this map can be converted to a location in 3D on the surface of the torus with the following procedure.\n\n```\npublic static Vector3 get3DLocationFor2DLocation (Vector2 mapLocation) {\n\n\t\tfloat vertAsRad = (mapLocation.y \/ MAP_HEIGHT) * (2 * Mathf.PI);\n\t\tVector2 minorCircle = new Vector2 (Mathf.Cos(vertAsRad), Mathf.Sin(vertAsRad));\n\t\tminorCircle *= MINOR_RADIUS;\n\n\t\tfloat horzAsRad = (mapLocation.x \/ MAP_WIDTH) * (2 * Mathf.PI);\n\t\tVector2 majorCircle = new Vector2 (Mathf.Cos (horzAsRad), Mathf.Sin (horzAsRad));\n\t\tmajorCircle *= MAJOR_RADIUS;\n\n\t\tVector3 result = new Vector3 \n\t\t\t( majorCircle.x + (Mathf.Cos(horzAsRad) * minorCircle.x),\n\t\t\t  minorCircle.y,\n\t\t\t  majorCircle.y + (Mathf.Sin(horzAsRad) * minorCircle.x)\n\t\t\t);\n\n\t\treturn result;\n}\n```\n\nEssentially, we find the `x,y` map location as a ratio of the map width and height; which are then converted to radians, `horzAsRad` and `vertAsRad`. These two ratios can then be turned into separate 2D locations on the major circle and minor circle respectively. These two 2D circle locations are then added together. The major circle is treated as laying flat in the **x**&#10799;**y** plane. Note that the **x** component of the minor circle is transformed before addition, as the position *along* the major circle determines its orientation in 3D space.\n\n![Torus_Diagram.PNG](\/\/\/raw\/56b\/2\/z\/2c05.png)\n\nTo correctly orient objects along the surface of the torus we also need to know the normal vector for a given location on the torus map. This was achieved by normalizing the offset from the 3D location retrieved from the function above and a 3D location on the major circle; though in retrospect there are more efficient methods by which to calculate the surface normal. \n\nMoving objects along the surface of the was achieved using a simple 2D RK4 integrator; the details of which I won\u2019t include here, but see the link below for more details.\n\nhttp:\/\/gafferongames.com\/game-physics\/integration-basics\/\n\nThis however lead to an issue: moving horizontally on the torus map would give different 3D velocities depending on the vertical component of the 2D location. Mapping from the 2D rectangle to the 3D torus introduced distortion which affected the horizontal components of distances; luckily this distortion could be represented and corrected in the evaluation step of the RK4 integrator by scaling the **x** component of the entity\u2019s torus map velocity by a factor given by the following function:\n\n```\npublic static float getHorizontalMapDistortion (float verticalPosition) {\n\tfloat vertAsRad = (verticalPosition \/ MAP_HEIGHT) * (2 * Mathf.PI);\n\tfloat offset = Mathf.Cos(vertAsRad);\n\n    return 1 - (offset * (MINOR_RADIUS \/ MAJOR_RADIUS)); \n}\n```\n\nIn effect, this function returns the ratio between the radius of the major circle and the radius of the circle created by a horizontal line placed at the given vertical position on the torus map. \n\nWith these functions available it became easy to orient entities on the torus surface along their direction of travel. This was handled by the inbuilt Unity function `Transform.LookAt(Transform target, Vector3 worldUp)`, which rotates the operand transform so its *forward* vector aims at the `target` transform and then rotates the operand transform so its *up* vector aims in the direction given by the `worldUp` parameter. The `target` transform was an empty object whose position was set by the `get3DLocationFor2DLocation` function; the parameter to which was the entity's 2D position plus its normalized velocity. The value of the `worldUp` parameter is the surface normal for the entity's position on the torus surface.\n\nPositioning the camera above the player object was done by directly setting the camera\u2019s position to be some distance along the normal vector from the player\u2019s current position. The camera\u2019s orientation was set by  the `Transform.LookAt()` function; though the camera\u2019s own *up* vector, transformed into world space, was used as the `worldUp` parameter. This allows the camera to smoothly follow the player around the surface without any jarring discontinuities in orientation or position.\n\n### Controlling Entities on the Torus Surface\n\nOne problem which gave me considerable trouble was that of relating camera local directions to directions on the torus surface. After a few dead ends, the procedure that is present in the final game is as follows: find a *local* 3D basis for the torus surface in world space, take a camera local direction and transform it into world space, and find the projection of this camera local direction onto the local 3D basis for the torus surface. \nThis local 3D basis for the torus surface is constructed for a given location on the torus map and is comprised of the surface normal at the given location, and two 3D offsets found by adding to the horizontal and vertical components of the given torus map location. This set of vectors is then orthonormalized with the inbuilt `Vector3.Orthonormalize()` function. In effect, this set is used to describe the 3D directions which correspond to the *up* and *right* directions on the torus map.\n\nThere is a useful assumption that can be made; the camera is positioned along the surface normal and is oriented so that its *forward* vector is parallel with the surface normal. Due to this assumption, we can ignore the **z** direction of the camera and the vector in the local 3D torus surface basis which corresponds to the surface normal. This means that our directional input is constrained to the camera local **x**&#10799;**y** plane. We use the inbuilt `Transform.TransformDirection()` function, which maps the input direction from camera local space to world space. From here we can project this direction onto the two remaining vectors in the local 3D torus surface basis by taking the dot products of this world space vector against the *right* and *up* vectors respectively. \n\n```\n\tVector3 torusHorz = ToroidMap.getTorusMapRightDirection (this.torusMapPos);\n\tVector3 torusVert = ToroidMap.getTorusMapUpDirection (this.torusMapPos);\n\tVector3 torusNorm = ToroidMap.getNormalVectorForMapLocation (this.torusMapPos);\n\n\tVector3.OrthoNormalize (ref torusNorm, ref torusVert, ref torusHorz);\n\n\tVector2 torusMapDirection = new Vector2 \n\t(\tVector3.Dot (worldSpaceDirection, torusHorz), \n\t\t Vector3.Dot (worldSpaceDirection, torusVert));\n\t\t\t\n```\n\nThis procedure allows the player to indicate directions for movement and weapons fire in 2D intuitively. In retrospect, there is an analytic solution to finding the local 3D surface basis.\n\n### Summary\n\nI very much enjoyed LD 38, and I was pleased with the end result of my entry. As with any game jam, there are always things I would have approached differently if I were to create this again. I hope this post has answered any questions about the implementation of this game you may have had.","title":"P L A N E T O R U S   Retrospective","wayback_source":[]}