Photon with Unity 随笔

Intro

Photon Unity Networking (PUN) is a Unity package for multiplayer games. Flexible matchmaking gets your players into rooms where objects can be synced over the network. RPCs, Custom Properties or “low level” Photon events are just some of the features. The fast and (optionally) reliable communication is done through dedicated Photon server(s), so clients don’t need to connect one to one.

相关代码可

Refer to the link below to view in the Asset Store (PUN2)

PUN 2 - FREE | 网络 | Unity Asset Store

This note is based on PUN2 Version 2.41 - August 2, 2022

Dev Region

一般的,如果配置中空缺此项,游戏会自动连接延迟低的服务器,可能会导致无法观察到彼此的房间。推荐将Dev Region设置为kr或者jp

General Functions

Common Use Variables

PhotonNetwork.CurrentRoom记录了当前连接的房间的信息

PhotonNetwork.PlayerList记录了连接到当前房间的玩家信息列表

PhotonNetwork.IsMasterClient当前客户端是房主

PhotonNetwork.NickName本地玩家的昵称

General Process

导入Photon的包后,可以使用PUN Wizard完成初始化,需要有自己的AppID。稍后可以通过修改Assets/Photon/PhotonUnityNetworking/Resources/PhotonServerSettings.asset路径下的配置文件进行配置。

一般的,连接的级别大致分为Server(Master)-Lobby-Room

下列代码给出了连接服务器、创建、加入房间的代码流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//MonoBehaviourPunCallbacks

PhotonNetwork.ConnectUsingSettings();
public override void OnConnectedToMaster(){}

PhotonNetwork.CreateRoom(roomNameInputField.text);
public override void OnCreatedRoom(){}
public override void OnCreateRoomFailed(short returnCode, string message){}

PhotonNetwork.JoinRoom(info.Name);
public override void OnJoinedRoom(){}

PhotonNetwork.LeaveRoom();

public override void OnRoomListUpdate(List<RoomInfo> roomList){}
public override void OnMasterClientSwitched(Player newMasterClient){}
public override void OnPlayerEnteredRoom(Player newPlayer){}

下面给出了实际的代码应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
public class Launcher : MonoBehaviourPunCallbacks
{
void Start()
{
//通过配置连接到服务器
PhotonNetwork.ConnectUsingSettings();
}

public void CreateRoom()
{
//创建房间,需要指定房间名
if (string.IsNullOrEmpty(roomNameInputField.text))
{
return;
}
PhotonNetwork.CreateRoom(roomNameInputField.text);
}

//加入房间,以房间名为参数
public void JoinRoom(RoomInfo info)
{
PhotonNetwork.JoinRoom(info.Name);
MenuManager.Instance.OpenMenu("Loading");
}

//退出房间
public void LeaveRoom()
{
PhotonNetwork.LeaveRoom();
}

//重要的回调方法:
//成功连接到服务器后调用
public override void OnConnectedToMaster()
{
Debug.Log("[Network]Connected to Master");
PhotonNetwork.JoinLobby();

//同步场景
PhotonNetwork.AutomaticallySyncScene = true;
}

//本地客户端连接到房间时调用
//PhotonNet
public override void OnJoinedRoom()
{
roomNamePUN.text = PhotonNetwork.CurrentRoom.Name;

Player[] players = PhotonNetwork.PlayerList;

foreach (Transform trans in playerListContent)
{
Destroy(trans.gameObject);
}

for (int i = 0; i < players.Count(); i++)
{
Instantiate(playerListItemPrefab, playerListContent).GetComponent<PlayerListItem>().SetUp(players[i]);
}

//使得按钮只对创建客户端可见
startGameBtn.SetActive(PhotonNetwork.IsMasterClient);
}

public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
//PhotonNetwork.countOfRooms != PhotonNetwork.GetRoomList();
Debug.Log("[Network]Room List is Updated: " + roomList.Count() + " in total");

//先清理已有的房间列表
foreach (Transform item in roomListContent)
{
Destroy(item.gameObject);
}


for (int i = 0; i < roomList.Count(); i++)
{
if (roomList[i].RemovedFromList)
continue;
Instantiate(roomListItemPrefab, roomListContent).GetComponent<RoomListItem>().SetUp(roomList[i]);
}
}

//当房主发生了变化
public override void OnMasterClientSwitched(Player newMasterClient)
{
startGameBtn.SetActive(PhotonNetwork.IsMasterClient);
}

//离开房间时调用
public override void OnLeftRoom()
{

}

//房间消息列表更新时调用(如创建了新房,已有的房间关闭)
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
//PhotonNetwork.countOfRooms != PhotonNetwork.GetRoomList();
Debug.Log("[Network]Room List is Updated: " + roomList.Count() + " in total");

//先清理已有的房间列表
foreach (Transform item in roomListContent)
{
Destroy(item.gameObject);
}


for (int i = 0; i < roomList.Count(); i++)
{
if (roomList[i].RemovedFromList)
continue;
Instantiate(roomListItemPrefab, roomListContent).GetComponent<RoomListItem>().SetUp(roomList[i]);
}
}

//当一个远程玩家进入房间时调用
public override void OnPlayerEnteredRoom(Player newPlayer)
{
Debug.Log("[Network]" + newPlayer.NickName + " connected, in room: " + PhotonNetwork.NickName);
Instantiate(playerListItemPrefab,playerListContent).GetComponent<PlayerListItem>().SetUp(newPlayer);
}
}

