提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作完整的地图和地图细节设置
- 二、制作摄像机系统
- 总结
前言
Hello大家好久不见,隔了几天没发文章并不是这几天放假了而是因为这期的工程量有点大,找素材和堆叠素材到场景中花费了很多时间,然后再制作一个完整的摄像机系统,我放弃了安装插件Cinemachine和Pro Camera2D之类的相机插件,而是自建了一个摄像机系统,所以花费的时间有点多,还有就是我做上头了,本来制作地图就够出一期了,但我做完后觉得不爽还是等做了一个摄像机系统才想起来要写文章。
OK话不多说,这期我们的主题是制作一张的完整地图和地图细节设置以及制作相机跟随玩家系统
一、制作完整的地图和地图细节设置
制作地图之前,我们可以给我们之前的场景命名叫Test_01,然后创建一个新的创建叫Tutorial_01,先给buildSetting上添加上去吧。
这些东西在其它场景也得用得到,所以我们可以先把他们当预制体放好,在新建的场景打开:‘
’
在新场景Tutorial_01我们先设置好相机吧,我们可以添加一个tk2dCamera,然后像我这样设置:
X和Y坐标无所谓,主要是Z轴为-38.1,Layer设置为UI,后面我们会做一个渲染UI的摄像机hudCamra,所以渲染层级要取消UI的层级
看到这里你是不是很想赶紧把素材箱里的图片素材全部堆进去呢,但别急,我们制作地图的时候可以想一想一个地图大概是什么样子的,想一想我们进入空洞骑士游戏的第一个场景,我们可以先绘制一个大概的模样,那到底要怎么做呢,这里我们可以用tk2dTilemap来实现!
右键创建游戏对象->tk2d->tilemap,你就可以看到这两个东西
点开tilemap,找到settings选项框,如果这里为空你就直接创建一个,
我们再来创建一个tk2dSprite,很简单,一张黑幕的图片:
在这里选择我们刚刚创建的TilemapSpriteCollection
除此之外,我们还要设置好图片的大小啊,地图的大小啊等等:
设置好Terrain的Layer层级设置和Physics Material
看看你的Tile Proteriles是不是和我一样
创建好后点开Paint选项,然后就可以看到我们创建好的Tiles了
同时你也注意到我的Scene场面左上角有类似于unity自带的Tile Palette画板系统,我们选择好palette后就可以点击最左边的画笔开画了:
下面这个就是我画的:
然后就是给每一个Chunk添加好碰撞箱Edge Collider 2D:
添加完成后如下:
-
3.制作地图之堆叠素材
好了。你已经学会了怎么绘制一张图前期该做的事了,首先创建一个原点位置的游戏对象_Scenery,
下面试着画出这样的地图吧:
没办法,这个就是自己调整每一个素材的Transform,这个得根据你有的素材来决定的,但我还是会挑几个可交互的物体来讲解:
首先是萤火虫。添加好tk2dSprite和tk2dSpriteCollection,以及子对象的tk2dSprite的灯光
同时它还需要一个playmakerFSM,只有一个行为,就是Idle:
可破坏的雕塑breakable Pole:
两个particle_rocks的粒子系统如下,这里我们暂时用不到它们
然后是一个被破坏后的头部Tute Pole 4 Top,它需要一个RB2D和Col2D,同时还要注意的是它的层级Corpse,只能和地面的Layer发生碰撞
添加好脚本BreakablePole.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BreakablePole : MonoBehaviour,IHitResponder
{
[SerializeField] private SpriteRenderer spriteRenderer;
[SerializeField] private Sprite brokenSprite;
[SerializeField] private float inertBackgroundThreshold;
[SerializeField] private float inertForegroundThreshold;
[SerializeField] private AudioSource audioSourcePrefab;
[SerializeField] private RandomAudioClipTable hitClip;
[SerializeField] private GameObject slashImpactPrefab;
[SerializeField] private Rigidbody2D top;
protected void Reset()
{
inertBackgroundThreshold = -1f;
inertForegroundThreshold = -1f;
}
protected void Start()
{
float z = transform.position.z;
if(z < inertBackgroundThreshold || z > inertForegroundThreshold)
{
enabled = false;
return;
}
}
public void Hit(HitInstance damageInstance)
{
int cardinalDirection = DirectionUtils.GetCardinalDirection(damageInstance.Direction);
if (cardinalDirection != 2 && cardinalDirection != 0)
{
return;
}
spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b,0f);
Transform transform = Instantiate(slashImpactPrefab).transform;
transform.eulerAngles = new Vector3(0f, 0f, Random.Range(340f, 380f));
Vector3 localScale = transform.localScale;
localScale.x = ((cardinalDirection == 2) ? -1f : 1f);
localScale.y = 1f;
hitClip.SpawnAndPlayOneShot(audioSourcePrefab, base.transform.position);
top.gameObject.SetActive(true);
float num = (cardinalDirection == 2) ? Random.Range(120, 140) : Random.Range(40, 60);
top.transform.localScale = new Vector3(localScale.x, localScale.y, top.transform.localScale.z);
top.velocity = new Vector2(Mathf.Cos(num * 0.017453292f), Mathf.Sin(num * 0.017453292f)) * 5f;
top.transform.Rotate(new Vector3(0f, 0f, num));
base.enabled = false;
}
}
以及一个ScriptableObejct来控制随机播放某个片段的脚本:
using System;
using UnityEngine;
/// <summary>
/// 根据权重weight确定随机播放某些音乐片段的概率
/// </summary>
[CreateAssetMenu(fileName = "RandomAudioClipTable", menuName = "Hollow Knight/Random Audio Clip Table", order = -1000)]
public class RandomAudioClipTable : ScriptableObject
{
[SerializeField] private RandomAudioClipTable.Option[] options;
[SerializeField] private float pitchMin;
[SerializeField] private float pitchMax;
protected void Reset()
{
pitchMax = 1f;
pitchMin = 1f;
}
public AudioClip SelectClip()
{
if (options.Length == 0)
{
return null;
}
if (options.Length == 1)
{
return options[0].Clip;
}
float num = 0f;
for (int i = 0; i < options.Length; i++)
{
RandomAudioClipTable.Option option = options[i];
num += option.Weight;
}
float num2 = UnityEngine.Random.Range(0f, num);
float num3 = 0f;
for (int j = 0; j < options.Length - 1; j++)
{
RandomAudioClipTable.Option option2 = options[j];
num3 += option2.Weight;
if (num2 < num3)
{
return option2.Clip;
}
}
return options[options.Length - 1].Clip;
}
public float SelectPitch()
{
if (Mathf.Approximately(pitchMin, pitchMax))
{
return pitchMax;
}
return UnityEngine.Random.Range(pitchMin, pitchMax);
}
public void PlayOneShotUnsafe(AudioSource audioSource)
{
if (audioSource == null)
{
return;
}
AudioClip audioClip = SelectClip();
if (audioClip == null)
{
return;
}
audioSource.pitch = SelectPitch();
audioSource.PlayOneShot(audioClip);
}
[Serializable]
private struct Option
{
public AudioClip Clip;
[Range(1f, 10f)]
public float Weight;
}
}
using System;
using UnityEngine;
public static class RandomAudioClipTableExtensions
{
public static void PlayOneShot(this RandomAudioClipTable table, AudioSource audioSource)
{
if (table == null)
{
return;
}
table.PlayOneShotUnsafe(audioSource);
}
public static void SpawnAndPlayOneShot(this RandomAudioClipTable table, AudioSource prefab, Vector3 position)
{
if (table == null)
{
return;
}
if (prefab == null)
{
return;
}
AudioClip audioClip = table.SelectClip();
if (audioClip == null)
{
return;
}
//TODO:Object Pool
AudioSource audioSource = GameObject.Instantiate(prefab).GetComponent<AudioSource>();
audioSource.transform.position = position;
audioSource.pitch = table.SelectPitch();
audioSource.volume = 1f;
audioSource.PlayOneShot(audioClip);
}
}
我的设置如下: 然后就可以做成预制体批量生产了
别忘了这些石块也要设置好Collider2D和Layer为Terrain
这些生命水蓝花也可以添加一下动画系统:
还有记得设置好Roof Colliders防止玩家卡在地图内部
制作完地图的大部分以后,我们还有一些工作要完成,首先是设置好玩家的位置让玩家看起来从悬崖上跳下来的:
然后是设置敌人的位置:
再添加一个Audio Source的Wind Player全局音量播放:
我们还需要制作后处理系统,来让场景看起来勃勃生机,导入Unity自带的后处理系统Post Processing来到tk2d
Camera中,我们先随便设置一下:New一个Profile就有了
我建立这个标题的原因是发现我的敌人Buzzer不会正常执行PlaymakerFSM,给我整半天才发现在游戏初期创建的一些脚本有大问题,所以我们来修改一下这些脚本:
首先是LineOfSightDetecotr的层级问题:应该使用LayerMask.GetMask("Terrain")
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LineOfSightDetector : MonoBehaviour
{
[SerializeField] private AlertRange[] alertRanges;
private bool canSeeHero;
public bool CanSeeHero
{
get
{
return canSeeHero;
}
}
protected void Awake()
{
}
protected void Update()
{
bool flag = false;
for (int i = 0; i < alertRanges.Length; i++)
{
AlertRange alertRange = alertRanges[i];
if(!(alertRange == null) && alertRange.IsHeroInRange)
{
flag = true;
}
}
if(alertRanges.Length != 0 && !flag)
{
canSeeHero = false;
return;
}
HeroController instance = HeroController.instance;
if(instance == null)
{
canSeeHero = false;
return;
}
Vector2 vector = transform.position;
Vector2 vector2 = instance.transform.position;
Vector2 vector3 = vector2 - vector;
if (Physics2D.Raycast(vector, vector3.normalized, vector3.magnitude, LayerMask.GetMask("Terrain")))
{
canSeeHero = false;
}
else
{
canSeeHero = true;
}
Debug.DrawLine(vector, vector2, canSeeHero ? Color.green : Color.yellow);
}
}
然后是设置方向的playmaker自定义行为脚本FaceDirection.cs我忘记哪个地方漏了一个负号
using System;
using UnityEngine;
namespace HutongGames.PlayMaker.Actions
{
[ActionCategory("Enemy AI")]
[Tooltip("Object will flip to face the direction it is moving on X Axis.")]
public class FaceDirection : RigidBody2dActionBase
{
[RequiredField]
[CheckForComponent(typeof(Rigidbody2D))]
public FsmOwnerDefault gameObject;
[Tooltip("Does the target's sprite face right?")]
public FsmBool spriteFacesRight;
public bool playNewAnimation;
public FsmString newAnimationClip;
public bool everyFrame;
public bool pauseBetweenTurns; //是否播放转身动画时暂停
public FsmFloat pauseTime;
private FsmGameObject target;
private tk2dSpriteAnimator _sprite;
private float xScale;
private float pauseTimer;
public override void Reset()
{
gameObject = null;
spriteFacesRight = false;
everyFrame = false;
playNewAnimation = false;
newAnimationClip = null;
}
public override void OnEnter()
{
CacheRigidBody2d(Fsm.GetOwnerDefaultTarget(gameObject));
target = Fsm.GetOwnerDefaultTarget(gameObject);
_sprite = target.Value.GetComponent<tk2dSpriteAnimator>();
xScale = target.Value.transform.localScale.x;
if(xScale < 0f)
{
xScale *= -1f;
}
DoFace();
if (!everyFrame)
{
Finish();
}
}
public override void OnUpdate()
{
DoFace();
}
private void DoFace()
{
if (rb2d == null)
return;
Vector2 velocity = rb2d.velocity;
Vector3 localScale = target.Value.transform.localScale;
float x = velocity.x;
if(pauseTimer <= 0f || !pauseBetweenTurns)
{
if(x > 0f)
{
if (spriteFacesRight.Value)
{
if(localScale.x != xScale)
{
pauseTimer = pauseTime.Value;
localScale.x = xScale;
if (playNewAnimation)
{
_sprite.Play(newAnimationClip.Value);
_sprite.PlayFromFrame(0);
}
}
}
else if(localScale.x != -xScale)
{
pauseTimer = pauseTime.Value;
localScale.x = -xScale;
if (playNewAnimation)
{
_sprite.Play(newAnimationClip.Value);
_sprite.PlayFromFrame(0);
}
}
}
else if(x <= 0f)
{
if (spriteFacesRight.Value)
{
if (localScale.x != -xScale)
{
pauseTimer = pauseTime.Value;
localScale.x = -xScale;
if (playNewAnimation)
{
_sprite.Play(newAnimationClip.Value);
_sprite.PlayFromFrame(0);
}
}
}
else if (localScale.x != xScale)
{
pauseTimer = pauseTime.Value;
localScale.x = xScale;
if (playNewAnimation)
{
_sprite.Play(newAnimationClip.Value);
_sprite.PlayFromFrame(0);
}
}
}
}
else
{
pauseTimer -= Time.deltaTime;//开始计时
}