罐头的编程工作室

行动 洞察 整合


  • 首页

  • 归档

【游戏开发】用二进制形式安全存档

发表于 2020-02-15

在Unity中存档机制实现的三种方式

在Unity中搭建一个存档机制,可以借助Unity自带的PlayerPrefs类,或者使用xml及json此类格式化文档。而除此之外,一种更安全、也并不复杂的方式是转化为二进制格式的文件进行存储。

二进制存档机制实现原理

此类方法主要借助一个被称为BinaryFormatter的二进制转换器,先将游戏内的玩家数据转换为二进制数据,再通过文件流写入存档文件。读档则进行相反的操作。

示例的实现过程

为了简化过程,我创建了一个最简单的示例,即只有一个整型游戏数值需要保存的情况。如果需要保存Vector3或Color等复杂的引用类型,需要将其拆为一个一维数组。

创建示例场景

在U3D中新建工程,添加一个Text文本框和三个按钮组件。文本框显示玩家数值。三个按钮组件中,其中两个是用于存档和读档的,另一个单击一次就能使文本框中的数值减一。
场景中新建一个名为Player的空物体,添加脚本Player.cs,代码如下:

Player
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
using UnityEngine.UI; //使用UI组件相关语句必用的

public class Player : MonoBehaviour
{
static public Player S; //单例模式,方便其他脚本查找此脚本的数据
public int score; //此次示例的玩家数据
public Text scoreRec; //对文本框组件的引用
// Start is called before the first frame update
void Start()
{
S = this;
score = 100;
}

// Update is called once per frame
void Update()
{
scoreRec.text = "" + score; //随着玩家数据改变,文本框数据也更新
}
}

脚本写完后,需要将文本框拖拽赋值给scoreRec变量。
再新建一个脚本Minus.cs,用于手动减小score的值,代码如下:

Minus
1
2
3
4
5
6
7
8
9
using UnityEngine;

public class Minus : MonoBehaviour
{
public void MinusOne()
{
Player.S.score -= 1;
}
}

将此脚本挂在Main Camera上(或者随便什么地方,只要你觉得方便操作就行),然后将其中的MinusOne函数赋给减一按钮的On Click。
此时运行游戏,文本框将显示100,每单击一次减一的按钮,数值就会变小。准备工作完成。

实现二进制文件存读档

现在新建一个脚本PlayerData.cs。
这个脚本是一个类,方便将所有玩家数据打包。我们的示例只有一个数据,所以无法体现这一点。不过我们还是新建它。代码如下:

PlayerData
1
2
3
4
5
6
7
8
9
[System.Serializable]   //可序列化,详细意义可以自行百度,相信你会有所收获
public class PlayerData
{
public int score;
public PlayerData(Player player) //构造函数,传入一个Player类,即我们之前写好的那个Player.cs的实例
{
score = player.score; //将Player类中的数据传给PlayerData
}
}

之后新建脚本SaveSystem.cs,和PlayerData类一样,它不需要挂在任何物体上,所以也不需要继承自MonoBehaviour。
在这个类中,我们完成新建用于存储数据的文档、打包玩家数据、使用formatter进行序列化的存档操作;以及读取数据存储文档、使用formatter反序列化、将玩家数据返回的读档操作。代码如下:

SaveSystem
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
using UnityEngine;
using System.IO; //管理文件数据流
using System.Runtime.Serialization.Formatters.Binary; //二进制转换器

public static class SaveSystem
{
public static void SavePlayer(Player player) //存档函数
{
BinaryFormatter formatter = new BinaryFormatter(); //新建二进制转换器
string path = Application.persistentDataPath + "/player.pl"; //路径字符串,引号中是文件名,由于此文件是二进制文件,故后缀名可以随意
FileStream stream = new FileStream(path, FileMode.Create); //使用刚才的路径新建文件输入流

PlayerData data = new PlayerData(player); //将传入的Player类数据打包入PlayerData类

formatter.Serialize(stream, data); //序列化PlayerData类,并存入文件player.pl
stream.Close();
}

public static PlayerData LoadPlayer() //读档函数
{
string path = Application.persistentDataPath + "/player.pl"; //路径字符串,用于寻找数据存储文件
if (File.Exists(path)) //判断此文件是否存在,如果存在
{
BinaryFormatter formatter = new BinaryFormatter(); //新建二进制转换器
FileStream stream = new FileStream(path, FileMode.Open); //对路径path建立文件输出流

PlayerData data = formatter.Deserialize(stream) as PlayerData; //用二进制转换器对此输出流中的数据反序列化,并映射为PlayerData类实例
stream.Close();

return data; //返回类实例
}
else //如果文件不存在,报错
{
Debug.LogError("Save file not found in " + path);
return null;
}
}
}