当玩家都进入同一个房间时,可以考虑使用来同步场景

1
PhotonNetwork.LoadLevel(1); 

PlayerProperties

PlayerProperties 是一种同步玩家间状态的方法,以下是代码示例,目的为同步玩家手中所持有的武器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//区分C#内置的ashtable与Photon提供的Hashtable
using Hashtable = ExitGames.Client.Photon.Hashtable;

public class PlayerController : MonoBehaviourPunCallbacks
{
PhotonView PV;

private void Awake()
{
PV = GetComponent<PhotonView>();
}

//同步武器的实质是同步所持武器的itemIndex
void EquipItem(int _index)
{
itemIndex = _index;

//当我们装备一个武器时,首先查看是否是localPlayer,若是,将把这个切换到的武器index发出去
if (PV.IsMine)
{
Hashtable hash = new Hashtable();
hash.Add("itemIndex", itemIndex);
PhotonNetwork.LocalPlayer.SetCustomProperties(hash);
}
}

//所有在房间内玩家会收到同步信息,调用此回调。参数中给出了属性被改变的玩家,属性新值
public override void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
{
//本地已经切换了武器,无需再次同步 && 在客户端看,仅仅改变对应玩家的index,而不是所有玩家
if (changedProps.ContainsKey("itemIndex")&&!PV.IsMine && targetPlayer == PV.Owner)
{
EquipItem((int)changedProps["itemIndex"]);
}
}
}

[PunRPC]

Example

需要Pun远程调用的方法,应当给与PunRPC标签,下面给出造成伤害、收到伤害的流程:

单发枪类,能够开火

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SingleShotGun : Gun
{
//枪开火,进行射线检测
void Shoot()
{
//从摄像机的近剪裁面的中点向着远剪裁面的中点绘制一条射线
Ray ray = cam.ViewportPointToRay(new Vector3(0.5f, 0.5f));
ray.origin = cam.transform.position;
if (Physics.Raycast(ray, out RaycastHit hit))
{
Debug.Log("[Weapon]Ray cast hit " + hit.collider.gameObject.name);
hit.collider.gameObject.GetComponent<IDamageable>()?.TakeDamage(((GunInfo)itemInfo).damage);

//生成 Bullet Impact
PV.RPC(nameof(RPC_Shoot),RpcTarget.All, hit.point,hit.normal);
}
}
}

PlayerController类,每一个玩家角色GameObject都挂载此类

使用nameof()可以避免可能的拼写错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//本地被击打的物体实现IDamageable接口,调用TakeDamage()方法
public class PlayerController : MonoBehaviourPunCallbacks, IDamageable
{
public void TakeDamage(float damage)
{
//PV.Owner指定了被打击的角色,damage为此次命中的伤害
PV.RPC(nameof(RPC_TakeDamage), PV.Owner, damage);
Debug.Log("[Combat]" + "I(" + PhotonNetwork.NickName + ") Try to hurt " + PV.Owner.NickName + " at damage " + damage);
}

//对于被打击的玩家来说,其RPC方法被调用了
[PunRPC]
void RPC_TakeDamage(float damage, PhotonMessageInfo info)
{
Debug.Log("[RPC][Combat]" + PV.Owner.NickName + " Take damage " + damage);
currentHealth -= damage;

healthbarImage.fillAmount = currentHealth / maxHealth;

if (currentHealth <= 0)
{
Die();
//找到发送信息者,追溯伤害来源
PlayerManager.Find(info.Sender).GetKill();
}
}
}

Bullet Impact

通过Photon同步大量生成的GameObject是昂贵的,一般使用RPC调用对应方法,生成物体。

1
2
3
4
5
6
7
8
9
10
11
12
[PunRPC]
void RPC_Shoot(Vector3 hitPosition,Vector3 hitNormal)
{
Collider[] colliders = Physics.OverlapSphere(hitPosition, 0.3f);
Debug.Log("[RPC][Combat]" + hitPosition);
if (colliders.Length != 0)
{
GameObject bulletImpactObj = Instantiate(bulletImpactPrefab, hitPosition + hitNormal * 0.005f, Quaternion.LookRotation(hitNormal, Vector3.up) * bulletImpactPrefab.transform.rotation);
Destroy(bulletImpactObj,10f);
bulletImpactObj.transform.SetParent(colliders[0].transform);
}
}

Photon View

View ID[1..999] 对于每个Photon View 应当是 unique 的

Photon.Instantiate

Instantiate prefabs from Photon is not the same as instantiate a prefab from Unity normally

为了能够在多种平台上成功的加载预制体,尤其是此挂载了PhotonView的预制体(而不是仅仅在Editor中),

PhotonPrefabs must be in the resources folder.

Because Unity automatically excludes any file not referenced in the editor from the final build, and we don’t reference PhotonPrefabs. We use strings.

1
PhotonNetwork.Instantiate(Path.ComPhotonNetwork.Instantiate(Path.Combine("PhotonPrefabs", nameof(PlayerManager)), Vector3.zero, Quaternion.identity);bine("PhotonPrefabs", "PlayerManager"), Vector3.zero, Quaternion.identity);

为了防止可能的拼写错误,可以使用nameof()

Unity 能够将在Resources文件夹中的文件正常打包

可以在Instantiate的时候为对应GameObject的PV赋值

