{"id":116,"date":"2015-02-07T19:07:41","date_gmt":"2015-02-07T19:07:41","guid":{"rendered":"http:\/\/cpscotti.com\/blog\/?p=116"},"modified":"2015-02-24T17:03:41","modified_gmt":"2015-02-24T17:03:41","slug":"swiftris-fixing-gravity","status":"publish","type":"post","link":"http:\/\/cpscotti.com\/blog\/?p=116","title":{"rendered":"Swiftris &#8211; Fixing Gravity"},"content":{"rendered":"<p>As I mentioned on my previous post, Swiftris&#8217; gravity is different than the <a href=\"http:\/\/en.wikipedia.org\/wiki\/Tetris#Gravity\" title=\"Original Tetris Implementation\">http:\/\/en.wikipedia.org\/wiki\/Tetris#Gravity<\/a> and that annoys anyone who played Tetris enough.<\/p>\n<p>On the original Switris source, the gravity\/collapsing behavior is implemented in &#8216;removeCompletedLines()&#8217; which is called (weirdly) from within GameViewController&#8217;s implementation of the &#8216;gameShapeDidLand()&#8217;. Let&#8217;s start by just reimplementing removeCompletedLines() without changing its interface.<\/p>\n<p>Here is the original implementation:<br \/>\n[sourcecode language=&#8221;javascript&#8221; title=&#8221;Swiftris.swift excerpt at the original removeCompletedLines()&#8221;]<br \/>\nfunc removeCompletedLines() -&gt; (linesRemoved: Array&lt;Array&lt;Block&gt;&gt;, fallenBlocks: Array&lt;Array&lt;Block&gt;&gt;) {<br \/>\n    var removedLines = Array&lt;Array&lt;Block&gt;&gt;()<br \/>\n    for var row = (NumRows &#8211; 1) ; row &gt; 0 ; row&#8211; {<br \/>\n        var rowOfBlocks = Array&lt;Block&gt;()<\/p>\n<p>        for column in 0..&lt;NumColumns {<br \/>\n            \/\/if block not null<br \/>\n            if let block = blockArray[column, row] {<br \/>\n                \/\/add to the rowOfBlocks<br \/>\n                rowOfBlocks.append(block)<br \/>\n            }<br \/>\n        }<br \/>\n        \/\/if amount of blocks equals the number of Columns<br \/>\n        if rowOfBlocks.count == NumColumns {<br \/>\n            removedLines.append(rowOfBlocks)<br \/>\n            for block in rowOfBlocks {<br \/>\n                blockArray[block.col, block.row] = nil<br \/>\n            }<br \/>\n        }<br \/>\n    }<\/p>\n<p>    if removedLines.count == 0 {<br \/>\n        return ([],[])<br \/>\n    }<\/p>\n<p>    let pointsEarned = removedLines.count * PointsPerLine * level<br \/>\n    score += pointsEarned<br \/>\n    if score &gt;= level * LevelThreshold {<br \/>\n        level += 1<br \/>\n        delegate?.gameDidLevelUp(self)<br \/>\n    }<\/p>\n<p>    var fallenBlocks = Array&lt;Array&lt;Block&gt;&gt;()<br \/>\n    for column in 0..&lt;NumColumns {<br \/>\n        var fallenBlocksArray = Array&lt;Block&gt;()<\/p>\n<p>        for var row = (removedLines[0][0]).row &#8211; 1; row &gt; 0; row&#8211; {<br \/>\n            if let block = blockArray[column, row] {<br \/>\n                var newRow = row<\/p>\n<p>                \/\/falls independently?<br \/>\n                while (newRow &lt; (NumRows-1) &amp;&amp; blockArray[column, newRow+1] == nil) {<br \/>\n                    newRow++<br \/>\n                }<\/p>\n<p>                block.row = newRow<br \/>\n                blockArray[column, row] = nil<br \/>\n                blockArray[column, newRow] = block<br \/>\n                fallenBlocksArray.append(block)<br \/>\n            }<br \/>\n        }<\/p>\n<p>        if fallenBlocksArray.count &gt; 0 {<br \/>\n            fallenBlocks.append(fallenBlocksArray)<br \/>\n        }<br \/>\n    }<\/p>\n<p>    return (removedLines, fallenBlocks)<br \/>\n}<br \/>\n[\/sourcecode]<\/p>\n<p>and this is my implementation (different name but same signatures so you can just replace in GameViewController):<br \/>\n[sourcecode language=&#8221;javascript&#8221; title=&#8221;A redesigned gravity\/collapsing lines with same signature&#8221;]<br \/>\nfunc removeFullLines() -&gt; (linesRemoved: Array&lt;Array&lt;Block&gt;&gt;, fallenBlocks: Array&lt;Array&lt;Block&gt;&gt;) {<br \/>\n        \/\/ Just so we can mock the original signature<br \/>\n        var externalRemovedBlocks = Array&lt;Array&lt;Block&gt;&gt;()<br \/>\n        var removedBlocks = Array&lt;Block&gt;()<\/p>\n<p>        \/\/ Just so we can mock the original signature<br \/>\n        var externalFallenBlocks = Array&lt;Array&lt;Block&gt;&gt;()<br \/>\n        var fallenBlocks = Array&lt;Block&gt;()<\/p>\n<p>        \/\/Iterating from bottom of the Tetris grid; this is the summary of that loop:<br \/>\n        \/\/  -&gt; dropHeight = 0 (number of dropped rows to that point in the loop)<br \/>\n        \/\/  for rows, bottom up<br \/>\n        \/\/    if row is full<br \/>\n        \/\/      -&gt; save that row&#8217;s blocks to removedBlocks<br \/>\n        \/\/      -&gt; set nil for the whole row<br \/>\n        \/\/      -&gt; incread dropHeight<br \/>\n        \/\/    else (row is not full)<br \/>\n        \/\/      if dropHeight &gt; 0<br \/>\n        \/\/        -&gt; lower that row by dropHeight rows        <\/p>\n<p>        var dropHeight:Int = 0<br \/>\n        for var row = (NumRows &#8211; 1) ; row &gt; 0 ; row&#8211; {<\/p>\n<p>            if isRowFilled(row) {<br \/>\n                for column in 0..&lt;NumColumns {<br \/>\n                    \/\/save row (we know its not null because its filled)<br \/>\n                    removedBlocks.append(blockArray[column, row]!)<\/p>\n<p>                    \/\/set nil<br \/>\n                    blockArray[column, row] = nil<br \/>\n                }<\/p>\n<p>                \/\/increase drop height<br \/>\n                dropHeight += 1<br \/>\n            } else {<br \/>\n                if dropHeight &gt; 0 {<br \/>\n                    \/\/lower row by drop height<br \/>\n                    for column in 0..&lt;NumColumns {<br \/>\n                        if let block = blockArray[column, row] {<br \/>\n                            block.row += dropHeight<br \/>\n                            blockArray[column, row] = nil<br \/>\n                            blockArray[column, block.row] = block<br \/>\n                            fallenBlocks.append(block)<br \/>\n                        }<br \/>\n                    }<br \/>\n                }<br \/>\n            }<br \/>\n        }<br \/>\n        externalRemovedBlocks.append(removedBlocks)<br \/>\n        externalFallenBlocks.append(fallenBlocks)<\/p>\n<p>        if dropHeight == 0 {<br \/>\n            return ([],[])<br \/>\n        }<\/p>\n<p>        let pointsEarned = dropHeight * PointsPerLine * level<br \/>\n        score += pointsEarned<br \/>\n        if score &gt;= level * LevelThreshold {<br \/>\n            level += 1<br \/>\n            delegate?.gameDidLevelUp(self)<br \/>\n        }<\/p>\n<p>        return (externalRemovedBlocks, externalFallenBlocks)<br \/>\n    }<br \/>\n[\/sourcecode]<\/p>\n<p>As you can see, the original Gravity is a lot simpler and makes the game harder &#038; more fun. Also, note that &#8216;removeFullLines()&#8217; has unnecessary complexity on its return values (Array of Arrays instead of simple Arrays) &#8211; we&#8217;ll fix that later.<br \/>\nAnd there&#8217;s a little problem with simply replacing &#8216;removeCompletedLines()&#8217; by &#8216;removeFullLines()&#8217;; removeFullLines returns all fallen blocks in the same\/first subarray of fallenBlocks; where the original one would return on subarray for each column. This causes a problem since the blockIdx within each of the subarrays is used to time a animation on the GameScene side so everything will work fine but the game will lock for a few seconds every time you fill a line (linear to the full amount of blocks falling), I plan to provide a way better fix for this later but for now this solves the immediate problem:<\/p>\n<p>[sourcecode language=&#8221;javascript&#8221; title=&#8221;GameScene.swift before&#8221;]<br \/>\n\/\/ &#8230;<br \/>\n    func collapsingLines(linesToRemove: Array&lt;Array&lt;Block&gt;&gt;, fallenBlocks: Array&lt;Array&lt;Block&gt;&gt;, completion:() -&gt; ()) {<br \/>\n    \/\/ &#8230;<br \/>\n    \/\/ &#8230;<br \/>\n                let delay = (NSTimeInterval(columnIdx) * 0.05) + (NSTimeInterval(blockIdx) * 0.05)<br \/>\n    \/\/\/ &#8230;<br \/>\n}<br \/>\n[\/sourcecode]<\/p>\n<p>To this:<br \/>\n[sourcecode language=&#8221;javascript&#8221; title=&#8221;GameScene.swift after&#8221;]<br \/>\n\/\/ &#8230;<br \/>\n    func collapsingLines(linesToRemove: Array&lt;Array&lt;Block&gt;&gt;, fallenBlocks: Array&lt;Array&lt;Block&gt;&gt;, completion:() -&gt; ()) {<br \/>\n    \/\/ &#8230;<br \/>\n    \/\/ &#8230;<br \/>\n                let delay = (NSTimeInterval(columnIdx) * 0.05) + (NSTimeInterval(1) * 0.05)<br \/>\n    \/\/\/ &#8230;<br \/>\n}<br \/>\n[\/sourcecode]<\/p>\n<p>Now the game works as it should but the architecture of it is still a mess and all my fixes are basically hacks inside it. For my next post I&#8217;ll try to redesign\/cleanup.. let&#8217;s hope for the best.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>As I mentioned on my previous post, Swiftris&#8217; gravity is different than the http:\/\/en.wikipedia.org\/wiki\/Tetris#Gravity and that annoys anyone who played Tetris enough. On the original Switris source, the gravity\/collapsing behavior is implemented in &#8216;removeCompletedLines()&#8217; which is called (weirdly) from within GameViewController&#8217;s implementation of the &#8216;gameShapeDidLand()&#8217;. Let&#8217;s start by just reimplementing removeCompletedLines() without changing its interface. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23,22,24],"tags":[],"class_list":["post-116","post","type-post","status-publish","format-standard","hentry","category-ios","category-swift","category-swiftris"],"_links":{"self":[{"href":"http:\/\/cpscotti.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/116","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/cpscotti.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/cpscotti.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/cpscotti.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/cpscotti.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=116"}],"version-history":[{"count":4,"href":"http:\/\/cpscotti.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/116\/revisions"}],"predecessor-version":[{"id":120,"href":"http:\/\/cpscotti.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/116\/revisions\/120"}],"wp:attachment":[{"href":"http:\/\/cpscotti.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=116"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/cpscotti.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=116"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/cpscotti.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=116"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}