随着游戏行业规模的不断扩大,创造独特的动态用户体验变得越来越重要。由于其粉丝群,企业必须最大限度地提升多人游戏体验,以推动客户获取和留存。然而,企业在尝试扩展多人游戏时面临着许多障碍,所有这些障碍都可以通过 Redis 解决。
个性化的交互和高速反应是创造独特用户体验的核心。Redis 为游戏发行商提供强大的数据库,可以支持低延迟的游戏用例。最近,一个 Launchpad 应用程序构建了一个独特的应用程序,由于其在数据传输方面的无与伦比的速度,该应用程序只能通过 Redis 部署。
这一点至关重要,因为参与者来自世界各地。玩家之间的交互必须实时执行才能支持游戏玩法,要求延迟小于一毫秒。
让我们深入探讨一下这是如何实现的。但在我们这样做之前,请务必浏览我们Launchpad上激动人心的各种不同应用程序。
您将使用 Redis 构建一个实时地理分布式多人俯视角街机射击游戏。应用程序的核心是 Redis,因为它充当高效的实时数据库,使您能够存储和分发数据。
随着我们按时间顺序逐步完成每个阶段,我们将解释重要的术语以及每个 Redis 函数。
让我们确定创建此游戏所需的不同组件。应用程序包含 3 个主要组件
这个多人游戏的核心思想是使其实时并地理分布。这意味着集群中的所有实例都应保持更新,以避免出现不同步情况。现在让我们看一下架构。
现在让我们看一下架构的流程。
git clone https://github.com/redis-developer/online_game/
在存储库的根目录下,您会找到一个 Docker compose YAML 文件
version: '3.8'
services:
redis:
build:
dockerfile: ./dockerfiles/Dockerfile_redis
context: .
environment:
- ALLOW_EMPTY_PASSWORD=yes
- DISABLE_COMMANDS=FLUSHDB,FLUSHALL,CONFIG,HSCAN
volumes:
- ./redis/redis_data:/data
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
ports:
- 6379:6379
restart: always
backend:
build:
dockerfile: ./dockerfiles/Dockerfile_node_backend
context: .
environment:
- NODE_ENV=development
volumes:
- ./game_conf.json:/game_config/game_conf.json
ports:
- 8080:8080
- 8082:8082
restart: always
在此 YAML 文件中,定义了两个主要服务 - redis 和 backend。
以下是 Redis 的 Dockerfile 的样子
FROM redislabs/redismod:latest
COPY ./redis/redis_functions /functionsCOPY ./redis/start_redis.sh /start_redis.sh
RUN chmod +x /start_redis.sh
ENTRYPOINT ["bash"]CMD ["/start_redis.sh"]
以下是 NodeJS 后端的 Dockerfile 的样子
FROM node:15.14WORKDIR /home/node/app
COPY ./app/package*.json ./RUN npm install --only=production
COPY ./app .CMD [ "npm", "start" ]
启动服务
从 online_game 目录运行以下命令
docker-compose up
您可以通过 http://127.0.0.1:8080 访问在线 Web 服务器
有三个 RedisGears 函数,每个函数都有一组子函数
当用户启动游戏时,用户将首先使用 RediSearch 搜索游戏。如果游戏存在,那么该用户将尝试加入游戏。否则,将触发 RedisGears 创建新游戏。请查看以下函数
def find_game(user_id):
game = query()
if game != [@] and type(game) == list:
return game[1].split(":") [1]
# CREATE A NEW GAME IF THERE ARE NO GAMES
game = execute("RG.TRIGGER", "create_new_game", f"USER:{user_id}")
if game:
return game[@]
(
GB( 'CommandReader ' )
.map(lambda x: find_game(*x[1:]))
.register(trigger=self.command_name
)
当用户创建新游戏后,其他玩家将加入,每个人都可以玩游戏。
class CreateNewGameFunctionBuilder(BaseFunctionBuilder):
def __init__(self):
super().__init__(command_name='create_new_game')
def register_command(self):
"""
Registers create_new_game redis gears fucntion to redis.
For each generate_new_game call creates a new HASH under game namespace:
GAME:[game_id] owner [user_id], secret [hash], private [bool], playercount [int]
Returns:
redis key [GAME:game_id]
Trigger example:
RG.TRIGGER create_new_game USER:123 1 secret123
"""
def subcall(user, private=0, secret=""):
game_id = uuid.uuid4().hex
key = f"GAME:{game_id}"
execute("HSET", key, "owner", user, "secret", str(secret), "private", int(private), "playercount", 0)
execute("EXPIRE", key, SECONDS_IN_DAY)
return game_id
(
GB('CommandReader')
.map(lambda x: subcall(*x[1:]))
.register(trigger=self.command_name, mode='sync')
)
如果不存在游戏,则将触发 RedisGears 创建新游戏。
当用户创建新游戏后,其他玩家将加入,每个人都可以玩游戏。
class CreateUserFunctionBuilder(BaseFunctionBuilder):
def __init__(self):
super().__init__(command_name='create_new_user')
def register_command(self):
"""
Registers create_new_user redis gears fucntion to redis.
For each create_new_user call creates a new HASH under user namespace:
USER:[u_id] name [str], settings [str], secret [str]
Returns:
redis key [USER:u_id]
Trigger example:
RG.TRIGGER create_new_user hhaa Player1 '' aahh
"""
def subcall(user_id, name, settings='{}', secret=""):
key = f"USER:{user_id}"
execute("HSET", key, "name", name, "setttings", settings, "secret", str(secret))
execute("EXPIRE", key, SECONDS_IN_DAY * 30)
return key
(
GB('CommandReader')
.map(lambda x: subcall(*x[1:]))
.register(trigger=self.command_name)
)
此函数将新玩家添加到游戏中,其方法与 Create_new_game 函数相同。同样,将触发 RedisGears,然后创建新用户
class JoinGameFunctionBuilder(BaseFunctionBuilder):
def __init__(self):
super().__init__(command_name='join_game')
def register_command(self):
"""
Determines best public server to join to.
- Assings User to the Game.
- Increments playercount
Arguments:
user, game, secret (optional)
Returns:
redis key [GAME:game_id]
Trigger example:
RG.TRIGGER join_game user1 game1
RG.TRIGGER join_game user1 game1 secret123
"""
当用户加入游戏时,将触发 RedisGears 以启用 Join_game 函数。这还会增加游戏实例的玩家数量 (HINCRBY)。
class LeaveGameFunctionBuilder(BaseFunctionBuilder):
def __init__(self):
super().__init__(command_name='leave_game')
def register_command(self):
"""
Determines best public server to join to.
- Removes USER to the ROOM.
- Decrements playercount
- Publishes a notification
Arguments:
user, game
Returns:
None
Trigger example:
RG.TRIGGER leave_game user1 game1
"""
def subcall(user_id, game_id, secret=None):
execute("HDEL", f"GAME:{game_id}", f"USER:{user_id}")
execute("HINCRBY", f"GAME:{game_id}", "playercount", -1)
(
GB('CommandReader')
.map(lambda x: subcall(*x[1:]))
.register(trigger=self.command_name, mode='sync')
)
当用户被淘汰出游戏或选择离开时,将触发 RedisGears 以促进该过程。这还会自动减少玩家数量,并自动创建通知以确认此操作已完成。
在游戏过程中,玩家将发射导弹来消灭其他对手。当玩家发射导弹时,将触发以下子函数
def click(self, game_id, user_id, x, y, o):
"""
Handle player main key pressed event.
"""
player = self.games_states[game_id]["players"][user_id]
self.games_states[game_id]["projectiles"].append({
"timestamp": self.ts, # server time
"x": player["x"] if player['x'] is not None else 9999,
"y": player["y"] if player['y'] is not None else 9999,
"orientation": o, # radians
"ttl": 2000, # ms
"speed": 1, # px/ms
"user_id": user_id
})
return True
如果导弹击中其他玩家,那么该用户将被淘汰出游戏。以下代码确定玩家是否被导弹击中。
def hit(self, game_id, user_id, enemy_user_id):
"""
Determines if the projectile has hit a user [user_id]
Extrapolates projectile position based on when projectile has spawned, and the time now.
Publishes a hit even if target is hit.
"""
projectiles = self.games_states[game_id]["projectiles"]
player = self.games_states[game_id]["players"][enemy_user_id]
for projectile in projectiles:
time_diff = self.ts - projectile['timestamp']
orientation = float(projectile["orientation"])
x = projectile['x'] + ( math.cos(orientation) * (projectile['speed'] * time_diff) )
y = projectile['y'] + ( math.sin(orientation) * (projectile['speed'] * time_diff) )
if abs(player['x'] - x < 50) and abs(player['y'] - y < 50):
self.games_states[game_id]['players'][projectile['user_id']]['score'] += 1
execute('PUBLISH', game_id, f"hit;{enemy_user_id}")
return False
return False
def respawn(self, game_id, user_id, x, y):
player = self.games_states[game_id]["players"][user_id]
player["respawns"] = player["respawns"] + 1
player["x"] = x
player["y"] = y
return True
MESSAGE_EVENT_HANDLERS | 说明 |
p (pose) 参数:[user_id, x, y, orientation]; | 客户端收到 user_id 位置更新 |
c(click) 参数:[user_id, x (点击位置), y (点击位置), angle (从玩家位置到点击位置的角度)]; | 客户端收到 user_id 点击事件 |
r (respawn) 参数:[user_id, x, y]; | 客户端收到 user_id 已重生 |
l (leave) 参数:[user_id]; | 客户端收到 user_id 已离开游戏 |
j (join) 参数:[user_id, x, y] | 客户端收到 user_id 已加入游戏,user_id 已在 (x, y) 位置生成 |
uid (user id) 参数:[is_valid]; | 客户端收到响应,确认是否可以“登录用户 |
gid (game id) 参数:[is_valid]; | 客户端收到响应,确认用户是否参与游戏(用户是否已授权) |
hit 参数:[user_id]; | 客户端收到消息,确认 user_id 已被击中/客户端可以从渲染中删除 user_id |
RediSearch 索引在容器启动时在 redis/start_redis.sh 中注册
已创建 Redis Search 索引
FT.CREATE GAME ON HASH PREFIX 1 GAME: SCHEMA owner TEXT secret TEXT private NUMERIC SORTABLE playercount NUMERIC SORTABLE
FT.CREATE USER ON HASH PREFIX 1 USER: SCHEMA name TEXT settings TEXT secret TEXT
FT.SEARCH "GAME" "(@playercount:[0 1000])" SORTBY playercount DESC LIMIT 0 1
与任何 Redis 数据库一样,其最受欢迎的资产之一是其以无与伦比的效率在组件之间传输数据的能力。但是,在此应用程序中,如果没有活动-活动提供的出色延迟速度,游戏将无法正常运行。
游戏玩法纯粹是交互式的,来自世界各地的用户会做出反应并向其他玩家发射导弹。从头到尾,RedisGears 根据应用程序设置的时序顺序部署一系列函数。
由于 RedisGears 的效率,这很容易实现,它使活动-活动地理分布式自上而下的街机射击游戏应用程序能够部署。
如果您想详细了解此激动人心的应用程序,您可以在 Redis Launchpad 上查看完整的应用程序。还要确保查看我们为您提供的 所有其他激动人心的应用程序。
Jānis Vilks
Jānis 是一位在 Shipping Technology 工作的大数据工程师。
如果您想了解更多关于他的工作以及他参与的项目,请确保 访问他的 GitHub 个人资料。