以创建PlayerController为例,将需要传递的参数封装到最后一个参数object[] 种

1
2
3
4
5
6
7
8
9
10
11
12
13
void CreateController() 
{
Transform spawnpoint = SpawnManager.Instance.GetSpawnpoint();
Debug.Log("[Player]Instantiated Player Controller: " + PhotonNetwork.NickName);
controller = PhotonNetwork.Instantiate(System.IO.Path.Combine("PhotonPrefabs",nameof(PlayerController)),spawnpoint.position, spawnpoint.rotation, 0, new object[] {PV.ViewID});
}


void GetParam()
{
//拆箱时,需要指定cast的类型
int id = (int)PV.InstantiationData[0];
}

Photon Transform View

需要通过Photon同步Transform信息的物体,应当在Photon View之外额外挂在此组件。通过Photon同步时,不应当继续使用本地的物理等等逻辑计算的Transform信息,否则会导致GameObject的抖动等不稳定现象。

在初始化之初关闭即可

1
2
3
4
if(!PV.isMine)
{
Destroy(rb);
}

PUN(Not recommanded)

Photon Unity Networking Classic - FREE | 网络 | Unity Asset Store

Photon.MonoBehaviour vs. Photon.PunBehaviour

For PunBehaviour:

This class provides a .photonView and all callbacks/events that PUN can call. Override the events/methods you want to use.

多与回调相关时候,应当使用 Photon.PunBehaviour

Install Anaconda & Jupyter Notebook

0. Intro

Anaconda offers the easiest way to perform Python/R data science and machine learning on a single machine.

Jupyter Notebook is the original web application for creating and sharing computational documents.

More information related to this topic could be found from:Installing Python · cs109/content Wiki · GitHub

1. Install Anaconda

官网下载链接:Anaconda | Anaconda Distribution

For Windows user:

  1. 点击绿色按钮即可下载,若下载速度过慢,可以考虑镜像清华大学开源软件镜像站 | Tsinghua Open Source Mirror,或右键使用迅雷下载。
    c61a4a5e9ff2e264b7a7a4a4e0289ad9.png

  2. 下载完毕后打开安装包,一路next默认同意即可。安装进度条走完需要约五到六分钟。

  3. 安装成功后,打开开始菜单(win+s)→搜索Anaconda,打开。
    e9559ebb95e08a8d87c1d6b8e5b52ee7.png

  4. 一般首次启动时间较长,有时会提示更新,安装更新即可。

  5. 启动 Jupyter Notebookd31359c5234f7109b9819b59c01f0f46.png

  6. 正常情况下浏览器打开,可以看到所在目录

    77e23a5cd60e6fd6715d0442d68ed9dc.png

  7. .ipynb文件添加到该目录下,点击即可在浏览器中打开。

Install Libraries

a379d50d6d045afbb61b641f41d972d7.png

Change the working directory

如果未生成过 Jupyter 的配置文件jupyter_notebook_config.py
打开 Anaconda Prompt
026c47fcb4a58e006629a22f4fa4fb3c.png
输入

1
jupyter notebook --generate-config

配置文件生成在了对应路径下,即C:\Users\xiaonan\.jupyter

打开此配置文件,找到c.NotebookApp.notebook_dir字段,等号右边填入需要变更为的目标路径
c.NotebookApp.notebook_dir = "D:\XJTLU\INT303\Labs",并去除行前的注释#

重新打开Jupyter Notebook 即可。

机甲柔情,铁血浪漫:《泰坦天降2》

磅礴大气的异星基地、坚实可靠的机械装甲、精致酷炫的未来枪械……《泰坦天降2》(Titanfall® 2,下文简称《TTF2》)在2016年为所有的科幻战斗爱好者们带来了无与伦比的游戏视听盛宴。

微信图片\_20220911100434.png

尽管在今天看来,《TTF2》与它的后续衍生作品《Apex Legends》凭借过硬的游戏质量和活跃的玩家社区在端游FPS领域中具有较高的热度,是堪称成功的游戏作品,但在《TTF2》发布之初,因为同期两部FPS大作的影响,即使其收到了来自玩家和媒体的广泛好评,其销量并没有达到重生工作室预期的目标,可谓是叫好不叫座的典型。

《TTF2》以边境反抗军对峙IMC集团军为背景,在单人剧情模式中讲述了泰坦BT-7274与其铁驭库珀增援反抗军的惊险旅程。一路上,BT与库珀生死相依,BT陪伴库珀从一名普普通通的士官兵成长为了他心目中受人景仰的铁驭。每当库珀觉得力不从心或者身处险境时,只要大喊“BT”,BT总能及时赶到化解危机。

中文配音版中的BT每次与库珀商量脱困对策的时候,总是以“相信我,铁驭”开启对话,给玩家十足的安全感。具有高度智能的BT不再是冷冰冰的机械装甲,而是成为了库珀的过命兄弟,在战场上,BT强大而可靠,而在旅程中,BT可以与库珀插科打诨,消遣时间,而在彼此命悬一线之际,它又机器一般以“协议三:保护铁驭”将库珀抛掷出仓,帮助其脱离险境,很多玩家在体验此结局时会被BT的忠诚可靠与感动。

微信图片\_20220911100445.png

也许读者会认为这样的剧情老套乏味,毕竟几乎所有科幻战斗作品都用着类似的套路塑造着人物形象,但是《TTF2》用杰出的关卡设计将整个故事在八个小时的短流程内讲述得自然、亲切、投入。

