TileMap使用
sorting layers用于层级排序,越下越靠镜头
Rules Tiles可以更便捷地绘制地图
Tilemap Collider更快地为tiles添加碰撞体
人物设置
2D闯关游戏一般会锁定人物Z轴
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
public class PlayerMoving : MonoBehaviour
{
private Rigidbody2D rb;
private BoxCollider2D coll;
[Header("移动参数")]
public float speed = 8f;
public float crouchSpeedDivisor = 3f;
[Header("跳跃参数")]
public float jumpForce = 6.3f;
public float jumpHoldForce = 1.9f;
public float jumpHoldDuration = 0.1f;
public float crouchJumpBoost = 2.5f;
public float hangingJumpForce = 15f;
float jumpTime;
[Header("状态")]
public bool isCrouch;
public bool isOnGrund;
public bool isJump;
public bool isHeadBlocked;
public bool isHanging;
[Header("环境检测")]
public float footOffset = 0.4f;
public float headClearance = 0.5f;
public float groundDistance = 0.2f;
float playerHeight;//角色高度
public float eyeHeight = 1.5f;//眼睛高度
public float grabDistance = 0.4f;
public float reachOffset = 0.7f;
public LayerMask groundLayer;
public float xVelocity;
//按键设置
bool jumpPressed;
bool jumpHeld;
bool crouchHeld;
bool crouchPressed;
//碰撞体尺寸
Vector2 colliderStandSize;
Vector2 colliderStandOffset;
Vector2 colliderCrouchSize;
Vector2 colliderCrouchOffset;
void Start()
{
rb = GetComponent<Rigidbody2D>();
coll = GetComponent<BoxCollider2D>();
playerHeight = coll.size.y;
colliderStandSize = coll.size;//站立时碰撞体的高度
colliderStandOffset = coll.offset;//站立时碰撞体的位置
colliderCrouchSize = new Vector2(coll.size.x, coll.size.y / 2f);//蹲下高度
colliderCrouchOffset = new Vector2(coll.offset.x, coll.offset.y / 2f);//蹲下位置
}
void Update()
{
if (GameManager.GameOver())
return;
jumpPressed = Input.GetButtonDown("Jump");
jumpHeld = Input.GetButton("Jump");
crouchHeld = Input.GetButton("Crouch");
crouchPressed = Input.GetButtonDown("Crouch");
}
private void FixedUpdate()
{
if (GameManager.GameOver())
return;
PhysicsCheck();
GroundMovement();
MidAirMovement();
}
void PhysicsCheck()//射线检测
{
RaycastHit2D leftCheck = Raycast(new Vector2(-footOffset, 0f), Vector2.down,groundDistance ,groundLayer );//从角色向左下发射了一条射线,判断是否接触到groundLayer
RaycastHit2D rightCheck = Raycast(new Vector2(footOffset, 0f), Vector2.down, groundDistance, groundLayer);
if (leftCheck || rightCheck )
isOnGrund = true;
else isOnGrund = false;//判断是否站立于地面
RaycastHit2D headCheck = Raycast(new Vector2(0f, coll.size.y), Vector2.up, headClearance, groundLayer);
if (headCheck)
isHeadBlocked = true;
else isHeadBlocked = false;
float direction = transform.localScale.x;
Vector2 grabDir = new Vector2(direction, 0f);
RaycastHit2D blockedCheck = Raycast(new Vector2(footOffset * direction,playerHeight ),grabDir ,grabDistance ,groundLayer );//头顶检测
RaycastHit2D wallCheck = Raycast(new Vector2(footOffset * direction, eyeHeight), grabDir, grabDistance, groundLayer);//墙体检测
RaycastHit2D ledgeCheck = Raycast(new Vector2(reachOffset * direction, playerHeight), Vector2.down, grabDistance, groundLayer);//是否能够悬挂检测
if (!isOnGrund && rb.velocity.y < 0f && ledgeCheck && wallCheck && !blockedCheck)//rb.velocity.y<0f代表下落时检测
{
Vector3 pos = transform.position;
pos.x += (wallCheck.distance - 0.05f) * direction;//将人物的水平位置与墙体相贴
pos.y -= ledgeCheck.distance;//将人物的头顶与墙体水平
transform.position = pos;
rb.bodyType = RigidbodyType2D.Static;//让碰撞体不再受物理引擎影响
isHanging = true;
}
}
void GroundMovement()//左右移动
{
if (isHanging)
return;
if (crouchHeld && !isCrouch && isOnGrund)
Crouch();
else if (!crouchHeld && isCrouch && !isHeadBlocked )
StandUp();
else if (!isOnGrund && isCrouch)
StandUp();
xVelocity = Input.GetAxis("Horizontal");
if (isCrouch)
xVelocity /= crouchSpeedDivisor;
rb.velocity = new Vector2(xVelocity * speed, rb.velocity.y);//将新速度赋给rb
FilpDirction();
}
void MidAirMovement()
{
if(isHanging )
{
if(jumpPressed )
{
rb.bodyType = RigidbodyType2D.Dynamic;//表示rb的刚体类型是可以受物理引擎影响的
rb.velocity = new Vector2(rb.velocity.x, hangingJumpForce);//给刚体赋予一个向上的速度
isHanging = false;//取消悬挂
}
if(crouchPressed)//在悬挂时按下蹲下取消悬挂
{
rb.bodyType = RigidbodyType2D.Dynamic;
isHanging = false;
}
}
if(jumpPressed && isOnGrund && !isJump && !isHeadBlocked)
{
if(isCrouch && isOnGrund )
{
StandUp();
rb.AddForce(new Vector2(0f, crouchJumpBoost), ForceMode2D.Impulse);//给一个向上的力,ForceMode2D 是一个枚举,定义了施加力的模式,impulse是指瞬间力
}
isOnGrund = false;
isJump = true;
jumpTime = Time.time + jumpHoldDuration;//计算跳跃时间
rb.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse);
AudioManger.PlayJumpAudio();
}
else if(isJump )
{
if (jumpHeld)
rb.AddForce(new Vector2(0f, jumpHoldForce), ForceMode2D.Impulse);//长按再加一个力
if (jumpTime < Time.time)
isJump = false;//当跳跃时间结束就取消跳跃
}
}
void FilpDirction()//左右人物反向
{
if (xVelocity < 0)
transform.localScale = new Vector3(-1, 1,1);
if (xVelocity>0)
transform.localScale = new Vector3(1, 1,1);
}
void Crouch()//蹲下
{
isCrouch = true;
coll.size = colliderCrouchSize;
coll.offset = colliderCrouchOffset;
}
void StandUp()//站立
{
isCrouch = false;
coll.size = colliderStandSize;
coll.offset = colliderStandOffset;
}
RaycastHit2D Raycast(Vector2 offset,Vector2 rayDiraction,float length,LayerMask layer)//射线检测
{
Vector2 pos = transform.position;//初始位置
RaycastHit2D hit = Physics2D.Raycast(pos + offset, rayDiraction, length, layer);//射线判断是否接触
Color color = hit ? Color.red : Color.green;//如果没碰到为绿色,碰到则为红色
Debug.DrawRay(pos + offset, rayDiraction * length,color);
return hit;
}
}
在实现悬挂功能时运用了射线检测,射线检测详情见:
声音控制
public class AudioManger : MonoBehaviour
{
static AudioManger current;
[Header("环境声音")]
public AudioClip ambientClip;
public AudioClip musicClip;
[Header("FX特效")]
public AudioClip deathFXClip;
public AudioClip orbFXClip;
public AudioClip doorFXClip;
public AudioClip startLevelClip;
public AudioClip winClip;
[Header("Robbie音效")]
public AudioClip[] walkStepClips;
public AudioClip[] crouchStepClips;
public AudioClip jumpClip;
public AudioClip deathClip;
public AudioClip jumpVoiceClip;
public AudioClip deathVoiceClip;
public AudioClip orbVoiceClip;
//为物体添加声音组件
AudioSource ambientSource;
AudioSource musicSource;
AudioSource fxSource;
AudioSource playerSource;
AudioSource voiceSource;
public AudioMixerGroup ambientGroup, musicGroup, FXGroup, playerGroup, voiceGroup;
private void Awake()
{
if (current !=null)
{
Destroy(gameObject);
return;
}
current = this;
DontDestroyOnLoad(gameObject);
ambientSource = gameObject.AddComponent<AudioSource>();//添加组件
musicSource = gameObject.AddComponent<AudioSource>();
fxSource = gameObject.AddComponent<AudioSource>();
playerSource = gameObject.AddComponent<AudioSource>();
voiceSource = gameObject.AddComponent<AudioSource>();
ambientSource.outputAudioMixerGroup = ambientGroup;
playerSource.outputAudioMixerGroup = playerGroup;
musicSource.outputAudioMixerGroup = ambientGroup;
fxSource.outputAudioMixerGroup = FXGroup;
voiceSource .outputAudioMixerGroup = voiceGroup;
StartLevelAudio();
}
public static void PlayDoorOpenAudio()//播放开门的声音
{
current.fxSource.clip = current.doorFXClip;
current.fxSource.PlayDelayed (1f);
}
void StartLevelAudio()
{
current.ambientSource.clip = current.ambientClip;
current.ambientSource.loop = true;//背景音循环播放
current.ambientSource.Play();
current.musicSource.clip = current.musicClip;
current.musicSource.loop = true;
current.musicSource.Play();
current.fxSource.clip = current.doorFXClip;
current.fxSource.Play();
}
public static void PlayerWonAudio()
{
current.fxSource.clip = current.winClip;
current.fxSource.Play();
current.playerSource.Stop();
}
public static void PlayFootstepAudio()//播放脚步声音并在animation调用播放
{
int index = Random.Range(0,current.walkStepClips.Length);
current.playerSource.clip = current.walkStepClips[index];
current.playerSource.Play();
}
public static void PlayCrouchstepAudio()
{
int index = Random.Range(0, current.crouchStepClips.Length);
current.playerSource.clip = current.crouchStepClips[index];
current.playerSource.Play();
}
public static void PlayJumpAudio()
{
current.playerSource.clip = current.jumpClip;
current.playerSource.Play();
current.voiceSource.clip = current.jumpVoiceClip;
current.voiceSource.Play();
}
public static void PlayerDeathAudio()
{
current.playerSource.clip = current.jumpClip;
current.playerSource.Play();
current.voiceSource.clip = current.deathVoiceClip;
current.voiceSource.Play();
current.fxSource.clip = current.deathFXClip;
current.fxSource.Play();
}
public static void PlayOrbAudio()
{
current.fxSource.clip = current.orbFXClip;
current.fxSource.Play();
current.voiceSource.clip = current.orbVoiceClip;
current.voiceSource.Play();
}
}
角色动画控制
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAnmation : MonoBehaviour
{
Animator anim;
PlayerMoving moving;
Rigidbody2D rb;
int groundID;
int hangingID;
int crouchID;
int speedID;
int fallID;
void Start()
{
anim = GetComponent<Animator>();
moving = GetComponentInParent<PlayerMoving>();
rb = GetComponentInParent<Rigidbody2D>();
groundID = Animator.StringToHash("isOnGround");
hangingID = Animator.StringToHash("isHanging");
crouchID = Animator.StringToHash("isCrouching");
speedID = Animator.StringToHash("speed");
fallID = Animator.StringToHash("verticalVelocity");
}
void Update()
{
anim.SetFloat(speedID, Mathf.Abs(moving.xVelocity));
anim.SetBool(groundID , moving.isOnGrund);
anim.SetBool(hangingID, moving.isHanging);
anim.SetBool(crouchID, moving.isCrouch);
anim.SetFloat(fallID, rb.velocity.y);
}
public void StepAudio()
{
AudioManger.PlayFootstepAudio();
}
public void CrouchStepAudio()
{
AudioManger.PlayCrouchstepAudio();
}
}
GameManager
死亡机制
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class Playerhealths : MonoBehaviour
{
public GameObject deathVFXPrefab;
int trapsLayer;//陷阱的图层
void Start()
{
trapsLayer = LayerMask.NameToLayer("Traps");
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.layer == trapsLayer)//当触碰到陷阱时
{
Instantiate(deathVFXPrefab, transform.position, Quaternion .Euler (0,0,Random .Range (-45,90)));//将死亡时的特效播放
gameObject.SetActive(false);
AudioManger.PlayerDeathAudio();//播放死亡声音
//SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);死亡后重加载
GameManager.PlayerDied();
}
}
}
道具收集
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Orb : MonoBehaviour
{
int player;
public GameObject explosionVFXPrefab;//特效
void Start()
{
player = LayerMask.NameToLayer("Player");//记录player图层编号
GameManager.RegisterOrb(this);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision .gameObject .layer ==player )
{
Instantiate(explosionVFXPrefab, transform.position, transform.rotation);
gameObject.SetActive(false);
AudioManger.PlayOrbAudio();//播放声音
GameManager.PlayerGrabbedOrb(this);
}
}
}
GameManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{
static GameManager instance;//定义为静态类
SceneFader fader;//开机动画
List<Orb> orbs;//道具
Door lockdoor;
float gameTime;
bool gameIsOver;
public int orbNum;//道具数量
public int deathNum;//死亡次数
private void Awake()
{
if (instance != null)//静态初始化
{
Destroy(gameObject);
return;
}
instance = this;
orbs = new List<Orb>();
DontDestroyOnLoad(this);
}
private void Update()
{
if (gameIsOver)
return;
//orbNum = instance.orbs.Count;
gameTime += Time.deltaTime;
UIManager.UpdateTimeUI(gameTime);
}
public static void RegisterDoor(Door door)
{
instance.lockdoor = door;
}
public static void RegisterSceneFader(SceneFader obj)
{
instance.fader = obj;
}
public static void RegisterOrb(Orb orb)//道具注册
{
if (instance == null)
return;
if (!instance.orbs.Contains(orb))//contains是list自带查找函数
instance.orbs.Add(orb);
UIManager.UpdateOrbUI(instance.orbs.Count);
}
public static void PlayerGrabbedOrb(Orb orb)//捡起道具
{
if (!instance.orbs.Contains(orb))
return;
instance.orbs.Remove(orb);
if (instance.orbs.Count == 0)//当道具捡完后开门
instance.lockdoor .Open();
UIManager.UpdateOrbUI(instance.orbs.Count);//将现在的道具数量实时传到UI上
}
public static void PlayerWon()
{
instance.gameIsOver = true;
UIManager.DisplayGameOver();
AudioManger.PlayerWonAudio();
}
public static bool GameOver()
{
return instance.gameIsOver;
}
public static void PlayerDied()
{
instance.fader.FadeOut();//调用sceneFader中的fadeout函数
instance.deathNum++;
UIManager.UpdateDeathUI(instance.deathNum);//将死亡数量实时传入UI
instance.Invoke("RestartScene", 1.5f);//延迟1.5s后调用RestarScene
}
void RestartScene()//重新加载场景
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
}
UIManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public class UIManager : MonoBehaviour
{
static UIManager instance;
public TextMeshProUGUI orbText, timeText, deathText, gameOverText;//UI Text
private void Awake()
{
if (instance != null)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(this);
}
public static void UpdateOrbUI(int orbCount)//更新道具数量UI
{
instance.orbText.text = orbCount.ToString();
}
public static void UpdateDeathUI(int deathCout)//更新死亡数量UI
{
instance.deathText.text = deathCout.ToString();
}
public static void UpdateTimeUI(float time)//实时时间
{
int minutes = (int)(time / 60);
float seconds = time % 60;
instance.timeText.text = minutes .ToString ("00")+":"+seconds .ToString ("00");
}
public static void DisplayGameOver()//放出gameover的UI
{
instance.gameOverText.enabled = true;
}
}
转场动画
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SceneFader : MonoBehaviour
{
Animator anim;
int faderID;
private void Start()
{
anim = GetComponent<Animator>();
faderID = Animator.StringToHash("Fade");
GameManager.RegisterSceneFader(this);
}
public void FadeOut()
{
anim.SetTrigger(faderID);//设置触发参数
}
}
游戏胜利
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WinZone : MonoBehaviour
{
int playerLayer;
void Start()
{
playerLayer = LayerMask.NameToLayer("Player");
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.layer == playerLayer)
Debug.Log("Player won!");
GameManager.PlayerWon();
}
}
游戏观感优化
安装插件Post Processing
为摄像机添加组件Post Process Layers
再添加一个空项目挂载Post Process Volume
总结
机关主要是通过角色触碰到特定的Layer实现的