16.4 环境准备
在开始编写程序代码之前需要完成环境准备的工作,主要包括安装依赖库、导入模块和配置环境等工作。在完成这些工作后,就可以开始编写和执行项目的代码,利用所导入的库和模块来实现项目的功能。环境准备是项目开发的重要一步,它确保了你的代码能够在特定的环境中顺利运行。
16.4.1 安装OpenAI gymnasium
本项目需要使用使用OpenAI的强化学习环境实现,从2021年开始,OpenAI的强化学习环境的接口从gym变成了gymnasium。本项目使用的是OpenAI的最新版的强化学习环境,在使用之前需要通过如下命令进行安装:
pip install gymnasium
OpenAI gymnasium是一个用于强化学习研究和开发的Python库,提供了一系列标准化的强化学习环境,让开发者能够轻松地创建、测试和比较不同强化学习算法。这个库的目标是为强化学习领域提供一个公共的基准测试平台,以帮助研究人员和开发者共同探索强化学习算法的性能和效果。
OpenAI gymnasium提供了各种各样的环境,包括经典的强化学习问题如CartPole、MountainCar和Atari游戏等。它还支持用户自定义环境的创建,以适应各种不同的强化学习任务。通过使用 OpenAI gymnasium,研究人员和开发者可以轻松地测试和比较不同的强化学习算法,以便进一步推进强化学习领域的研究和应用。
16.4.2 导入库
导入本项目所需要的多个Python库和模块,并设置了使用GPU(如果可用)设备。具体实现代码如下所示。
from collections import namedtuple, deque
import random
import gymnasium as gym
from gymnasium import spaces
import pygame
import numpy as np
import pandas as pd
import time # for sleep
from itertools import count
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
在上述代码中,“device”的功能是根据可用的CUDA(GPU)设备来选择在GPU上还是CPU上运行PyTorch代码。
16.5 捕食者-猎物(Predator-Prey)的环境
创建一个名为PredatorPreyEnv的自定义强化学习环境类,用于模拟实现“捕食者-猎物”(Predator-Prey)的环境。这是一个捕食者游戏的模拟环境,其中有捕食者和猎物,但只有一个捕食者需要捕食猎物,而所有的参与者都会获得奖励。对环境PredatorPreyEnv的具体说明如下:
- Predator Prey(捕食者-猎物):这个环境中包括两种类型的角色,一种是捕食者(Predator),另一种是猎物(Prey)。捕食者的目标是捕获猎物,而猎物的目标是躲避捕食者。
- Only One Predator needs to eat(只需一个捕食者吃到猎物):在这个环境中,只要有一个捕食者成功捕食到猎物,所有的捕食者都会获得奖励。这意味着所有的捕食者共享奖励,无论哪一个实际上吃到了猎物。
环境PredatorPreyEnv通常用于研究协作策略和共享奖励的情况,捕食者必须合作以确保至少有一个成功捕食猎物,以便让所有的捕食者都能获得奖励。在本节的内容中,将详细讲解这个环境的实现过程。
16.5.1 定义自定义强化学习环境类
定义了一个名为 PredatorPreyEnv 的自定义强化学习环境类,用于模拟捕食者-猎物的情境。具体实现代码如下所示。
PredatorPreyEnv(gym.Env):
metadata = {'render.modes': ['human', 'rgb_array'],
'render-fps': 4}
def __init__(self,
render_mode=None,
size:int=10,
vision:int=5,
predator:int =3,
prey:int =1,
error_reward:float=-2,
success_reward:float=10,
living_reward:float=-1,
img_mode:bool=False,
episode_length:int=100,
history_length:int=4,
communication_bits:int=0,
cooperate:float=1):
self.size = size
self.vision = vision
self.window_size = 500
self.render_mode = render_mode
self.predator_num = predator
self.prey_num = prey
self.active_predator = [True for i in range(self.predator_num)]
self.active_prey = [True for i in range(self.prey_num)]
self.error_reward = error_reward
self.success_reward = success_reward
self.living_reward = living_reward
self.episode_length = episode_length
self.img_mode = img_mode
self.steps = 0
self.window = None
self.clock = None
self.cooperate = cooperate
self.render_scale = 1
self.observation_space = spaces.Dict({
'predator': spaces.Sequence(spaces.Box(0, size-1, shape=(2,), dtype=np.int32)),
'prey': spaces.Box(0, size-1, shape=(2,), dtype=np.int32),
})
total_actions = 5
self.action_space_predator = spaces.MultiDiscrete([total_actions]*predator)
self.action_space_prey = spaces.MultiDiscrete([total_actions]*prey)
self.single_action_space = spaces.Discrete(total_actions)
self._action_to_direction = {
0: np.array([0, 1]),
1: np.array([1, 0]),
2: np.array([0, -1]),
3: np.array([-1, 0]),
4: np.array([0, 0])
}
self.frame_history = deque(maxlen=4)
self.history_length = history_length
self.communication_bits = communication_bits
if self.communication_bits>0:
self.pred_communication = np.zeros((self.predator_num))
self.prey_communication = np.zeros((self.prey_num))
上述代码的实现流程如下:
- 定义了一个自定义强化学习环境类 PredatorPreyEnv,该类继承自gym.Env。
- 允许通过参数来配置环境的各种属性,包括环境大小、视觉范围、捕食者和猎物数量、奖励函数等。
- 定义了观察空间和动作空间,以规定代理程序的感知和行动范围。
- 实现了环境的初始化、状态重置、动作执行和渲染等核心方法。
- 可以在图形界面或以RGB数组的形式进行渲染。
初始化方法 __init__定义了环境的初始化逻辑,包括环境的参数设置、观察空间和动作空间的定义等。具体实现代码如下所示:
- 定义初始化环境的各种属性和参数,包括环境大小、视觉范围、捕食者和猎物数量、奖励函数等。
- 定义了观察空间和动作空间,观察空间是一个包含捕食者和猎物位置信息的字典,动作空间是分别为捕食者和猎物定义的多离散动作空间。
- 初始化环境的状态,包括捕食者和猎物的随机位置、时间步数等。
- 设置渲染相关的属性,如渲染模式、窗口大小等。
16.5.2 定义自定义强化学习环境类
分别定义两个方法 _get_obs 和 _get_np_arr_obs,用于获取环境的观察信息(observation)。具体实现代码如下所示。
def _get_obs(self):
if self.img_mode:
return self._get_np_arr_obs()
return {
'predator': self._predator_location,
'prey': self._prey_location
}
def _get_np_arr_obs(self):
predator_states = []
prey_states = []
for i in range(len(self._predator_location)):
state = self._render_predator_frame(predator_id=i)
predator_states.append(state)
for i in range(len(self._prey_location)):
state = self._render_prey_frame(prey_id=i)
prey_states.append(state)
return {
"predator":predator_states,
"prey":prey_states
}
对上述代码的具体说明如下:
- 方法_get_obs:功能是根据环境的 img_mode 属性决定返回的观察信息的格式。如果 img_mode 为 True,则调用 _get_np_arr_obs 方法以图像数组的形式返回观察信息;如果 img_mode 为 False,则以字典的形式返回观察信息,包括捕食者和猎物的位置信息。
- 方法_get_np_arr_obs:功能是如果 img_mode 为 True,则调用此方法以获取以图像数组的形式表示的观察信息。该方法会遍历捕食者和猎物,分别调用 _render_predator_frame 和 _render_prey_frame 方法获取它们的图像状态。该返回一个字典,包括 "predator" 和 "prey" 两个键,对应的值是捕食者和猎物的图像状态列表。
上述方法的目的是获取环境的观察信息,这些信息可以用于代理程序感知环境的当前状态。根据 img_mode 的设置,可以选择返回图像状态或位置信息,以适应不同类型的强化学习代理程序。在 _get_np_arr_obs 方法中,捕食者和猎物的状态是通过调用 _render_predator_frame 和 _render_prey_frame 方法生成的图像帧来表示的。
16.5.3 环境重置
定义reset 方法,用于重置环境状态,开始新的回合(episode)。在reset方法中,随机生成捕食者和猎物的初始位置。将时间步数(steps)重置为0,同时将所有捕食者和猎物标记为“活跃”状态。如果渲染模式为 'human',则调用 _render_frame 方法渲染初始状态。最后调用 _save_frame_history 方法保存当前帧的状态历史,返回观察信息和附加信息的元组。具体实现代码如下所示。
def reset(self, *, seed: int=1, options=None):
self._predator_location = np.random.randint(0, self.size, size=(self.predator_num, 2))
self._prey_location = np.random.randint(0, self.size, size=(self.prey_num, 2))
self.steps = 0
self.active_predator = [True for i in range(self.predator_num)]
self.active_prey = [True for i in range(self.prey_num)]
if self.render_mode == 'human':
self._render_frame()
self._save_frame_history()
return self._get_frame_history(self.history_length), self._get_info()
16.5.4 计算捕食者和猎物的奖励
定义方法_get_reward 和方法_get_prey_reward,分别用于计算捕食者和猎物的奖励,这些奖励将在每个时间步之后返回给代理程序,以便进行强化学习训练。具体实现代码如下所示。
def _get_reward(self):
# if any predator reaches prey, success. else, living reward
rewards = [self.living_reward for i in range(self.predator_num)]
for i in range(self.predator_num):
for j in range(self.prey_num):
if self.active_predator[i]:
if np.all(self._predator_location[i]==self._prey_location[j]):
rewards = [self.cooperate*self.success_reward for i in range(self.predator_num)]
rewards[i] = self.success_reward
# print("EATEN")
return rewards
return rewards
def _get_prey_reward(self):
# if any predator reaches prey, success. else, living reward
rewards = [self.success_reward for i in range(self.prey_num)]
for i in range(self.prey_num):
if self._prey_location[i] in self._predator_location:
rewards[i] = 0
return rewards
对上述代码的具体说明如下:
- _get_reward 方法:用于计算捕食者的奖励。如果任何一个捕食者达到猎物,将获得成功奖励 self.success_reward,否则将获得生存奖励 self.living_reward。返回一个奖励列表,其中每个元素对应一个捕食者的奖励。
- _get_prey_reward 方法:用于计算猎物的奖励。如果任何一个猎物被捕食者捕获,其奖励将为0(被吃掉),否则将获得成功奖励 self.success_reward。返回一个奖励列表,其中每个元素对应一个猎物的奖励。
16.5.5 判断回合是否结束
定义方法_is_done,用于判断当前回合是否结束。返回一个布尔值,表示回合是否结束。如果当前时间步数达到设定的回合长度(self.episode_length)或者所有猎物都被捕食者捕获,回合将被标记为结束。具体实现代码如下所示。
def _is_done(self):
# if all prey are gone or episode length is reached, done
if self.steps >= self.episode_length:
return True
if np.sum(self.active_prey) == 0:
return True
return False
16.5.6 检查动作的合法性
分别定义方法_is_valid_predator 和方法_is_valid_prey,用于检查动作的合法性。具体实现代码如下所示。
def _is_valid_predator(self, location, index):
# check if location is valid
if location[0] < 0 or location[0] >= self.size or location[1] < 0 or location[1] >= self.size:
return False
if location in np.delete(self._predator_location, index, axis=0):
return False
return True
def _is_valid_prey(self, location, index):
# check if location is valid for prey of i'th index
if location[0] < 0 or location[0] >= self.size or location[1] < 0 or location[1] >= self.size:
return False
if location in np.delete(self._prey_location, index, axis=0):
return False
return True
对上述代码的具体说明如下:
- 方法_is_valid_predator:用于检查捕食者的新位置是否合法。返回一个布尔值,表示新位置是否合法。检查捕食者是否移动到了环境边界之外或与其他捕食者重叠。
- 方法_is_valid_prey:用于检查猎物的新位置是否合法。返回一个布尔值,表示新位置是否合法。检查猎物是否移动到了环境边界之外或与其他猎物重叠。
16.5.7 记录和获取状态历史
分别定义方法_save_frame_history 和方法_get_frame_history,用于记录和获取状态历史,以供代理程序使用。具体实现代码如下所示。
def _save_frame_history(self):
self.frame_history.append(self._get_obs())
def _get_frame_history(self, history=4):
if len(self.frame_history) < history:
return None
return list(self.frame_history)[-history:]
对上述代码的具体说明如下:
- 方法_save_frame_history:用于保存当前帧的状态历史,将当前观察信息添加到状态历史列表中。
- 方法_get_frame_history 方法:用于获取状态历史。如果状态历史的长度小于指定的历史长度(history),则返回 None。否则返回最近的若干个状态历史,用于构建观察信息。
16.5.8 实现step方法
定义了环境类 PredatorPreyEnv 中的 step 方法,该方法用于执行动作并进行环境交互。step 方法是环境中最重要的方法之一,它实现了环境与代理程序之间的互动,允许代理程序执行动作并获取观察、奖励和回合状态。具体实现代码如下所示。
def step(self, action_pred, action_prey, pred_communication=None, prey_communication=None):
if self._is_done():
raise RuntimeError("Episode is done")
self.steps += 1
# move predator
for i in range(self.predator_num):
if self.active_predator[i] == False: # if predator is dead,
continue
if i < len(action_pred):
action = action_pred[i]
else:
action = self.single_action_space.sample()
new_location = self._predator_location[i] + self._action_to_direction[action]
if self._is_valid_predator(new_location, i):
self._predator_location[i] = new_location
# move prey
for i in range(self.prey_num):
if self.active_prey[i] == False: # if prey is dead,
continue
if i < len(action_prey):
action = action_prey[i]
else:
action = self.single_action_space.sample()
new_location = self._prey_location[i] + self._action_to_direction[action]
if self._is_valid_prey(new_location, i):
self._prey_location[i] = new_location
# check if any predator reaches prey and give reward
pred_reward = self._get_reward()
prey_reward = self._get_prey_reward()
for i in range(self.predator_num):
for j in range (self.prey_num):
if np.all(self._predator_location[i] == self._prey_location[j]):
# print("EATEN !!!")
self.active_prey[j] = False
#save communication of agents
if self.communication_bits > 0:
self.pred_communication = pred_communication
self.prey_communication = prey_communication
done = self._is_done()
reward = {
'predator': pred_reward,
'prey': prey_reward
}
if self.render_mode == 'human':
self._render_frame()
self._save_frame_history()
return self._get_frame_history(self.history_length), reward, done, self._get_info()
对上述代码的具体说明如下:
(1)step方法接受四个参数:action_pred、action_prey、pred_communication 和 prey_communication。其中:action_pred 是一个包含每个捕食者动作的列表,action_prey 是一个包含每个猎物动作的列表,pred_communication 是一个包含捕食者通信信息的列表(可选,根据 communication_bits 是否大于 0 决定是否使用),prey_communication 是一个包含猎物通信信息的列表(可选,根据 communication_bits 是否大于 0 决定是否使用)。
(2)step方法首先检查当前回合是否已结束(通过调用 _is_done 方法),如果回合结束,则抛出运行时错误。
(3)step方法递增当前时间步数 self.steps,表示环境中经过的时间步数。
(4)然后,step方法迭代每个捕食者和猎物,并根据其动作进行移动:
- 对于每个活动的捕食者,根据其动作更新其位置,但在以下情况下动作不会生效:
- 如果动作导致两个捕食者重叠在一起。
- 如果动作导致捕食者与猎物重叠在一起。
- 如果动作导致捕食者移动到边界或墙上。
- 对于每个活动的猎物,根据其动作更新其位置,但在以下情况下动作不会生效:
- 如果动作导致猎物移动到边界或墙上。
(5)接下来,方法检查是否有捕食者捕获了猎物,并根据情况给予奖励:
- 如果捕食者的位置与猎物的位置完全重叠,捕食者将捕获猎物,猎物将被标记为不活跃(active_prey[j] = False)。
- 使用 _get_reward 方法计算捕食者的奖励,并使用 _get_prey_reward 方法计算猎物的奖励。
(6)如果通信比特数大于 0,方法还会保存捕食者和猎物的通信信息(如果提供)。
(7)最后,step方法检查回合是否已结束,构建并返回观测、奖励、结束标志和信息。
(8)如果渲染模式为 'human',则在每个时间步上调用 _render_frame 方法以可视化环境。
(9)最后,step方法保存当前状态历史并返回状态历史以供代理程序使用。
16.5.9 生成视图帧
定义_render_predator_frame 方法,用于生成以捕食者为中心的视图帧,以便监视捕食者和猎物的行为和交互。视图帧中的颜色和通道编码方式用于表示不同类型的对象和信息。具体实现代码如下所示。
def _render_predator_frame(self, predator_id:int=0):
if predator_id==None:
return
frame = np.zeros((4, self.vision, self.vision), dtype=np.uint8)
# draw predator
pred_loc = self._predator_location[predator_id]
min_pred_loc = pred_loc - np.array([self.vision//2, self.vision//2])
max_pred_loc = pred_loc + np.array([self.vision//2, self.vision//2])
# add predator to centre of frame
frame[1, self.vision//2, self.vision//2] = self.active_predator[predator_id]
# for each predator or prey within min and max it will be added in the frame
for i in range(self.predator_num):
if i==predator_id:
continue
if (min_pred_loc[0] <= self._predator_location[i][0] <= max_pred_loc[0]
and
min_pred_loc[1] <= self._predator_location[i][1] <= max_pred_loc[1]):
loc_x = self._predator_location[i][0]-min_pred_loc[0]
loc_y = self._predator_location[i][1]-min_pred_loc[1]
frame[2, loc_x, loc_y] = self.render_scale
# frame[2, loc_x, loc_y] = self.pred_communication[i]
if self.communication_bits > 0:
frame[3, loc_x, loc_y] = self.pred_communication[i]
for i in range(self.prey_num):
if (min_pred_loc[0] <= self._prey_location[i][0] <= max_pred_loc[0]
and
min_pred_loc[1] <= self._prey_location[i][1] <= max_pred_loc[1]):
loc_x = self._prey_location[i][0]-min_pred_loc[0]
loc_y = self._prey_location[i][1]-min_pred_loc[1]
frame[0, loc_x, loc_y] = self.render_scale
# create white for cells outside grid
if min_pred_loc[0] < 0:
frame[:, :abs(min_pred_loc[0]), :] = self.render_scale
if max_pred_loc[0] >= self.size:
frame[:, -(max_pred_loc[0]-self.size+1):, :] = self.render_scale
if min_pred_loc[1] < 0:
frame[:, :, :abs(min_pred_loc[1])] = self.render_scale
if max_pred_loc[1] >= self.size:
frame[:, :, -(max_pred_loc[1]-self.size+1):] = self.render_scale
return frame
该方法的实现流程如下要:
- _render_predator_frame 方法接受一个可选的参数 predator_id,该参数表示要在视图帧中以中心位置显示的捕食者的ID。如果 predator_id 为 None,则函数立即返回。
- 创建一个大小为 (4, self.vision, self.vision) 的三维 NumPy 数组 frame,其中第一个维度代表通道(通常对应于不同的对象或信息),第二和第三个维度代表视图帧的宽度和高度。
- 将中心位置的像素设置为 self.active_predator[predator_id],表示捕食者的位置,并将其标记为绿色。
- 计算视图帧中的捕食者和猎物的位置范围,以确定哪些捕食者和猎物在视图帧中可见。
- 对于每个捕食者和猎物,检查其位置是否在视图帧的可见范围内,如果是,则将其添加到视图帧中。在添加时,不同的对象使用不同的通道表示。
- 如果通信比特数大于 0,则还将通信信息添加到相应的通信通道。
- 处理边界情况,如果捕食者靠近视图帧的边界,将在视图帧外部添加白色像素以表示边界。
- 返回生成的视图帧 frame,其中包含了捕食者、猎物和通信信息的可视化信息。
16.5.10 渲染环境的视图
定义方法_render_prey_frame 和方法_render_frame,用于渲染环境的视图。这两个方法的目的是提供环境的可视化,以便监视捕食者、猎物和它们之间的交互。视图的颜色和图像表示方式用于区分不同类型的对象,并可根据需要进行自定义。具体实现代码如下所示。
def _render_prey_frame(self, prey_id:int=1):
if prey_id==None:
return
frame = np.zeros((3, self.vision, self.vision), dtype=np.uint8)
prey_loc = self._prey_location[prey_id]
min_prey_loc = prey_loc - np.array([self.vision//2, self.vision//2])
max_prey_loc = prey_loc + np.array([self.vision//2, self.vision//2])
frame[1, self.vision//2, self.vision//2] = self.render_scale
# for each predator or prey within min and max it will be added in the frame
for i in range(self.predator_num):
if (min_prey_loc[0] <= self._predator_location[i][0] <= max_prey_loc[0]
and
min_prey_loc[1] <= self._predator_location[i][1] <= max_prey_loc[1]):
frame[2, self._predator_location[i][0]-min_prey_loc[0], self._predator_location[i][1]-min_prey_loc[1]] = self.render_scale
for i in range(self.prey_num):
if (min_prey_loc[0] <= self._prey_location[i][0] <= max_prey_loc[0]
and
min_prey_loc[1] <= self._prey_location[i][1] <= max_prey_loc[1]):
frame[0, self._prey_location[i][0]-min_prey_loc[0], self._prey_location[i][1]-min_prey_loc[1]] = self.render_scale
if min_prey_loc[0] < 0:
frame[:, :abs(min_prey_loc[0]), :] = self.render_scale
if max_prey_loc[0] >= self.size:
frame[:, -(max_prey_loc[0]-self.size+1):, :] = self.render_scale
if min_prey_loc[1] < 0:
frame[:, :, :abs(min_prey_loc[1])] = self.render_scale
if max_prey_loc[1] >= self.size:
frame[:, :, -(max_prey_loc[1]-self.size+1):] = self.render_scale
return frame
def _render_frame(self):
if self.window is None and self.render_mode == 'human':
pygame.init()
pygame.display.init()
self.window = pygame.display.set_mode((self.window_size, self.window_size))
self.window = pygame.display.set_mode((self.window_size, self.window_size))
if self.clock is None and self.render_mode == 'human':
self.clock = pygame.time.Clock()
canvas = pygame.Surface((self.window_size, self.window_size))
canvas.fill((255, 255, 255))
pixel_size = self.window_size // self.size
# draw grid
for i in range(self.size):
pygame.draw.line(canvas, (0, 0, 0), (0, i*pixel_size), (self.window_size, i*pixel_size))
pygame.draw.line(canvas, (0, 0, 0), (i*pixel_size, 0), (i*pixel_size, self.window_size))
# draw prey as rectangle
for i in range(self.prey_num):
if self.active_prey[i]:
pygame.draw.rect(canvas, (255, 0, 0), (self._prey_location[i][1]*pixel_size, self._prey_location[i][0]*pixel_size, pixel_size, pixel_size))
# draw predator as circle
for i in range(self.predator_num):
if self.active_predator[i]:
pygame.draw.circle(canvas, (0, 0, 255), (self._predator_location[i][1]*pixel_size+pixel_size//2, self._predator_location[i][0]*pixel_size+pixel_size//2), pixel_size//2)
if self.render_mode == 'human':
self.window.blit(canvas, canvas.get_rect())
pygame.event.pump()
pygame.display.update()
self.clock.tick(self.metadata['render-fps'])
else:
return np.transpose(pygame.surfarray.array3d(canvas), (1, 0, 2))
def close(self):
if self.window is not None:
pygame.quit()
self.window = None
self.clock = None
在上述代码中,方法_render_prey_frame的实现流程如下:
- _render_prey_frame 方法接受一个可选的参数 prey_id,该参数表示要在视图帧中以中心位置显示的猎物的ID。如果 prey_id 为 None,则函数立即返回。
- 创建一个大小为 (3, self.vision, self.vision) 的三维 NumPy 数组 frame,其中第一个维度代表通道(通常对应于不同的对象或信息),第二和第三个维度代表视图帧的宽度和高度。
- 将中心位置的像素设置为 self.render_scale,表示猎物的位置,并将其标记为红色。
- 计算视图帧中的猎物的位置范围,以确定哪些捕食者和猎物在视图帧中可见。
- 对于每个捕食者和猎物,检查其位置是否在视图帧的可见范围内,如果是,则将其添加到视图帧中。在添加时,不同的对象使用不同的通道表示。
- 处理边界情况,如果猎物靠近视图帧的边界,将在视图帧外部添加白色像素以表示边界。
- 返回生成的视图帧 frame,其中包含了捕食者、猎物和通信信息的可视化信息。
方法_render_frame的实现流程如下所示:
- _render_frame 方法用于渲染整个环境的视图,适用于逐帧的可视化。
- 如果窗口和时钟尚未创建,并且渲染模式为 'human',则初始化 Pygame 窗口和时钟。
- 创建一个 Pygame 表面 canvas,并用白色填充整个表面。
- 根据环境的大小和视图的比例计算像素的大小。
- 绘制网格线,以在视图中创建网格。
- 绘制每个活动猎物作为红色矩形。
- 绘制每个活动捕食者作为蓝色圆圈。
- 如果渲染模式为 'human',则将 canvas 显示在窗口上,并更新 Pygame 显示。
- 如果渲染模式不是 'human',则将 canvas 转换为 NumPy 数组并返回。