《TTF2》单人模式的每一关都准备了一些传递给玩家的明确概念,并且相互联系。例如,在第一关中,玩家操作库珀单兵作战,在熟悉基本步兵枪械的同,玩家可以通过游戏中大量使用的可视化幻影学习如何针对不同地形应用“滑墙”、“二段跳”等等基础身法,为玩家后续的冒险打开了思路。同时,玩家也体会到了步兵对步兵的战斗节奏。而在第二关中,玩家驾驶BT与敌方步兵展开对抗,与第一关步兵战的节奏形成鲜明对比,进而玩家体验到泰坦作战的酣畅淋漓。

微信图片\_20220911100451.jpg

《TTF2》最出彩的关卡设计莫过于第四关“因果效应”,库珀在这一关获得了时空穿梭的能力,这允许他在“现在”与“过去”两条时间线中自由穿梭。在“现在”的时间线中,库珀执行任务的IMC科研基地已然破败不堪,散落的石板横亘在通道中间,熊熊烈火阻断了库珀的路线。然而在“过去”的时间线中,IMC的基地窗明几净,老实可爱的马文机器人随处可见,一派欣欣向荣的景象。

微信图片\_20220911100459.jpg

库珀需要利用时空穿梭在两条时间线中来回切换,从而顺利通过“现在”时间线中的地形障碍,躲避“现在”的IMC的机器人追杀,同时在“过去”的时间线中对抗IMC的警卫安保。有意思的是,在“过去”中被击杀的敌人尸体会被保留在“现在”的场景中。事实上,正是“过去”的库珀破坏了IMC正在研究的时间装置,导致了时间线的异常。

《TTF2》的每一关都通过精心的设计避免了玩法上的单调。在保证了射击手感和游玩流畅度的同时,其利用新奇的玩法与精湛的特效画面部分取代了同类枪战游戏以CG过场动画补齐FPS互动方式单薄的短板,提供了更多的沉浸感、参与感。

微信图片\_20220911100505.jpg

在《TTF2》多人模式中,区别于传统的FPS玩家对抗游戏,玩家操作的角色——铁驭被赋予了飞檐走壁、使用技能、驾驶泰坦的特殊能力。玩家可以根据自己的喜好配合称手的枪械、技能、投掷物。由于各种滞空、加速身法的加入,有经验的玩家可以操作铁驭长时间行走在半空中完成对敌方的击杀。同时,泰坦机甲的加入让整个对局有了抓眼的冲突,铁驭与泰坦往往会配合完成对点位的攻占与防守。

微信图片\_20220911100511.jpg

尽管玩家对《泰坦天降3》的呼声很高,但由于重生工作室正忙于《Apex Legends》的维护以及其他游戏的开发,EA官方表示并没有《泰坦天降3》的启动计划。由此,BT-7274与库珀的故事将定格在库珀的头盔被再度点亮的瞬间。

热血反恐,历久弥新!《反恐精英》系列

自1999年的夏天发布第一个测试版以来,《反恐精英》系列历经了十数个大大小小的版本迭代,二十余年间稳坐全球竞技FPS的第一把交椅,俨然成为了游戏界家喻户晓的神话。

微信图片\_20220911100333.png

据Steam Charts统计,Valve在2012年发布的最近作品《反恐精英:全球攻势》(下文简称《全球攻势》)自从2021年恢复举办一年一度的全球顶级赛事 CS:GO Major 后,游玩人数明显回暖,并在今年的一月份重新突破同时在线人数五十万的大关。而反观同期发布的诸多FPS竞品,例如《战争前线》、《使命召唤Online》等等在当时有一定热度的作品,到今天即便没有停服也失去了往日的光鲜,只能在相对局限的社区玩家中偏安一隅,几乎丧失了增添新鲜血液的能力。

微信图片\_20220911100341.png

《反恐精英》开创了团队对抗竞技FPS游戏的时代,一款没有关卡设计、主线剧情的拟真射击游戏在当时看来是十分具有创意和颠覆性的,而从随后其在全球范围内的流行和粉丝社群的活跃可以看出玩家对如此设计的喜欢和追捧。《反恐精英》让世界看到了竞技FPS游戏的无穷潜力和迷人魅力。

在《反恐精英》经典竞技模式中,10名玩家将分别扮演参与CT或者T阵营,前者的目的是保护地图中特定的部分(通常是A、B两区)在限定时间内不被爆破,或者全歼T阵营角色;后者的目的是在限定时间内安装定时炸弹破坏地图建筑或物品,或者全歼CT阵营角色。

玩家将利用地图高低、地形掩体、投掷物配合、交叉火力等等一系列组合玩法完成一定数量的攻守战。为了保证游戏的公平性和竞技性,游戏的地图进行了非常考究的设计和安排,两个阵营发生摩擦与冲突的关键位置都有对应的玩法套路,一些经典的竞技地图,诸如de_dust2,de_inferno,等等在后来的众多FPS游戏中都得到了复刻与致敬。许多关于《反恐精英》的梗和欢笑都来自于特定地图的特殊玩法,“Rush B”讲的就是T阵营玩家在Dust2地图中耿直地冲向B点,展开激斗(往往有出人意料的戏剧性效果)。

微信图片\_20220911100352.jpg