现在要做的最后一步就是将SavePlayer函数和LoadPlayer函数赋给两个按钮,使我们可以通过点击来存档、读档。
首先在Player类中加入以下代码:

Player
1
2
3
4
5
6
7
8
9
public void SavePlayer()
{
SaveSystem.SavePlayer(this);
}
public void LoadPlayer()
{
PlayerData data = SaveSystem.LoadPlayer();
score = data.score;
}

之后将Player拖拽给存档和读档按钮的On Click,并分别选择SavePlayer和LoadPlayer函数。
完成,现在我们运行游戏,例如我们点击减一按钮使数值变成88,点击存档之后,不论是否退出过游戏,再点击读档,都可以使数值重新变成88。

【游戏开发】Unity3D的场景切换(开始、游戏场景、退出游戏)

发表于 2019-10-27

游戏中的开始界面到游戏界面的切换,以及游戏退出如何在Unity3D中实现?

基本资源

需要在Assets下建立两个场景(右键->Create->Scene)一个场景作为开始菜单,另一个场景作为游戏界面。
在开始场景中新建画布(Canvas),画布中新建两个按钮(Button)。一个按钮中文本为“开始游戏”,另一个为“退出游戏”。

脚本

Assets下新建脚本MainMenu,用来管理场景切换。
需要的两个命名空间分别为using UnityEngine;和using UnityEngine.SceneManagement;。其中,using UnityEngine.SceneManagement;用来管理场景。
删除不必要的Start()和Update()函数,然后在MainMenu类中添加如下代码:

SceneManager
1
2
3
4
5
6
7
8
9
10
11
12
13
//用这个函数触发切换到场景列表中下一个场景
public void PlayGame()
{
//加载编号比当前场景大1的场景
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}

//用这个函数退出游戏
public void QuitGame()
{
Debug.Log("Quit!");
Application.Quit();
}

调整场景列表和按钮函数

现在基本的场景和脚本都准备完毕。剩下的操作为:

  1. 将MainMenu脚本拖拽至开始菜单场景的MainCamera上
  2. Unity中打开File->Build Settings,将游戏开始场景拖拽至Scenes In Build窗口,注意开始场景的编号应比游戏场景多1
  3. 给开始按钮添加函数PlayGame,给退出按钮添加QuitGame函数(在监视面板的Button中将MainCamera拖至On Click内,选择需要的函数)

完成,现在运行游戏后点击“开始游戏”,场景将切换至游戏场景。
想要从游戏界面回到菜单,则函数类似,只不过变成场景编号-1。
注意,Application.Quit()的效果只能在Build之后查看,Unity编辑器中没有效果(Unity编辑器中只有我们添加的控制台语句)。

【游戏开发】Unity中的txt文本读取及显示

发表于 2019-10-09 更新于 2019-10-27

如何使用脚本将一个txt文本逐行读入Unity并打印?

命名空间

首先需要在脚本中包含using System.IO;。
这个不是Unity的,而是C#中的,用于处理文件读写。包含这个,才能使用进行文件读取所需要的类和方法。
之后,加上用于操作Unity中UI控件的using UnityEngine.UI;。
准备完毕。

类对象

下一步,在类中定义public Text myText;然后在Unity窗口操作,使其引用一个游戏中的文本框,便于之后修改并显示文本。
此外,需要定义private string fileName;用于存储txt文件的相对路径;定义private string[] strs;用于存储读取的文本内容。

txt文本文件

将需要的文本文件写好,每一句用换行符分隔。
txt文件存储的位置随意,只要位于工程文件夹(即用本工程名称命名的文件夹)内即可。
注意记住该位置相对于工程文件夹的相对位置。
如我在工程文件夹下新建文件夹命名为“Text”,并存储文件“A.txt”在文件夹内。

脚本

脚本逻辑如下:

  1. 首先中为对象赋初值:fileName = "Text/A.txt"; //读取文件位置;
  2. 然后开始读取并存储文本:strs = File.ReadAllLines(fileName); //将文件内容存入strs;
  3. 最后将句子显示在文本框中:foreach (string str in strs){myText.text += str; myText.text += "\n";} //更新文本框内容;

完整代码

ShowText
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using UnityEngine.UI;

public class ShowText : MonoBehaviour
{
public Text myText;

private string fileName;
private string[] strs;
// Start is called before the first frame update
void Start()
{
fileName = "Text/A.txt";
strs = File.ReadAllLines(fileName);

myText.text = "";

foreach(string str in strs)
{
myText.text += str;
myText.text += "\n";
}
}

}

