Intro to Android

Application Components

Services

A service is an component that runs at the background

E.g. music play at the background

Service is not a thread

Broadcast Receiver

Receives broadcast from Android system and other apps and responses with pre-coded actions.

Apps are required to regist to get the broadcast

Example: When the battery level changes, the system broadcast a message.

Activity

Offen refer to one interface on your phone.

Primary class for interacting with user.

For example, Wechat:

  • When you click on the app launcher, its corresponding greeting page
    will be shown to you.

    Android system invokes the main activity of the Wechat.

  • Apps that request for online payment (such as railway ticket
    payment) can directly reach the payment page.

    Another app invokes the payment activity of Wechat.

  • Apps that request for “share on moments” can directly invoke the
    moments sharing page of Wechat.

    Another app invokes the “share on moments” activity of Wechat

Back Stack

When a new activty is launched, the previous activity will be paused and sent to the top of the back stack

3074c66675d6c28231d50f91cb67a2ae.png

Life circle

  • Runing: The activity is on the top of the screen and gained focus. Running will not be killed.

  • Paused: The activity is on the top of the screen and gained focus.

  • Stopped: The activity is completely covered by another running activity.

  • Destroyed

When running short of memory, a stopped activity is more likely to get killed than a paused/running activity.

5f74b1146359b2da39a899a5156f07a6.png

Good implementation of callback functions can make your app more robust and performant.

Possible issues with a bad implementation:

  • Crashing if the user receives a phone call or switches to another app while using your app.

  • Consuming valuable system resources when the user is not actively using it.

  • Losing the user’s progress if they leave your app and return to it at a later time.

  • Crashing or losing the user’s progress when the screen rotates between landscape and portrait orientation.

CallBack Funtions: Typical Uses
  • onCreate(): Initial setup, load persistent state.

  • onRestart(): read cached state

  • onStart(): reset application

  • onResume(): start foreground-only behaviors

  • onPause(): shutdown foreground-only behaviors

  • For example: temporarily stop UI animations.

  • onStop(): cache state

  • onDestroy(): save persistent state

The above points are very general. Carefully design your app and keep the life cycle graph in mind.

Create Activities

23c0e5afcf14c33b320504479143645f.png

  1. Create a new Activity class. Which either inherits Android.app.Activity or its subclasses.

  2. Override Activity.onCreate().

  3. Create a layout XML file in res/layout and use setContentView() to load this layout.

  4. Register the new activity in AndroidManifest.xml.

    If it is a main activity, you need to add a special section in the manifest file.

    5f50cb9f027a1bd432f0dc1ea3bb051c.png

Activities can be started by calling the function

1
startActivity(Intent intent)

To call Activity2 Inside an activity, do:

1
2
3
4
Intent intent =
new Intent(this, Activity2.class);

startActivity(intent);

The name of the target activity is not always explicitly specified. For instance, to let Android system choose an suitable activity for sending email (in Lecture 9):

1
2
3
Intent intent = new Intent(Intent.ACTION_SEND);
Intent.putExtra(Intent.EXTRA_EMAIL, recipientArray);
startActivity(intent);

Obtain vals from another activity

Sometimes we wish to obtain results from another activity. We need to start the activity using

1
startActivityForResult()

You must also implement the function below to get the return result

1
onActivityResult()

e3abb411a93727fb5764515c2ad064d3.png

Closing Activities

Android will automatically manage the life cycles of your activities.

You can destroy the current activity manually by calling finish().

To finish an activity that you previously invoked with

1
2
startActivityForResult(Intent,
int), use finishActivity(int requestCode)

Can be handy when you want to make sure that the user won’t return to this activity in the future.

Passing Data between Activities

f7362876239d8ffc3874022bfed42ee0.png

Bundle and Intent is actually the same thing.

Console output

Your console output (System.out) can be seen from the “run” window in Android Studio.

You should normally use Log class instead, though:

Android Logcat | Log.v(), Log.d(), Log.i(), Log.w(), Log.e() - EyeHunts

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
输入

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

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