《全球攻势》仍然以竞技性为核心要素,其延续了《反恐精英》一贯的经典竞技玩法,并在一些细节上加以简化与优化。例如,在《全球攻势》中,玩家不再需要像1.6版本那样在对局开始之初额外花钱补齐子弹,而是仅仅需要根据自己游戏内的经济情况选择合适的枪械即可获得固定数量的子弹,省去了一部分相对繁琐的记忆与运算,玩家因此可以更好地投入到射击枪法、战术安排的比拼中去,专注于体验纯粹的操作性内容。

微信图片\_20220911100401.jpg

国内的玩家都知道,由腾讯代理的“三亿鼠标的枪战梦想”《穿越火线》凭借国内网络游戏行业崛起的春风和在当时相对于《反恐精英》1.6版本更好上手的界面和多样的玩法,在互联网走进千家万户的同时,吸引了一大批FPS爱好者。而在后续的运营策略上,《穿越火线》选择了娱乐性与多样性,在相当长的时间内,连局榜首的营收能力证明了选择的正确性。

然而,随着设备硬件水平的提高以及玩家对开放性、公平性、竞技性的重视与需要,模式繁多、画面落伍、略显臃肿的《穿越火线》逐渐走向了落寞,取而代之的正是“一招鲜,吃遍天”的老大哥《全球攻势》,尽管《全球攻势》在今天也被诟病引擎过时、画面效果落后,大多数玩家殊不知在画面质量和竞技质量的权衡上,《全球攻势》选择了后者:一些影响玩家视线的特效被弱化、删除,大多数地图使用大量阳光照明,以便为玩家提供一个更直观、清晰的对局环境,即使这样做会损失一些地图气氛。

微信图片\_20220911100411.jpg

出身于MOD(游戏模组)的《反恐精英》同样保持了对MOD的广泛支持,提供了高度可定制的内容。一些高水平的社区大佬甚至可以自己动手开发地图、配置玩法上传到创意工坊供所有玩家游玩,玩家可以自己选择体验喜欢的玩法与地图,V社官方也会选择优秀的社区地图加入到正式版的游戏本体中加以推送。一些玩家认为,《全球攻势》之所以要沿用老旧的起源引擎,正是要保证对海量社区内容的维护和支持。

当然,在众多精品游戏一同登台的今天,《全球攻势》并没有以一成不变的骄傲姿态展现在玩家面前,其始终在计划中调整武器属性、地图地形,来确保游戏的平衡性。每年的一众赛事也在为这个二十余岁的游戏系列撑腰打气,在可预见的将来,《反恐精英》系列仍将紧跟时代、造就时代,书写新的辉煌。

微信图片\_20220911100417.jpg

从另一个角度看待世界!视错觉游戏

眼见为实?在这些游戏世界中一起体验不一样的视觉规则!

微信图片\_20220911100009.gif

在当今品类繁多的游戏产品中,视错觉游戏凭借其带来的新奇体验和无穷尽的有趣想象力俘获了不少玩家,持续保持着强劲的生命力和表现力。此类游戏通常在为玩家呈现美轮美奂的风格化美术画面时,以一定的规则更改画面的透视、色彩、明暗关系,使得玩家产生一些空间、速度、时间上的错觉,从而将游戏的内容从二维的屏幕空间延伸至多维的思想空间。在TapTap的编辑推荐中,发布七年有余的视错觉游戏《纪念碑谷》一直保持着评分第一的傲人战绩,足见此类游戏迷人的独特魅力。

微信图片\_20220911100017.jpg

Waterfall (M. C. Escher)

石版画《瀑布》是荷兰版画家埃舍尔创作的视错觉作品。读者乍看此作之初,也许会觉得图画中的景物安排得秩序井然,合情合理。而细看时,画作中的逻辑却相当违背常理直觉,画作竟然描述了一个不可能的“永动机”!瀑布倾泻而下,激起浪花,带动水车,流入水渠,最终竟然又回到了瀑布口!因为无法找到一个合理的参照物,很难说得清两座塔的空间关系、水的流向等等关键信息,也难以在现实世界中真正制作出这样的系统,埃舍尔利用巧妙的数学几何关系在平面画布上构筑了充满不可能的世界。而在游戏制作中,制作者的创作空间将不再受限于画布,而是可以操纵三维空间乃至把握时间维度,并且写入自己的世界运作规律,可以相对容易地创造出看似合理而又处处充满意外的世界。

微信图片\_20220911100031.jpg

一般来说,视错觉大致可以被分为三类,分别是几何学错觉、生理错觉以及认知错觉。在游戏应用中,几何学错觉出现得最为广泛。对一些几何物体而言,从特定角度观察到的长度、角度、面积等等属性与实际上测得的数值有明显出入的情况,称为几何学错觉。在实际开发中,设计者往往根据游戏需要更改了一些物体的正交投影或透视投影中的关键规则,让游戏物体看起来拥有十分不寻常的几何外形。

玩家主要通过屏幕画面来获得游戏中的几何信息——而对于游戏空间,大小、方向的概念则来自于对参照物的选择。只要几何透视关系能够自洽、参照物稳定而连续,玩家就可以自适应游戏世界中的空间关系并自然地带入其中,只要游戏中的人物、道具、环境大小比例符合实际生活常识,玩家就可以很自然地进行游戏。

微信图片\_20220911100039.gif