Unity中效果

【读书笔记】C++泛型编程学习小结

发表于 2019-08-23

泛型指针(Iterator)

指针作为算法中常用的工具,在泛型编程中进行了“泛型化”,使其可以针对不同容器展现相同行为,达到了十分便捷、通用的抽象效果。
取得iterator,可以借助标准容器的begin()及end()操作函数,前者返回指向第一个元素的iterator,后者指向最后一个元素。
iterator可以进行赋值(assign)、比较(compare)、递增(increment)、提领(dereference)操作。
用法案例:

  • 一个指向vector开头的泛型指针:vector<string>::iterator iter=svec.begin();
  • 一个指向vector结尾的常量泛型指针:vector<string>::const_iterator iter=vec.end();

泛型算法(Generic Algorithms)

使用泛型算法

头文件:#include <algorithm>
泛型算法与泛型指针配合使用:binary_search(vec.begin(), vec.end(), elem);

设计泛型算法

Function Object

function object可以消除泛型算法中“通过函数指针来调用函数”时需付出的额外代价。
使用预先定义的function object,需包含头文件:#include <functional>
使用案例:sort(vec.begin(), vec.end(), greater<int>);其中的greater<int>()会产生一个未命名的class template object,传给sort()。

Function Object Adapter

标准库提供了adapter(适配器)来完成function object的个性化使用。
binder adapter(绑定适配器)可以将function object的参数绑定至某特定值,使binary function object转化为unary function object。
negator可以对function object的真伪值取反。

Iterator Inserter

可以使用特定的赋值函数取代assignment运算符。
需包含头文件:#include <iterator>

iostream Iterator

此类泛型指针可以指向各类输入输出设备,结合泛型算法copy()使用,可以进行输入输出。
也需包含头文件:#include <iterator>

C++中srand()及rand()的最简单用法浅析

发表于 2019-07-27 更新于 2019-08-23

生成5-200之间的随机整数

C++中,标准库提供了一种伪随机数(pseudo-random number)生成器,可以达到随机获取某两个数之间的一个数的效果。
我们以随机获取5-200之间的一个整数为例。首先看完整代码。

pseudo-random number
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <cstdlib> //为使用srand()及rand()引入标准库

const int MIN = 5; //随机数最小值为5
const int MAX = 200; //随机数最小值为200

int main(){
using std::cout;

srand( MAX - MIN ); //设置随机数种子
int my_random;
for (int i = 0;i<10;i++){
my_random = rand() % (MAX - MIN) + MIN; //获取一个随机数并打印
cout<<my_random<<"\n";
}

return 0;
}

在我的计算机上随机到的结果为:96,85,165,176,191,116,68,91,5,19。可以看出代码是没有问题的。

语法解析

首先,#include <cstdlib>在头文件中引入了C++的标准库,之后才可以使用srand()以及rand()这两个函数。之后初始化最大值和最小值。
srand()函数用于设置种子(seed),种子可以简单理解为之后rand()所获得的随机数的最小值。而rand()所能获得的最大值为整型的最大值,rand() % (MAX - MIN)将等概率得到一个0-195之间的数,因为求余运算使得这个数只能落在0-195之间。
最后,my_random = rand() % (MAX - MIN) + MIN;将获得一个5-200之间的随机数。

扩展使用

同理,不论是想要获得怎样的随机数,只要首先将其转化为等价的0-N随机数获取,再进行结果值调整即可。例如,获得一个-50.0-50.0之间的随机一位小数,只要改动代码如下。

more pseudo-random number
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <cstdlib> //为使用srand()及rand()引入标准库
#include <iomanip> //设置格式

const int MIN = -50; //随机数最小值为-50
const int MAX = 50; //随机数最小值为50

int main(){
using namespace std;

cout<<setiosflags(ios::fixed)<<setprecision(1);

srand(( MAX - MIN )*10); //设置随机数种子
float my_random;
for (int i = 0;i<10;i++){
my_random = (rand() % (( MAX - MIN )*10))/10.0 + MIN; //获取一个随机数并打印
cout<<my_random<<"\n";
}

return 0;
}

关于删除hexo个人博客文章时的坑

发表于 2019-07-24

记录一个删除hexo博文时的坑

别在source文件夹为空时执行hexo clean

网上许多文章说删除hexo文章时,先在source文件夹下删掉相应的md文件,再执行hexo g。
这时很多人会发现文章还是存在,于是又有人说要执行hexo clean。
我试了之后发现,
千万不要在_post文件夹中没有文件时执行hexo clean!!!这样做会使你的整个博客404!!!


解决方案

