实时页面

实时页面与服务器持续通讯并实时更新,让实时游戏成为可能。实时页面对于多位用户间存在很多交互或单人快速重复的游戏非常适合。

这里 有很多例子。

向服务器发送数据

在你模板的JavaScript代码中,调用 liveSend() 函数来向服务器发送数据。例如,代表用户提交一个值为99的出价:

liveSend(99);

定义一个函数接收此消息。它的参数就是所发送的数据。

class MyPage(Page):
    @staticmethod
    def live_method(player, data):
        print('received a bid from', player.id_in_group, ':', data)

如果你使用了 oTree Studio,你必须定义一个函数名以 live_ 开头的玩家函数。(注意, WaitPage 上的 live_method 尚未被支持。)

向页面发送数据

为将数据发回,返回一个字典,键为玩家的ID以收到消息。例如,下面是一个简单的方法,发送“thanks”给任何发送消息的人。

def live_method(player, data):
    return {player.id_in_group: 'thanks'}

为向多位玩家发送消息,使用他们的 id_in_group 作为键即可。举例来说,下面的函数将消息发送给玩家2和3:

def live_method(player, data):
    return {2: data, 3: data}

向全组进行广播,使用 0 作为键(这是特殊情况,不是某个 id_in_group)。

def live_method(player, data):
    return {0: data}

在你的JavaScript代码中,定义函数 liveRecv。此函数在每次收到服务器的消息时会被自动调用。

function liveRecv(data) {
    console.log('received a message!', data);
    // your code goes here
}

例子:拍卖

class Group(BaseGroup):
    highest_bidder = models.IntegerField()
    highest_bid = models.CurrencyField(initial=0)

class Player(BasePlayer):
    pass
def live_method(player, bid):
    group = player.group
    my_id = player.id_in_group
    if bid > group.highest_bid:
        group.highest_bid = bid
        group.highest_bidder = my_id
        response = dict(id_in_group=my_id, bid=bid)
        return {0: response}
<table id="history" class="table">
<tr>
  <th>Player</th>
  <th>Bid</th>
</tr>
</table>
<input id="inputbox" type="number">
<button type="button" onclick="sendValue()">Send</button>

<script>

  let history = document.getElementById('history');
  let inputbox = document.getElementById('inputbox');

  function liveRecv(data) {
      history.innerHTML += '<tr><td>' + data.id_in_group + '</td><td>' + data.bid + '</td></tr>';
  }

  function sendValue() {
    liveSend(parseInt(inputbox.value));
  }

</script>

(注意,在JavaScript中 data.id_in_group == data['id_in_group']。)

数据

你所发送和接受的数据可以是任意数据类型(只要其能够被JSON序列化)。举例来说下面均是合法的:

liveSend(99);
liveSend('hello world');
liveSend([4, 5, 6]);
liveSend({'type': 'bid', 'value': 10.5});

最多用途的数据类型是字典,它可以让你包含多种元数据,尤其是消息的类型:

liveSend({'type': 'offer', 'value': 99.9, 'to': 3})
liveSend({'type': 'response', 'accepted': true, 'to': 3})

此时你就可以用 if 语句来处理不同种类的消息:

def live_method(player, data):
    t = data['type']
    if t == 'offer':
        other_player = data['to']
        response = {
            'type': 'offer',
            'from': player.id_in_group,
            'value': data['value']
        }
        return {other_player: response}
    if t == 'response':
        # etc
        ...

历史

默认情况下,参与者不会看见他们到达页面之前发送的消息。(并且如果刷新页面数据并不会重新发送。)如果你希望保存历史,你应当将其存储在数据库中。当玩家加载页面时,你的JavaScript可以调用像 liveSend({}) 的东西,并且你可以配置你自己的实时方法来从数据库中提取游戏的历史。

附加模型

实时页面常与 ExtraModel 一同使用,这使得你可将每条独立的消息或行为存入数据库中。

让用户停留在页面上

假定玩家在进行下一个页面之前你需要先发送10条消息。

首先,将 {{ next_button }} 删除。(或使用JS将其隐藏直到工作完成。)

当信息发送完成后,发送一条消息:

class Group(BaseGroup):
    num_messages = models.IntegerField()
    game_finished = models.BooleanField()