而游戏《超阈限空间》则将透视纵深关系、参照物这两个要素纳入了游戏互动之中。在此游戏中,玩家扮演的角色在梦境中醒来,进入一个大小、空间、逻辑概念变得模糊暧昧的世界。作为基础玩法,玩家需要捡起环境中的特定道具,利用最直接的透视关系来放大与缩小物体,从而将特定道具变换为合适大小,达成通关条件。

同时,玩家需要打破常规的思考方式,具有创造性地“通关”。在游戏中,场景的路标、旁白等等在不断给玩家指示,而这些指示并不完全正确可靠,在某些关卡中,玩家必须反其道而行之,选择和路标箭头方向相悖的路线,才能避免陷入死循环的尴尬境地。可以说,《超阈限空间》成功并且出色地将视错觉作为一个玩法加入了游戏。

微信图片\_20220911100051.gif

广受好评的国产解密游戏《笼中窥梦》也同样将“视错觉”作为核心玩法。在游戏中玩家将通过观察与推理,发现正方体相邻两个面所描绘的场景的内在联系。随后,玩家通过拖拽视角到特定角度将外形相近、可以拼接的物体合二为一,让物品讲述自己的故事,推动剧情的发展。由于《笼中窥梦》是剧情导向游戏,其游戏性并没有达到《超阈限空间》的高度,但其讲述的亲情反战主题的感人故事,同样值得玩家多次思考回味。

微信图片\_20220911100057.gif

值得注意的是,在视觉设计出众的同时,以上的精品佳作也对游戏配乐、新手指引、关卡难度等等影响游戏的关键要素有着相当考量的把握,保证玩家不会在遭遇挫折时失去游戏的动力。如果游戏一味地追求视错觉带来的新鲜体验而忽视其他的游戏内容,玩家将很快失去游玩的耐心,甚至因为非常规的视觉效果而感到不适。

感人至深!《To The Moon》

“我们总会在月亮上相遇的。”

随着前段时间《影子工厂》的发布,高瞰出品的系列游戏第一部《To The Moon》(下文简称《Moon》)也在它发布十一年之际再度回归玩家们的视野,作为一款用实力斩获无数最佳剧情、优秀故事奖项的游戏,其细腻出众的精神内涵正在俘获着新一批的游戏故事爱好者们。

微信图片\_20220911095859.jpg

尽管《Moon》以16位平面像素美术的方式呈现在玩家面前,但其构思精巧的软科幻剧情、形象丰满的人物角色为玩家所带来的思考与感动,在面对现在的一些高清的流行剧情游戏时也不遑多让。关于人生、关于爱情、关于时间、关于死亡,这是《Moon》这部游戏,换言之,互动小说所讲述的故事的核心。

《Moon》是一款典型的剧情导向游戏,在游戏中,玩家会扮演两位西格蒙德公司的科学家,为濒临死亡的John提供记忆编辑的服务,以帮助其“不留遗憾地去世”。游戏操作相当简单,玩家只需要操作游戏人物探索地图、对话NPC(非玩家角色)、收集记忆碎片,就可以推动游戏顺利地进展下去。整个流程游戏并没有设计解谜、对战等等有一定门槛的互动环节,而是如同一本真正的小说那样,在情节上设置悬念、埋下伏笔,同时利用游戏可探索区域的面积大小、地图的光影强弱、背景的音乐起伏等等游戏独有的元素以很高超的节奏把控能力完成了对玩家感情的引导、蓄势、高潮、整理。可以说,《Moon》得兼了作为游戏的能动性和作为小说的故事性。

游戏的标题《To The Moon》即是游戏中濒死的老人John的愿望,但John自己也说不上来为什么要去月球,John的家人们则希望两位科学家不要修改过多记忆的内容,好让John最终在离世时能记得现实中的的确确的家庭与欢乐。即便动机难以确定,此番工作没有什么好的切入口,但两位西格蒙德科学家依旧用心执行了任务,试图在John的记忆之旅中找到合适的片段并埋下愿望的种子。

微信图片\_20220911095929.jpg

游戏随即以倒叙的形式展开,两位科学家第一次旅行时穿梭到了晚年的记忆片段在这个片段中——大量的折纸兔铺满了闲置房间的地面,John时常会去看望去世的妻子River的墓碑,打扫附近的灯塔,似乎也接受了这样的生活。而回到中年之时,John生活中的矛盾露出了冰山一角。在记忆中,River身患重病,高昂的治疗费用让夫妻俩不得不在治疗延续生命和盖房之间做出选择。相信大多数玩家此时想做的选择和John一样,会全力支持治疗而放弃盖房的想法。而River的想法却不一样,她希望John能够完成他们结婚时的约定——在有灯塔的高崖边盖一栋楼,陪伴孤独的灯塔。

微信图片\_20220911095937.jpg

显然,这里交代的“陪伴灯塔”的理由是不够的,随着剧情的推进,更多的细节呈现到了玩家面前:年幼的John初识River的当晚,他们躺在草坪上聊了很多很久,River将星星比作孤独的灯塔,而John则耐心地倾听,River的智慧与学识令他着迷。分别之际John和River互相约定,来年在同样的地方再见,如果未来没有相见,他们就在月球上相会。“我们总会在月亮上相遇的”,John非常自信地对River说。