如果已经执行了而且真的404了怎么办?

不用重新搭建博客!只需要把文件夹结构恢复至初始结构就行了,我当时给source文件夹下重新添加了_post文件夹,然后用hexo new重新发一篇新的文章,再hexo g就好了。

正确的清空文章姿势

原则是_post文件夹不能为空,所以主要有以下几种方法:

  1. 等到下一篇文章创建后再删除之前的
  2. 直接删除原文章的标题和内容,但不要删除文件,等想要写下一篇时在这个文件上改
  3. 给文章设置隐藏。请参考博文:hexo首页隐藏部分文章

出错原因

最后来总结一下原理。

首先,hexo g执行后为什么文章仍然存在?

在hexo官方文档中,说明了g(generate)指令是生成静态网页的指令。且在生成文件的说明中指出,g指令将比对原有文件,有变动的文件才会更新。我尝试之后发现,g指令没有任何问题,问题出在hexo d!推送到远端后,远端的文章没有被删除,这才是根源。

那么hexo clean后为何造成网站崩溃呢?

仍旧查看hexo官方文档,hexo clean是一条用于“清除缓存文件 (db.json) 和已生成的静态文件 (public)”的指令。public文件夹下存放的是hexo g后生成的静态网站,其中有一个代表博客主页的index.html,而该网站的生成需要有至少一个_post文件夹下的文章。如果_post文件夹下是空的,在hexo clean后hexo g,会因为找不到publish文件夹下的index.html而404。

简单的计算机硬件原型特点小结

发表于 2019-07-22

一、常用简写

IC:计算机内部元件

主要IC

  • CPU:中央处理器,相当于计算机的大脑
  • I/O:负责把键盘、鼠标、显示器等周边设备和主机连接在一起,实现数据的输入与输出
  • 内存:存储指令和数据

如何更加生动地理解呢?
计算机最基本的功能就是输入数据、处理数据、输出数据,而CPU、I\O和内存配合,正是为了完成这样的步骤。
我个人喜欢将计算机想象成加工工厂,I\O作为传送带将需要的零件运送进来,内存存放零件和图纸,CPU按照图纸加工零件,最后再由I\O运送出厂。

IC引脚

  • Vcc与GND:用于为IC供电
  • A:Address,即地址,代表地址总线引脚,指定输入输出数据时的源头或目的地
  • D:Data,即数据,代表数据总线引脚,用该引脚进行数据的输入输出
  • P:Port,即端口,I\O与外部设备之间输入输出数据的场所
  • C:Control,即控制模式,
  • NC:No Connection,表示该引脚什么都不连接
  • CLK:Clock,即时钟,通过时钟引脚保证CPU和I\O的频率同步
  • MREQ:Memory Request,内存请求
  • IORQ:I\O Request,I\O请求,和MREQ一起,负责区分访问对象是内存还是I\O
  • CE:Chip Enable,选通芯片,设置IC的激活状态
  • RD:Read,输入引脚
  • WE:Write,输出引脚
  • 控制引脚
    • M1:Machine Cycle 1,机器周期1
    • INT:Interrupt,中断
    • RESET:Reset,重置。先设置为0,再还原为1,可以重置CPU
    • BUSRQ:Bus Request,总线请求,可以设置DMA(直接存储器访问,不通过CPU进内存)模式
    • BUSAK:Bus Acknowledge,响应总线请求

二、原型制作步骤总结

1. 连接电源、数据和地址总线

1.1 连接CPU、内存和I\O的Vcc引脚和GND引脚
1.2 连接CPU与内存之间的地址总线引脚和数据总线引脚

2. 连接I\O

2.1 连接CPU与I\O的数据总线引脚
2.2 连接I\O的寄存器与CPU的地址总线引脚

3. 连接时钟信号

3.1 连接时钟发生器与CPU、I\O的CLK引脚

4. 连接用于区分读写对象是内存还是I\O的引脚

4.1 将CPU的MREQ引脚连接至内存的CE引脚
4.2 将CPU的IORQ引脚连接至I\O的CE引脚和IORQ引脚上
4.3 连接CPU的RD引脚与内存的RD引脚
4.4 连接CPU的WR引脚与I\O的WE引脚

5. 连接剩余的控制引脚

5.1 连接CPU与I\O的M1引脚和INT引脚
5.2 连接CPU的RESET、BUSRQ和BUSAK引脚

6. 连接外部设备,通过DMA输入程序

7. 连接用于输入输出的外部设备

8. 输入测试程序并进行调试

Skitar

Skitar

官方认证扯犊子杂学家
7 日志
10 标签
GitHub
© 2020 Skitar
|