class MyPage(Page):
    def live_method(player, data):
        group = player.group
        group.num_messages += 1
        if group.num_messages >= 10:
            group.game_finished = True
            response = dict(type='game_finished')
            return {0: response}

然后在模板中,通过JavaScript自动提交页面:

function liveRecv(data) {
    console.log('received', data);
    let type = data.type;
    if (type === 'game_finished') {
        document.getElementById("form").submit();
    }
    // handle other types of messages here..
}

顺带一提,使用相同的技巧,你可以实现一个自定义的等待页面,例如在某个时间之后才会进行下一页面,即使所有玩家均已到达的等待页面。

关于实时页面的常规建议

下面是一些常规建议(并不适用于所有情况)。我们推荐使用Python实现大多数逻辑,并只使用JavaScript来更新页面的HTML,因为:

  • 正确使用JavaScript可能是十分具有技巧性的
  • 你的Python代码运行在服务器上,是中心化和可靠的。JavaScript运行在客户端上,可能会与其他客户端失去同步,而且数据在页面关闭或者重载的情况下可能会丢失。
  • 由于Python代码是运行在服务器上的,故它更加安全并不会被参与人看到或修改。

例子:井字棋

假定你想要实现一个井字棋游戏。你的live_method可能会收到两种消息:

  1. 一位用户标记了一个方格,故你需要通知其他玩家
  2. 一位用户载入(或重载)了页面,故你需要将当前的局面发给他们。

对于情况1,你应当使用JavaScript事件处理器如 onclick,例如当用户点击方格3时,此操作会被发送给服务器:

liveSend({square: 3});

对于情况2,推荐将类似下面的代码放入你的模板中,以在页面载入时发送一条空消息到服务器:

document.addEventListener("DOMContentLoaded", (event) => {
    liveSend({});
});

服务器通过“if”语句处理这两种情况:

def live_method(player, data):
    group = player.group

    if 'square' in data:
        # SITUATION 1
        square = data['square']

        # save_move should save the move into a group field.
        # for example, if player 1 modifies square 3,
        # that changes group.board from 'X O XX  O' to 'X OOXX  O'
        save_move(group, square, player.id_in_group)
        # so that we can highlight the square (and maybe say who made the move)
        news = {'square': square, 'id_in_group': player.id_in_group}
    else:
        # SITUATION 2
        news = {}
    # get_state should contain the current state of the game, for example:
    # {'board': 'X O XX  O', 'whose_turn': 2}
    payload = get_state(group)
    # .update just combines 2 dicts
    payload.update(news)
    return {0: payload}

情况2中(用户载入页面),客户端获得一条类似下面的消息:

{'board': 'X OOXX  O', 'whose_turn': 2}

情况1中,玩家获得其他玩家刚刚进行的操作,与当前的局面。

{'board': 'X OOXX  O', 'whose_turn': 2, 'square': square, 'id_in_group': player.id_in_group}

JavaScript代码是“被动”的。它不需要追踪是谁进行了操作;它只是相信从服务器收到的信息。它甚至可以在每次收到一条消息的时候重绘界面。

你的代码需要验证用户输入。例如,如果玩家1在属于玩家2的回合行动,你就需要屏蔽这样的操作。出于之前列出的原因,更推荐将此逻辑放在你的live_method中,而不是在JavaScript代码中。

小结

正如上面说明的,live_method的典型模式如下:

if the user made an action:
    state = (get the current state of the game)
    if (action is illegal/invalid):
        return
    update the models based on the move.
    news = (produce the feedback to send back to the user, or onward to other users)
else:
    news = (nothing)
state = (get the current state of the game)
payload = (state combined with news)
return payload

注意到我们两次获取了游戏的状态。这是因为当我们更新模型的时候状态发生了改变,故我们需要刷新它。

故障排除

如果你在页面载入完成之前调用了 liveSend ,你会得到一个错误如 liveSend is not defined。所以先等待 DOMContentLoaded (或jQuery document.ready,等等):

window.addEventListener('DOMContentLoaded', (event) => {
    // your code goes here...
});

不要让用户点击“下一页”按钮时触发 liveSend ,因为离开页面可能会打断 liveSend。相反的,应当让用户点击一个普通按钮时触发 liveSend,然后在 liveRecv 中使用 document.getElementById("form").submit();