随后的一年里,因为一场意外,John的孪生兄弟不幸去世,John为了忘去这惨痛的一幕,选择吞下了记忆阻滞剂,主动忘去了意外之前的所有事情,包括与River的约定。但“去月球”的想法却一直被掩埋在John的记忆深处,这也正是最终垂暮的John想要“去月球”的根源。冥冥之中,失忆的John又在学校与River重新相遇、随后相识、相爱。尝试多种办法帮助John恢复记忆无果的River最终选择了折纸兔子、坚持在灯塔下盖房的方式帮助John回想起了“去月球”的心愿,希望能够找回那晚陪她在草地上欣赏星空的男生。

微信图片\_20220911095949.jpg

科学家最终在记忆里推迟了John与River的初识,利用航天宣讲会为John埋下了”去月球”的心愿种子。在John新的记忆世界中,John与River各自努力,终于在NASA的航天员训练中心中相识、相爱,走到了最后。

当游戏的记忆碎片被一块块拾起、拼凑,John的生命也走向了尽头,游戏画面定格在了载有John和River的奔月航天飞机发射的一刻。五个小时的游戏流程远远说不上多,但《Moon》留给玩家的思考却是多样而长久的,非常值得一玩。近乎掩耳盗铃的编纂记忆是否真的比骨感的现实更值得保留?当爱的人失去记忆,是否重新相识会有更好的结局?在抹平了生活的遗憾和痛感之后,人生是否也失去了原来的经历感?每个玩家每次游玩,都有自己的答案和观点。

微信图片\_20220911095958.jpg

一如John为病重的River谱写并弹奏的钢琴曲《For River》游戏给人带来的感觉是那样的温情、充满希望。

涨知识!经典的游戏设计介绍

微信图片\_20220911095651.jpg

玩家或多或少都有对某款游戏“欲罢不能”的体验:或是在沙盒类游戏中专注塑造自己的世界,或是在竞技类游戏中不断追求高击杀和长连胜,也或是在剧情类游戏中沉浸久久不能平复……

当我们说一款游戏“好玩”、“上头”时,我们究竟在评价游戏的哪一个方面?游戏又如何有条理地整合视听资源,给玩家带来一个沉浸的交互体验?是什么将“游戏”这种互动的表达方式区别于“电影”、“音乐”等等艺术形式?这篇文章将带领读者分析几个经典的游戏设计实例,与大家聊聊出彩的游戏设计所强调的几个要素。

微信图片\_20220911095659.jpg

在之前的文章中我们介绍过,游戏是交互的艺术,而出彩的游戏设计可以很好地解决和交互相关的几个基本问题:为什么要交互(目的),怎样交互(玩法),以及交互的结果是什么(反馈)?通常,一款游戏在任何一个问题上有自己的建树都会被认为是一款精品游戏。

微信图片\_20220911095703.jpg

以拥有极高自由度的经典沙盒《我的世界》为例,在“为什么要交互”的问卷上,它的设计者们看似交了一张近乎有哲学意味的白卷,或者说,《我的世界》的游戏设计成功地把这个问题抛给了每一个玩家。几乎没有的游戏指引和任务系统结合一个没有边际的开放世界,给玩家提供了无数种游玩的可能性。

《我的世界》利用方块和像素化完成了对世界的抽象、简化,玩家可以操作史蒂夫在这里撸树、喂马、盖楼、开矿….. 玩家怀着自己充满想象力的目的与这个世界以一些基本的物理法则互动,构筑着完全属于自己的乐土。即使在《我的世界》中的“生存”模式下玩家可以选择披甲打怪,但游戏并没有直接地给出可供期望的奖励清单,游戏机制并没有赋予某项活动远超其他活动的太多的意义。一定程度上来说,生存过今晚才到来的第二天和“今天”并没有两样,玩家好像在真正地生活一般,一直在一个美丽生动的循环之中。

沙盒类的游戏往往都为玩家提供一个按照一定规则运行的客观世界,一般不会提供“世界观”。玩家会将自己的世界观映射到游戏中,并指导其对游戏的探索。相比之下,竞技游戏则是强目的导向的、基础玩法相对固定的。

微信图片\_20220911095711.png

以传统第一人称竞技射击游戏《反恐精英》为例,玩家一进入对局就被置身于警匪冲突的紧张关系中,被告知必须和敌方阵营决出胜负。小地图、炸弹倒计时、血量、弹药等等关于对局进度的关键信息被清晰地显示在屏幕上,经验丰富的玩家可以很快地依据这些信息给出合理的判断。对于热门的竞技地图而言,对局前期的套路几乎是一致的,然而,不断变化的是双方自主选择的攻防策略、队员分布。这些大都是玩家在玩法上针对游戏内容主动发觉衍生的新想法、新思路。可以说,此类竞技游戏的设计关键点在于玩法的公平性,而玩法的公平性又衍生为游戏道具、游戏地图的公平性。对于一套大家都熟知的武器系统,一张得到时间验证的相对公平的地图,即使沿用十几年也是没有问题的,游戏资源服务于游戏核心设计,这也正是《反恐精英》系列能够始终作为电子游戏常青藤之一的原因。即时的计分板排名记录、开局人物状态的重置、对弱队的连败补偿等等机制,都在给予玩家一个“再来一把,还有机会”的积极心理反馈,让玩家主动参与到每一个比赛中去。

微信图片\_20220911095714.jpg

