4.使用模運算來實現世界的捲動

4.Teaching The World To Wrap: using modular arithmetric to make the world wrap around

This post is about wrapping coordinates so that when you go off one side of the world, you come back on the opposite side, or: one use of modular arithmetic.

這篇帖子討論座標的捲動,這樣當你走到遊戲世界的一端時可以從另一邊走出來,或者說:討論模運算的使用。

A feature that used to be quite prevalent in 2D games was a “wrapped” world: when you reached the right-hand side of the world, instead of stopping or bouncing off, you would appear on the left-hand side (and vice versa). The top and the bottom of the world would usually operate in a similar way. Asteroids is a classic example, but various other arcade and strategy (Settlers, Civ, etc) games have used the same mechanism.

過去在2D遊戲中非常普遍的特徵就是“捲動的”世界:當你抵達世界的右邊界時並不會停下來活彈回來,而是會出現在左邊界(反之亦然)。對於遊戲世界的上端和下端通常也做類似的處理。星球大戰是一個經典的例子,此外其他各種街機遊戲和策略遊戲也使用同樣的機制。

The Simple Method

一個簡單的方法

The simple way to implement world-wrapping is to check if the location you are trying to move to is beyond the bounds of the world: if it is, then move to the opposite border of the world.

實現世界捲動的簡單方法是檢測移動目的地是否接近了世界的邊緣:若是,則移動到世界的另一邊。

In Greenfoot, we must be careful about how to implement this, because Greenfoot already prevents your location from going outside the world. So if you try to implement world-wrapping along these lines:

在Greenfoot中去實現這個功能時必須要小心,因爲Greenfoot是禁止座標超出世界邊緣的。比如按照以下代碼去實現世界捲動:

setLocation(getX() - 5, getY()); // Move 5 units left
if (getX() < 0)
    ...

The above won’t work: the setLocation method will already have prevented you moving beyond x=0, so getX() will never be negative.

上面的代碼不能正確運行:setLocation方法會阻止移動時的x值小於0,因此getX()將不可能會負數值。

Instead this check must be done before you tell Greenfoot to set the location. The easiest way to do this is to override (i.e. replace) the setLocation method to add in this check, before calling the original setLocation method. Here’s the code for wrapping just the x coordinate:

除此之外,檢測工作必須在Greenfoot設定位置之前進行。最簡便的辦法是覆蓋(即替換)setLocation方法,並在調用原始的setLocation方法之前加入檢測的代碼。以下僅是x座標捲動的代碼:

    public void setLocation(int x, int y)
    {
        if (x >= getWorld().getWidth())
        {
            x = 0;
        }
        if (x < 0)
        {
            x = getWorld().getWidth() - 1;
        }
        
        super.setLocation(x, y);
    }

The first if-statement checks if the desired X coordinate is greater than or equal to the world width (say, 640). If it is, it is moved to the opposite side: x=0. The second if-statement checks if the desired X coordinate is negative, and if it is, it is moved to the maximum X coordinate: world-width minus one (e.g. 639). Finally, we pass the potentially-adjusted coordinates to the original setLocation method.

第一個if語句檢測當前的x座標值是否大於等於世界的寬度(比如640)。若是,則移動到相對的一邊:x=0.第二個if語句檢測當前x座標值是否爲負數,若是則移動到x座標的最大值處:世界的寬度值減1(比如639)。最後我們將調整過後的座標值傳給原始的setLocation 方法。

The logic for the Y coordinates is identical, but using the world height instead of width. I’ve uploaded the Greenfoot scenario with simple X and Y wrapping implemented for you tohave a play, or todownload and view the source.

對y座標的處理方法是一致的,但是使用世界的高度值來替代寬度值。我上傳了一個遊戲劇本是關於x和

y座標捲動的,你可以試玩一下,或者下載後查看源代碼。

The More Accurate Method

更加精確的方法

There is a small issue with our previous method. Let’s say you have a small, 20-wide world, and you are at position (14, 2), heading exactly right at 5 units per frame. You’ll move to (19, 2) — no problem. Then the next frame after that, you’ll try to move to (24, 2), and then the above code will kick in and you’ll get placed at (0, 2). Here’s a diagram of that:

