游戏设计整体构思
游戏主体
首先游戏需要一个开场的渐变动画,之后再显示游戏开始选项界面,界面上面得有开始游戏,继续游戏和退出游戏这些基本按键。第一次进入游戏要加载主角前往岛屿的过场动画,过场动画播放完成之后主角正式登上岛屿。游戏采用上帝视角,角色可以前后左右移动,点击鼠标右键可以进行镜头的移动。游戏界面上方需要有道具卡,道具卡的右下方得显示道具卡的剩余数量。当鼠标移动到道具卡上面时显示道具信息,鼠标移开后道具信息隐藏。在岛屿上面的每个守护者都要有一个巡逻路线,守护者都按照指定的巡逻路线行走,守护者们带有触发器,当主角进入守护者的触发器范围,守护者就是奔跑到主角身边,之后触发对话,在对话结束之后便进入挑战。
追逐挑战
进入挑战时利用对话框介绍游戏的玩法,之后便开始倒计时,倒计时解释后游戏开始。游戏玩法为在规定时间内,保持主角永远跑在守护者前面,守护者会不定时的加速。在游戏画面的中间有个按键提示图,玩家要准确的按出按键提示图中的按钮主角才会加速,反之如果按错了,主角就会减速。在追逐的过程中,人物会随机的进行对话。当主角的速度到达规定值的时候,主角就会骑上摩托车。
炫舞挑战
进入挑战后,游戏镜头要从远处移动到人物所在的位置,最终聚焦于人物身上。之后也是通过对话介绍玩法,倒计时后正式开始挑战。游戏界面的下方分批显示按键提示,玩家按对按键就会加一分。每批按键都有限制时间,过了限制时间就会切换到下一批按键,玩家要在规定时间内按出对应的按键。根据玩家在规定时间内按对的按键数,显示完美,一般般或者差等提示。当玩家在规定时间内全部按对按键主角就会成为C位,反之主角退出C位。玩家的最终得分要超过总分的百分之六十才能算是挑战成功。
问答挑战
进入挑战后同样是通过对话介绍游戏玩法。该游戏玩法就是问答,玩家要准确的回答出问题的答案,遇到不会的问题,玩家可以消耗道具券来查看答案,但是道具券的数量是有限的,每用一次就消耗一个道具卡,当道具卡使用完了就不能查看答案了。玩家要回答出所有的题目才算是完成挑战。
挑战结束
当挑战结束时要判断是否挑战成功,挑战成功后就弹出胜利的弹窗,反之弹出失败的图标。挑战结束后可以消耗道具卡重新挑战一次。挑战成功后主角可以获得该挑战的祝福,挑战失败就没有祝福。
游戏详细设计
logo的淡出淡入
利用Animation给图片创建一个动画,动画内容就是修改其透明度让其在1秒内从0到1再到0,这就出现渐变效果了,最后在动画播放结束时触发事件触发器,展示游戏开始界面 关键代码:
public void LoadOtherScene()
{
this.gameObject.SetActive(false);
canvas.gameObject.SetActive(true);
}
游戏开始界面是一个Panel面板,选择了一个图片作为Panel面板的背景,之后为面板添加3个按钮作为它的孩子对象,这3个按钮分别是开始游戏、继续游戏和退出游戏按钮。这3个按钮也相应的换上漂亮的背景。之后为3个按钮绑定点击事件。 实现效果: 按钮点击事件关键代码:
private void GameStart()
{
panel.SetActive(false);
darkPanel.SetActive(true);
flowchart.ExecuteBlock("New Block");
}
private void GameContinue()
{
SceneManager.LoadScene("QAForest");
}
private void QuitGame()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
游戏环境设计
游戏资源都是从网上找的,游戏的主环境是一个岛屿,首先创建一个地形Terrain,为地形设置绿色的草地贴图,然后设置地形中间凸起,使其像个岛屿。最后将下载的树木和建筑模型布置到地形上面。岛屿旁边还得有水,由于不会Sharder编程,只能去网上寻找现成的水资源,然后将它布置在岛屿旁边,最终整个岛屿就完成了。
游戏主界面UI设置
道具卡UI
游戏首次进入的时候界面的左上角有两个道具卡,分别是再来一次和洞察,再来一次道具卡的作用是再次挑战一次,洞察道具卡的作用是查看问题答案。这两个道具卡分别有自己的图标。实际上,他们就是两个image,在每个图片的右下角添加文字控件Text显示该道具剩余的数量。之后再创建一个文本框,用来显示道具说明。为这些道具图片添加触摸事件,当鼠标移动上去的时候显示道具说明,鼠标移开则隐藏道具说明文本框。 最终效果图:
关键代码:
private void ImageAddPointEvent()
{
for(int i=0;i<images.Length;i++)
{
EventTrigger eventTrigger = images[i].GetComponent<EventTrigger>();
int k = i;
UnityAction<BaseEventData> callback = new UnityAction<BaseEventData>(data=>PointEnterImage(k,data));
EventTrigger.Entry entry = new EventTrigger.Entry
{
eventID = EventTriggerType.PointerEnter
};
entry.callback.AddListener(callback);
eventTrigger.triggers.Add(entry);
entry = new EventTrigger.Entry
{
eventID = EventTriggerType.PointerExit
};
entry.callback.AddListener(new UnityAction<BaseEventData>(data=>tip.gameObject.SetActive(false)));
eventTrigger.triggers.Add(entry);
}
}
游戏设置界面UI
在游戏当中,按下esc键就会弹出设置窗体并且暂停游戏。设置窗体中分为两个分页,一个是操作界面,一个是祝福界面。其中操作界面中可以执行游戏重新开始、返回游戏、退出游戏、调节游戏音量等操作。而祝福界面主要是显示目前获得的祝福情况。
关键代码:
private void ChangeVolumn()
{
CameraFollow.instance.ChangeVolume(volumnSlider.value);
}
private void ShowControlPanel()
{
controlButton.image.sprite = clickSprite;
blessButton.image.sprite = unClickSprite;
controlPanel.SetActive(true);
blessPanel.SetActive(false);
}
private void ShowBlessPanel()
{
controlButton.image.sprite = unClickSprite;
blessButton.image.sprite = clickSprite;
controlPanel.SetActive(false);
blessPanel.SetActive(true);
}
public void InitBlessTextShow()
{
if (QAForestConfig.blessing.Equals(""))
return;
string[] strs = QAForestConfig.blessing.Split(',');
for(int i=0;i<strs.Length;i++)
{
for(int j=0;j<blessTexts.Length;j++)
{
if(strs[i].Equals(blessTexts[j].text))
{
cupImgs =Util.GetObject<Image>(blessTexts[j].transform);
cupImgs[0].sprite = sprite;
cupImgs[0].color = new Color(1,239f/255f,0,93f/255f);
cupImgs[1].gameObject.SetActive(true);
}
}
}
}
游戏对话设计
首先前往Unity商城下载Fungus插件,下载完后点击import将其导入Unity3D引擎。导入成功后在Tools面板下面就可以看到Fungus,之后点击Create->FlowChart就是可以创建一个对话框了。之后我们打开FlowChart窗体就可以看见一个对话面板了,默认名字为New Block。然后我们可以利用这个Block编辑对话内容。
对话控件Block:
点击NewBlock,在Inspector面板就可以看到它拥有的组件了。其中Commands就是用来编辑对话操作的。点击下方的加号就可以添加内容了,其中Say代表添加对话内容,If和Else代表条件判断,SetVariable代表参数设置,InvokeEvent代表执行指定函数,LoadScene代表加载场景。 Block控件的Inspector面板:
人物以及敌人控制
主角移动与视野转换
在游戏当中,按下WASD这几个按键主角就会向前、向左、向后、向右移动。在主角移动的时候,摄像机要跟随主角以保证主角永远在视野范围内,这个就是视野的跟随。当点击鼠标右键并且拖动鼠标就可以实现摄像机围绕着主角旋转,这样可以让玩家选择一个适合自己的视野。当按住shift键时,主角就会切换为奔跑状态,移动速度也会相应的增加,放开shift键主角又会切换回行走状态,速度也会随之降低。 摄像机围绕主角旋转关键代码:
private void Update()
{
if(Input.GetMouseButton(1))
{
rotateAngle = Input.GetAxis("Mouse X");
}
CameraRoate();
}
private void LateUpdate()
{
this.transform.position = hero.transform.position - posi;
}
private void CameraRoate()
{
if(rotateAngle!=0 && HeroMove.instance.canWalk)
{
this.transform.RotateAround(hero.position, Vector3.up, rotateAngle);
hero.transform.Rotate(0,rotateAngle,0);
posi = hero.position - this.transform.position;
rotateAngle = 0;
}
}
主角移动关键代码:
public void Move()
{
Run();
hNum = Input.GetAxis("Horizontal");
vNum = Input.GetAxis("Vertical");
eulerAnglesY = Camera.main.transform.eulerAngles.y;
if (canWalk && (Mathf.Abs(hNum) > 0 || Mathf.Abs(vNum) > 0))
{
UpdateLookAtPosi();
this.transform.LookAt(lookAtPosi);
if (Mathf.Abs(anim.GetFloat("speed") - speed) > 0)
{
anim.SetFloat("speed", speed);
}
transform.Translate(new Vector3(0, 0, speed * Time.deltaTime));
}
else
{
anim.SetFloat("speed", 0);
}
}
private void Run()
{
if (Input.GetKeyDown(KeyCode.LeftShift))
{
speed += 2f;
}
else if (Input.GetKeyUp(KeyCode.LeftShift))
{
speed -= 2f;
}
}
private void UpdateLookAtPosi()
{
lookAtPosi = new Vector3(vNum, 0, -hNum);
lookAtPosi = Quaternion.AngleAxis(eulerAnglesY - rotateAngle, Vector3.up) * lookAtPosi;
lookAtPosi += transform.position;
}
效果:
敌人AI
在游戏中要尽量使敌人看起来更加的智能一点,所以就需要一点点AI。首先敌人要巡逻,巡逻用到的就是寻路算法,寻路算法常用的就是A*算法,但是Unity自带了自己的寻路组件,也就是Nav Mesh Agent组件,它允许您使用从场景自动创建的导航网格来创建可以在游戏世界中智能移动的角色几何,动态障碍使您可以在运行时更改角色的导航,而离网链接则使您可以建立特定的动作。所以我们在敌人身上添加Nav Mesh Agent组件,之后将场景的地面设置为Static,然后打开Navigation窗体,选择bake分页,在分页上面设置好各种参数,其中Agent Radius用来定义网格和地形边缘的距离,Agent Height用来定义可以通行的最高度,Max Slope定义可以爬上楼梯的最大坡度,Step Height定义可以登上台阶的最大高度,Drop Heights设置允许最大下落距离,Jump Distance设置允许最大的跳跃距离。最后点击Bake按钮,这样就会开始烘焙导航网格了,烘焙完导航网格后就可以跟根据网格来进行寻路的计算了。之后我们在地图上面选取几个地点作为敌人的寻路地点,然后通过代码切换地点就可以实现敌人的巡逻了。当然敌人也需要一个触发器作为他们的视野,当主角进入他们的视野后他们就要前往主角所在的位置,之后触发对话,对话结束后进入挑战。
敌人移动关键代码:
public bool Move(Vector3 posi,bool isHero)
{
if (InCameraView() && CanWalk)
{
RefreshPosi(posi);
if (Vector3.Distance(nowPosi, goalPosi) > meshAgent.stoppingDistance)
{
meshAgent.speed = isHero ? runSpeed : aWakeSpeed;
meshAgent.stoppingDistance = isHero ? distanceBetweenObject : 0.1f;
meshAgent.SetDestination(goalPosi);
if (Mathf.Abs(anim.GetFloat("speed") - meshAgent.speed) > 0)
{
anim.SetFloat("speed", meshAgent.speed);
}
return true;
}
anim.SetFloat("speed", 0);
return false;
}
else
{
return true;
}
}
private void RefreshPosi(Vector3 posi)
{
nowPosi= this.transform.position;
goalPosi.x = posi.x;
goalPosi.z = posi.z;
goalPosi.y = nowPosi.y;
}
private void OnTriggerStay(Collider other)
{
if(other.transform==heroMove.transform && !isFistTalk && !isTalk)
{
CanvasControl.instance.UpdateTipImgPosi(this.transform.position+new Vector3(0,5f,0));
CanvasControl.instance.ShowTipImg(true);
PushKeyToTalk();
}
else if(other.transform == heroMove.transform && isTalk)
{
isTalk = false;
goToHero = false;
isFistTalk = false;
UpdateCollderSize();
EnableComponent(false);
talkControl.InvokeRepeating("Talking",0f,0.01f);
}
}
public void UpdateCollderSize()
{
sCollider.radius = 2f;
sCollider.center = Vector3.zero;
}
public void EnableComponent(bool state)
{
sCollider.enabled = state;
this.enabled = state;
}
对话关键代码:
public bool Talking()
{
if (!talkingNow && flowChart.HasBlock(blockName))
{
flowChart.ExecuteBlock(blockName);
}
talkingNow =flowChart.GetBooleanVariable(isTalkingVariable);
if(!talkingNow)
{
HeroMove.instance.canWalk = true;
enemyMotor.isTalk = false;
enemyMotor.EnableComponent(true);
enemyMotor.eMove.CanWalk = true;
enemyMotor.GotoChallenge = flowChart.GetBooleanVariable(gotoChallenge);
CancelInvoke("Talking");
}
return talkingNow;
}
挑战设计
追逐挑战
追逐挑战主要是赛跑,在规定时间内挑战者必须要一直领跑。游戏开始时先有个对话,然后是开始倒计时,最后游戏开始。玩家要想保持领跑,必须得准确的按出提示按键,按对了玩家就加速,按错就减速
按键提示设计
在追逐挑战中,敌人随时会加速,所以要想保持领跑的状态必须得根据提示按键按出正确的字符。提示的按键放在整个游戏画面的正中间,本质就是一个图片不断的修改图源,提示按键的刷新机制是当玩家在键盘按下任何一个按键时就刷新,然后根据玩家按下的键判断是否按对了,按对了角色就加速,按错了角色就减速。 关键代码:
public static void ChangeWorld()
{
int index = Random.Range(0, 26);
theWorld = allChar[index];
}
public void UpdateImgSprite()
{
PursueConfig.ChangeWorld();
Sprite sprite = Resources.Load<Sprite>("world/" + PursueConfig.theWorld);
tipTextImg.sprite = sprite;
}
地形设计
在挑战中,两个角色都要不停的往前跑,所以游戏的地图要竟可能的无限大,但是如果做的太大又会比较消耗内存,进而降低了游戏的性能,所以要模拟无限地图。首先创建三个空的地板,分别在上面添加一些树木,花朵,建筑等点缀,然后将这3个地形紧挨在一起,我们称它们为A、B、C地形,再将它们的位置存储起来,最后利用碰撞检测判断角色是否跑到了B地形上面,是的话将A地形接到C地形后,当角色跑到C地形上时将B地形接到A地形后。如此反复,就可以实现无线地形了。
关键代码:
public void Change()
{
nowFloor = planeArr[0];
nowFloor.position += distancePosi;
planeArr.Remove(nowFloor);
planeArr.Add(nowFloor);
}
private void OnCollisionEnter(Collision collision)
{
if(collision.transform!=nowFloor)
{
nowFloor = collision.transform;
if (nowFloor.parent == LimitlessFloor.instance.planeArr[2])
LimitlessFloor.instance.Change();
}
}
炫舞挑战
炫舞挑战顾名思义就是跳舞的PK,玩家要控制自己的角色跳舞,通过按键的正确率来决定最终得分,得分达到总分的百分之六十就代表挑战成功。其实就是对炫舞游戏的拙劣的模仿
提示按键设计
在游戏画面的下方要放一个按键的提示框,里面显示着按键的信息。这些按键都是上后左右方向键的组合。按键提示有刷新机制,每隔5秒刷新一次,每次刷新的按键个数和类型都是随机的,而且每次按键提示的刷新都有倒计时,而这个倒计时是用滚动条模拟的,当滚动条滚到末尾的时候就是刷新的时机了。如果玩家按下的按键和提示的一样,那么该提示就会变色,否则出现红色的叉号。当玩家在规定时间内全部按对了这一批次的提示按键,那么就会从游戏画面底部飘出标志着完美的图标,如果只是按对一部分则飘出标志着一般般的图标,如果全部按错则出现标志着糟糕的图标。 关键代码:
public void RefreshShowArrow()
{
showArrowTipNum = Random.Range(0, 6);
int i = 0;
int j;
nowArrowIndex = 0;
System.Array.Clear(DancingConfig.ArrowData, 0, DancingConfig.ArrowData.Length);
while (i < arrowImages.Length)
{
if (i <= showArrowTipNum)
{
j = Random.Range(0, 4);
arrowImages[i].gameObject.SetActive(true);
arrowImages[i].sprite = greenArrows[j];
DancingConfig.ArrowData[i] = j;
}
else
{
arrowImages[i].gameObject.SetActive(false);
}
i++;
}
}
public bool IstrueArrowKeyDown(int num)
{
if (nowArrowIndex > showArrowTipNum)
return false;
if (num == -1 || num != DancingConfig.ArrowData[nowArrowIndex])
{
arrowImages[nowArrowIndex].sprite = errorSprite;
nowArrowIndex++;
return false;
}
else
{
arrowImages[nowArrowIndex].sprite = yellowArrows[num];
nowArrowIndex++;
mySore++;
}
return true;
}
void Update () {
if (DancingConfig.startDancing)
{
if (Input.anyKeyDown)
{
AnyKeyDownListener();
}
animatorStateInfo = animator.GetCurrentAnimatorStateInfo(0);
if (animatorStateInfo.IsName("all_vmd")&& animatorStateInfo.normalizedTime>=1f)
{
GameOver();
}
}
}
private void AnyKeyDownListener()
{
if (!Input.GetMouseButtonDown(0) && !Input.GetMouseButtonDown(1))
{
if (Input.GetKeyDown(KeyCode.UpArrow))
{
arrowIndex = 0;
}
else if (Input.GetKeyDown(KeyCode.DownArrow))
{
arrowIndex = 1;
}
else if (Input.GetKeyDown(KeyCode.LeftArrow))
{
arrowIndex = 2;
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
arrowIndex = 3;
}
else
{
arrowIndex = -1;
}
if(DancingCanvasControl.instance.IstrueArrowKeyDown(arrowIndex))
{
rightInputNum++;
}
}
}
让角色跳起舞来
首先将下载的MMD插件导入Unity中,之后选择一个漂亮的模型pmd文件导入Unity,再下载一个动作vmd文件复制进入Unity。最后点击自动生成的.MMD4Mecanim文件,将vmd动作包拉入Inspector面板对应的框中,点击Process按钮生成模型和动画。最后利用状态机将模型和动画绑定起来,一个会跳舞的角色就产生了。在游戏运行的过程中,规定处于前面的角色为舞蹈团的C位,当玩家把当前批次的按键全部按对时,玩家就变为C位,否则则退出C位。
关键代码:
private void ExChangePosi()
{
this.transform.position = Vector3.Lerp(this.transform.position, goalPosi, 0.2f);
princess.transform.position = Vector3.Lerp(princess.position, nowPosi, 0.2f);
if(this.transform.position==goalPosi)
{
CancelInvoke("ExChangePosi");
}
}
问答挑战
挑战就是答题挑战,玩家必须使出浑身解数答完所有的题目,并且要全部正确才算是挑战成功。
提问模块
这个模块就是通过对话的方式进行问题的提问,也就是问题都是显示在对话框中的。选项使用按钮来显示。首先将问题、选项和答案存储在xml文件当中,之后用代码将xml文件的数据取出来存储在哈希表中。最后按需求将哈希表的内容显示在对话框中。
关键代码:
private void LoadXml()
{
string path = Application.dataPath + "/XMLData/Answer.xml";
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.Load(path);
xmlNodeList = xmlDocument.SelectSingleNode("item").ChildNodes;
indexList = new List<int>(xmlNodeList.Count);
for (int i = 0; i < xmlNodeList.Count; i++)
{
indexList.Add(i);
}
}
private void RefreshQuestion()
{
int index = Random.Range(0,indexList.Count);
int i = 0;
lookAnswer.gameObject.SetActive(true);
foreach(XmlElement xmlElement in xmlNodeList[indexList[index]])
{
switch(xmlElement.Name)
{
case "question":
questionText.text = xmlElement.InnerText;
break;
case "answer":
answer = xmlElement.InnerText;
break;
case "select":
buttonTexts[i].text = xmlElement.InnerText;
i++;
break;
}
}
indexList.RemoveAt(index);
}
实现效果:
挑战结束
挑战结束后要根据分数计算玩家是否挑战成功,假如挑战成功则弹出挑战成功的弹窗,否则弹出挑战失败的弹窗。无论是挑战成功还是挑战失败,玩家都可以执行两个操作,一个是重玩,一个是退出挑战。点击重玩后,玩家可以消耗道具卡重新进行一次挑战,假如道具卡已经用完了,则会出现道具卡已用完的提示。 关键代码:
private void GameOver()
{
gameStartUI.gameObject.SetActive(false);
gameStopPanel.gameObject.SetActive(true);
if(isWin==0)
{
failTip.SetActive(true);
}
else
{
winTip.SetActive(true);
}
}
public void TryAgain()
{
if (QAForestConfig.chanceNum > 0)
{
QAForestConfig.chanceNum--;
ResetData();
}
else
{
tryAgainButton.SetActive(false);
tryAgainTip.SetActive(true);
}
}
数据存储
每次进入挑战和挑战结束后都要进行一次角色数据的存储,主要存储的是角色的状态信息和获得的祝福。存储数据是使用了Json,并将生成的.json文件存储在本地,等下次继续游戏的时候可以读取json文件的数据,然后以此来决定角色的个人信息。 代码:
[Serializable]
public class HeroData
{
public Vector3 nowPosi;
public Vector3 lookAtPosi;
public Vector3 cameraPosi;
public string name;
public bool firstTalk;
public int chanceNum;
public string hasBlessing;
public Quaternion cameraRotation;
public Vector3 shipPois;
public int passChance;
}
public void SaveHeroData()
{
UpdateHeroData();
string path = Application.dataPath + "/JsonDataFolder/" + heroName + ".json";
StreamWriter sw = new StreamWriter(path,false);
string jsonDataStr = JsonUtility.ToJson(heroData);
sw.Write(jsonDataStr);
sw.Close();
#if UNITY_EDITOR
AssetDatabase.Refresh();
#endif
}
|