“游戏制作就像上帝创造世界”,此话不假。游戏设计就是在为虚拟世界制定物理定律、定义时间流向,塑造地形地貌,雕刻人文景观。和其他艺术一样,出彩的游戏设计源于生活,却又高于生活,最终回归生活。合格的游戏设计会在核心问题上十分注意细节,一刀一划地推进,而在不该限制、应当留白的部分把主动权交到玩家手中,与玩家共同合作出一款经得起考验的、十分有趣味的游戏。

游戏设计的最终呈现方式也许就像《我的世界》中那样简朴、凝练,“没有设计”就是最好的设计,也可能像《反恐精英》一样,以各种元素刺激着玩家的感官。设计没有高低之分,每款游戏都有自己的受众群体,但,游戏设计应当连贯、条理清晰,让玩家能够在花费一定的学习成本后,获得其享受的内容。

微信图片\_20220911095719.png

涨知识!游戏制作小白初入门

从1952年诞生在剑桥大学数学实验室中的《井字棋》到如今在全球一年狂揽28亿美金的《王者荣耀》,七十年间电子游戏不断发展、扩充、完善,其作为“第九大艺术”的价值正在被越来越多的群体认可与接受,《DOTA2》等一众流行的游戏项目加入亚运会就是一个很好的例证。

随着手机、电脑硬件性能的持续增长和相关工具软件功能的不断完善,参与游戏制作的门槛正在逐步降低,一个游戏从原始构思到最终成为一个可以游玩的成品所必须的环节简化了不少。从某种意义上来说,制作游戏的最初设计部分的工作在今天正发挥着往日难以想象的作用,一个出彩的游戏设计会让后续的工作事半功倍。

如果读者有一个有趣的灵感希望发展成可以和好友们同乐的游戏,或者对制作游戏的主要流程与分工感兴趣,不妨读完这篇文章。

微信图片\_20220911095610.jpg

游戏制作伊始,在充分的交流和可行性论证后,制作者需要将零碎的灵感扩写成相对丰满的故事,让对应的角色拥有鲜明的性格与背景故事,交代其来到游戏世界的动机、能力,这样可以让整个游戏具有初步的逻辑与世界观。在游戏拥有基本的角色形象和人物关系后,制作者便可以着手于定位玩家目标群体和确认游戏大致类型。

游戏类型和最初的构思灵感息息相关,不同的游戏类型提供着不一样的游戏体验,有些游戏强调烧脑解谜,而有些游戏又追求简单休闲…… 横板闯关、推塔MOBA、第一人称射击、角色扮演、即时策略、体育等等游戏类型都有各自的优点与短板。对于个人、小团体游戏制作者而言,这些主流的游戏类型在网络上已经有很多相当成熟的模板和资源供学习和借鉴,只需要对初始的灵感稍作加工,制作者就可以很好地利用资源,产出自己的游戏。在确定了游戏的大致方向以后,一些具体的内容就可以被高效地讨论出来了。

微信图片\_20220911095613.jpg

一般的,游戏开发文档(Game Design Document)会在游戏进入实际开发前完成。文档使用文字与美术画稿的方式记录游戏的基础世界观、地图地貌、主要角色、技能特效、推荐玩法、流程长度等等呈现在玩家面前的元素,为整个游戏提供了主要框架。参与制作的人员会被分工,通常,游戏的制作将分为策划、程序与美术三个大块,策划在与程序、美术沟通后会提出可能的思路和修改建议,程序将负责游戏核心玩法的实现及主要功能的维护,美术则确定游戏的画面效果,给玩家留下一个特定的美术印象。当然,成员的分工视团队的大小而定,个人制作游戏相当具有挑战性,非常锻炼制作者的耐心和毅力。许多商业游戏会在文档完成后利用原型图、概念图启动游戏的宣发,维护粉丝社区,以保证在游戏接近完成之时拥有足够的热度和流量。

微信图片\_20220911095617.jpg

游戏开发的过程,实际上也就是创作并整合游戏逻辑、人物地图、贴图素材、音频音效、UI的过程。与早期和代码死磕不同,现代图形技术已经产生出很多直观易用的软件来辅助制作者产出内容,例如Maya,Blender,Unreal,Godot等等,都是相当著名的模型生产工具和游戏引擎。负责程序的成员会用编程语言(C++、Java等等)来为游戏添加逻辑脚本,为游戏世界制定规则。当然,如果制作者只想做一些小游戏小功能,现成的游戏引擎或许显得过于臃肿,一些著名的图形库例如EasyX、OpenGL等已经可以满足需求。不用担心自己的游戏梦会因为不精通编程语言而止步,事实上,可视化编程功能已经处于一个比较成熟的阶段,在熟悉引擎的基本使用后,“蓝图”已经可以帮助开发者利用连线和基础的数学知识完成对游戏世界的创造和编辑。

微信图片\_20220911095620.jpg

为了能够在游戏中提供给玩家一个更经得起揣摩、探索的世界,制作者需要向相关人员问询情况,在实际生活中留心取材,了解对应的学科知识。新手时借鉴学习,未尝不是一个好的选择。

游戏是一门综合应用的学问,也是一门新兴的艺术,交互的艺术。或许艺术并不该有过多的条条框框,本文章作为一篇概述,介绍的只是一般游戏的主流开发过程,实际上一款游戏的诞生需要考虑的因素是相当多的。制作一款”成功”的精品独立游戏难度不亚于创业盈利,但是只要有心投入,乐于结识志同道合的伙伴,制作一款属于自己的游戏来圆游戏梦还是比较容易的。

微信图片\_20220911095623.jpg