關於之前問題有一個小小的爭議。我們假設這兒有一個20個單位寬的小型世界,你所處的位置在(14,2),以每幀5個單位的速度向右移動。毫無疑問你將會移動到(19.2)處。然後下一幀你將會移動到(24,2)處,但是由於上面代碼的作用,你將會到達(0,2)處。效果如圖:

From this diagram, the problem may not be totally obvious. But since our world is wrapped, it should be as if the left-hand edge is directly next to the right-hand edge. When we draw it like that, we can see the problem:

從這個圖來看,問題並不是那麼明顯。然而由於世界是捲動的,其左邊緣應該可看作是是直接與右邊緣相鄰的。當我們用下圖表現的話便會發現問題:

From this diagram, you can see that in effect we only moved 1 unit rather than 5. So every time you cross the world boundary, you have a “slow frame” where you don’t move as far. This is not very noticeable with low speeds, but it can become noticeable with higher speeds, or if you have multiple actors in formation. If we were at (19, 2), we should have moved to (4, 2). Similarly, if we were at (18, 2), we should have moved to (3, 2), and so on. You’ll notice the pattern is that we are subtracting 20 (the world width) from the coordinate we would normally move to. So (19, 2) should move to (24, 2), but we instead move to (4, 2). You can also see that the logic works the other way: (4, 2) should move to (-1, 2) if they are moving 5 units left, but instead they move to (19, 2). So in that case we add on the world width. Let’s set this down in code:

從該圖可以看到,我們事實上只移動了1個單位而不是5個。因此每當你越過世界邊緣時,你將會遇到一個“慢幀”,它要小於平時的移動距離。在速度慢時這個問題不太明顯,而速度快時或有多個角色組隊移動時就會很明顯。如果你處在(19,2),我們應該移動到(4,2)。類似地,如果我們處在(18,2),我們應該移動到(3,2),等等。你會注意到一個模式,即我們把移動目標的座標值減去了20(世界的寬度值)。因而(19,2)本來應該移動到(24,2),但替換後我們移動到(4,2)。該邏輯可以表現爲另一種形式:如果向左移動5個單位,則應該從(4,2)移動到(-1,2),但是替換後我們移動到(19,2)。於是在那種情況下我們加上世界的寬度值。代碼如下:

    public void setLocation(int x, int y)
    {
        int width = getWorld().getWidth();
        int height = getWorld().getHeight();
        
        if (x >= width)
        {
            x -= width;
        }
        if (x < 0)
        {
            x += width;
        }

        if (y >= height)
        {
            y -= height;
        }
        if (y < 0)
        {
            y += height;
        }
        
        super.setLocation(x, y);
    }


You can go have a play with this improved version. And as a quick exercise: this code isn’t completely foolproof. See if you can work out why not and figure out the simplest fix, before you take a look at the source code (which does have the foolproof answer).

你可以去試玩一下這個改進的版本。作爲一個簡單的演示,以上代碼並不是萬無一失的。看看你是否能夠在查看源代碼(沒使用最優方案)之前找到問題所在並用最簡單的方式解決。

The Technical Term

術語

This wrapping of coordinates is an example of modular arithmetic (a quick overview is available onSimple Wikipedia). Other examples of modular arithmetic include hours on the 24-hour clock: after 23 you go back to 0, or degrees: after 359 degrees you go back to 0 degrees. And in fact, if you know a little about computer arithmetic, you’ll know that addition and subtraction on many finite integer types in computing resemble modular arithmetic: for example, if you try to add 1 to a byte value of 127 in Java, you’ll get -128. There are several more uses of modular arithmetic in computing, and I will come back to some of them in future.

以上座標捲動是關於模運算的一個簡單示例。其他模運算的例子包括24制時鐘:23點過後回到0點,或者角度值:359度之後回到0度。事實上,如果你對計算機的算術運算稍有了解的話,你便會知道許多有限整型的加減運算類似於模運算:比如說,java中如果要給值爲127的字節類型加1的話,將會得到-128.計算機信息處理中很多地方使用到模運算,我將在今後進行討論。

As a final note, some of you may know that Java has a “modulo” operator — but that operator is not very useful for solving the problem in this post. I leave it to you as an exercise to figure out why not. (Why not try modifying my code to use the modulo operator?)

最後一點說明,有些人或許知道java中有取模操作符——但是該操作符並不適用解決本帖中的內容。作爲一個練習,我將這個問題留給你思考。(何不試試用取模操作符替代我的代碼?)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章