i wanna be the shy
这是一款横版跳跃过关游戏
游戏规则:←→移动,shift/space跳跃,可供跳跃次数为2,射击Z,存档方法为射击存档点,R重新开始游戏,P重新回到当前场景起点开始游戏。
游戏实录:







功能实现:
1. 地图编辑以及逻辑:
对于一款跳跃游戏,最基础的便是地图制作,制作好地图逻辑我们才能够进一步实现我们的跳跃功能。所以在unity的学习中,通过瓦片式地图,摄像机size,分辨率大小,地图分层,即可作出一款像素类型的地图。
(size:摄像区域纵向的一半有多少个单位长度。由我的地图举例,现在分辨率为750*570,我要求竖格有19格,那么一半就是9.5格,size函数就填入9.5)

2. 人物逻辑
对于玩家操控的kid,他是拥有一定逻辑的,首先在i wanna游戏中是有重力的,那么我们需要给予kid刚体组件,对其及进行操作,然后还有动画的实现,跳跃,移动,落下的动画切换。对kid的操作(跳跃,射击)也是需要通过函数进行实现的。

我们对于地面判定的实现是通过unity中的射线进行实现的,以kid当前坐标生成两条线,贴近刚体两端,只要线能够碰到我们的地板层,那么我们此时的动画就应该是原地动画(除非有移动动画覆盖)。
private bool IsFloored()//地面判断函数
{
Vector3 leftVec = transform.position;//左侧射线探测
leftVec.x = transform.position.x-0.25f;
Debug.DrawRay(leftVec, Vector2.down, Color.green);//射线检测功能,从start起始位置绘制一条color颜色的线,持续时间为duration,如果为0线会出现一帧,方向为2D的下方
RaycastHit2D lefthit = Physics2D.Raycast(leftVec, Vector2.down, m_FloorCheckDistance, m_floorLayer);
Vector3 rightVec = transform.position;//右侧射线探测
rightVec.x = transform.position.x + 0.25f;
Debug.DrawRay(rightVec, Vector2.down, Color.green);//射线检测功能,从start起始位置绘制一条color颜色的线,持续时间为duration,如果为0线会出现一帧,方向为2D的下方
RaycastHit2D righthit = Physics2D.Raycast(rightVec, Vector2.down, m_FloorCheckDistance, m_floorLayer);
if (lefthit.collider!=null|| righthit.collider != null)
{
m_jumpTimes = 0;
return true;
}
return false;}
对于基础的移动,首先要解决的就是人物朝向问题,这里我对x*-1即可翻转kid。
private void Flip()//水平翻转函数,人物面对方向
{
m_FacingRight = !m_FacingRight;
Vector3 scale = transform.localScale;
scale.x *= -1;
transform.localScale = scale;
}
然后再写运动函数
private void Move(float h)//运动函数
{
m_isWall = IsWalled(m_FacingRight?1:-1);//如果是1,那就true,如果是-1,那就false
if (m_FacingRight)
{
if (h < 0)
{
Flip();
}
else if (m_isWall)//改变动画为跑步动画
{
m_anim.SetBool("run", false);
return;
}
}
else
{
if (h > 0)
{
Flip();
}
else if (m_isWall)//改变动画为跑步动画
{
m_anim.SetBool("run", false);
return;
}
}
Vector2 v = m_body.velocity;
v.x = h * m_Speed;
m_body.velocity = v;//赋值返回
m_anim.SetBool("run", !(h == 0));//意思即为,h=0时,那么就说明我没在跑,h!=0时,说明我在跑 }
```
动画
我们也同时需要一套完整的动画逻辑来进行更改,比如我在任何时候都可以起跳。先在unity中插入动画并写好我们的相关逻辑。

然后在代码中进行动画的捕获即可操作。
private void Awake()
{
m_audio = gameObject.GetComponent<SoundManager>();
m_anim = GetComponent<Animator>();
m_body = GetComponent<Rigidbody2D>();
m_render = GetComponent<Renderer>();
}
跳跃
既然是一款横版跳跃过关游戏,那么跳跃的实现是必不可少的,对于原版游戏的还原,这里我做了二段跳以及大跳的实现。
public float m_CanJumpTime = 0.2f;//跳跃时间限定(大跳)
private float m_JumpTimer;//跳跃时间
private bool m_isJumping;//判断跳跃
void jump()
{
if(Input.GetButtonDown("Jump"))
{
if (m_onFloor)
{
//fjump_audio.Play();
SoundManager.instance.fJumpAudio();
m_jumpTimes = 1;
m_isJumping = true;//跳跃中
m_JumpTimer = 0f;//跳跃时间
m_onFloor = false;
m_vec.x = m_body.velocity.x;
m_body.velocity = m_vec;//固定跳跃速度
}
else if (m_jumpTimes == 1)
{
//sjump_audio.Play();
SoundManager.instance.sJumpAudio();
m_jumpTimes = 2;
m_isJumping = true;
m_JumpTimer = 0f;
m_onFloor = false;
m_vec.x = m_body.velocity.x;
m_body.velocity = m_vec;//固定跳跃速度
}
else if (m_jumpTimes == 0 && !m_onFloor)
{
//sjump_audio.Play();
SoundManager.instance.sJumpAudio();
m_jumpTimes = 2;
m_isJumping = true;
m_JumpTimer = 0f;
m_onFloor = false;
m_vec.x = m_body.velocity.x;
m_body.velocity = m_vec;//固定跳跃速度
}
}
}
射击
射击的操作就比较简单了,不过我们要先在unity中写好一个子弹的预制体并赋予代码伤害。

public int bullet_damage = 10;
private void OnTriggerEnter2D(Collider2D collision)//子弹伤害
{
//if(collision.gameObject.layer)
collision.SendMessage("BeShoot", bullet_damage, SendMessageOptions.DontRequireReceiver);//函数名,输出值,是否需要接收者
Destroy(gameObject);
}
然后在人物函数中写出shoot函数。
if(Input.GetButtonDown("shoot"))
{
GameObject obj = Instantiate(pfb_bullet, transform.position, Quaternion.identity);
obj.GetComponent<Rigidbody2D>().velocity = m_FacingRight ? bulletSpeed : -1 * bulletSpeed;
}
在能收到伤害的对象上写好beshoot函数即可完成。
3.游戏全局管理
存档点操作其实很简单,重新构建场景,并且在你射击存档点后存入点位数据即可,还记得我们子弹会传的beshoot函数吗?我们将使用beshoot函数进行存档。
void BeShoot(int k)
{
PlayerPrefs.SetFloat("PlayerPosX", transform.position.x);//保存玩家位置
PlayerPrefs.SetFloat("PlayerPosY", transform.position.y);
PlayerPrefs.SetFloat("PlayerPosZ", transform.position.z);
PlayerPrefs.SetFloat("CameraPosX", Camera.main.transform.position.x);//保存摄像机位置
PlayerPrefs.SetFloat("CameraPosY", Camera.main.transform.position.y);
PlayerPrefs.SetFloat("CameraPosZ", Camera.main.transform.position.z);
m_spriteRenderer.sprite = m_save_success;//动画变化(红色变成绿色的存档画面);
}
然后我们的存档功能就在全局中进行实现。
if(Input.GetButtonDown("return"))
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);//重新加载画面,也就是游戏重开
if(m_player == null)//按下后停下死亡音乐,然后继续开始播放刚刚的背景音乐
{
SoundManager.instance.stopDeathAudio();
SoundManager.instance.StartLevelAudio();
}
}
在一个场景中我们初始时应该处于什么位置呢,这是一个需要进行记录的事情,所以我们将通过在每个场景存一个出生点位确认出生位置
public SpriteRenderer wherePlayer;//每个场景玩家应该出现的位置
public GameObject m_player;//玩家位置,便于存档点的存储与读取
void Start()
{
Vector3 vec=Vector3.zero;
vec.x = PlayerPrefs.GetFloat("CameraPosX", -0.5f);
vec.y = PlayerPrefs.GetFloat("CameraPosY", 0.5f);
vec.z = PlayerPrefs.GetFloat("CameraPosZ", -10);
Camera.main.transform.position = vec;//固定摄像机位置
vec.x = PlayerPrefs.GetFloat("PlayerPosX", wherePlayer.transform.position.x);
vec.y = PlayerPrefs.GetFloat("PlayerPosY", wherePlayer.transform.position.y);
vec.z = PlayerPrefs.GetFloat("PlayerPosZ", 0);
m_player.transform.position = vec;//固定每个场景开始时出现位置
}
关卡切换在unity中十分的简单,只需要切换下一场景即可,那么我们给传送物件写一个场景切换。
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Player")//tag词条
{
PlayerPrefs.DeleteAll();//删除所有点位数据,进入下一场景时直接为该场景默认出生位置
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
}
音效管理
对于音效,我希望背景音乐是不会被删除的,只是暂停和继续播放,但是场景一旦重载就包括音乐一起删除了,可是如果不删除又会导致出现多个音乐一起播放,十分的吵闹,所以我们一开始就预制好一个全局的音乐播放类,并且使用单例输出,保护它不被删除的情况下又不新增多个音乐
public static SoundManager instance;//提前加载好,以便外界直接调用函数
public AudioSource AudioSource;//音效
public AudioSource backaudio;//背景音乐
[SerializeField]
private AudioClip fjumpAudio, sjumpAudio, deathAudio, backAudio,bossAudio;//私有数据但能被unity赋值
private void Awake()
{
if (instance!=null && instance != this)
{
Destroy(this.gameObject);//避免多个不可删除的该组件出现,也就是单个个例
return;
}
else
{
instance = this;
}
DontDestroyOnLoad(gameObject);//不允许删除,因为我需要保持音乐进度,切换场景,重新开始游戏都需要保存音乐
backaudio = gameObject.AddComponent<AudioSource>();//获取组件
AudioSource = gameObject.AddComponent<AudioSource>();
backaudio.clip = backAudio;
/*GameObject[] objs = GameObject.FindGameObjectsWithTag("music");
if (objs.Length > 1)
{
Destroy(this.gameObject);
}*/
instance.AudioSource.volume = 0.7f;//音量大小
instance.backaudio.volume = 0.5f;
instance.backaudio.loop = true;//背景音乐循环播放
StartLevelAudio();//开始时自动播放音乐
}
陷阱处理
陷阱就十分简单了,制作一个触发器,直接给伤害物体赋予予一个向量和速度就可以实现。
public Rigidbody2D m_thorn;
public float speed = 1400f;
private void OnTriggerEnter2D(Collider2D other)
{
//刺的向上移动
if (other.tag == "Player")
{
print("success");
Vector2 v = new Vector2(0, speed);
m_thorn.velocity = v * Time.deltaTime;
Destroy(gameObject);
}
}
4.BOSS制作
弹幕
弹幕的攻击方式在这款游戏中我大体只实现了两种。
1.随机弹幕
void apple()//右侧apple随机出现
{
bulletSpeed = new Vector2(10, 0);
float y = Random.Range(-10.5f, 10.5f);
Vector3 pos = new Vector3(11, y, transform.position.z);//随机位置的生成预制体
GameObject obj = Instantiate(pfb_bullet, pos, Quaternion.identity);
obj.GetComponent<Rigidbody2D>().velocity = -1 * bulletSpeed;//速度方向
}
2.跟踪弹幕
void stalk()//右侧追踪apple
{
if(m_player != null)
{
GameObject obj = Instantiate(pfb_bullet, transform.position, Quaternion.identity);
bulletSpeed = new Vector2(m_player.transform.position.x - obj.transform.position.x, m_player.transform.position.y - obj.transform.position.y);//向量相减得到子弹方向向量
obj.GetComponent<Rigidbody2D>().velocity = bulletSpeed * 0.7f;
}
}
击中特效
与子弹生成相同,不过此时我们不再需要给予向量,只是在boss身上随机生成特效,其实原本想做子弹射在哪儿哪儿生成特效点,但是还没有做出对象池所以读取不了每个子弹的坐标。
void BeShoot(int damage)//被射中后的效果
{
Vector3 blood ;
blood.x = Random.Range(transform.position.x- 0.5f, transform.position.x + 0.5f);
blood.y = Random.Range(transform.position.y - 7f, transform.position.y + 7f);
blood.z = transform.position.z;
Instantiate(pfb_blood, blood, Quaternion.identity); //生成特效
m_HP -= damage;//血量减少
EnemyHP.HealthCurrent = m_HP;//血量条更新1
}
kid受伤闪烁(在boss战中拥有三条命)
首先对于新手玩家肯定是需要无敌时间的,并且需要一定提示告诉玩家我的无敌时间剩余,于是想到了闪烁操作
void BeDamaged(int damage)//受到伤害
{
if(isInvincible==false)
{
m_HP -= damage;
}
if(m_HP<=0)
{
//玩家死亡
Destroy(gameObject);
ui_GameOverImage.SetActive(true);
SoundManager.instance.pauseLevelAudio();
SoundManager.instance.DeathAudio();
//death_audio.Play();
}
BlinkPlayer(blinkSeconds,blinkTime);
}
void BlinkPlayer(int numBlinks,float seconds)//闪烁操作
{
StartCoroutine(DoBlinks(numBlinks,seconds));
}
IEnumerator DoBlinks(int numBlinks, float seconds)//无敌时间
{
isInvincible = true;
for (int i =0; i< numBlinks * 2; i++)
{
m_render.enabled = !m_render.enabled;
yield return new WaitForSeconds(seconds);
}
m_render.enabled = true;
isInvincible = false;
}
github地址:https://github.com/zero2744338955/i-wanna-be-the-shy.git
游戏百度网盘地址:https://pan.baidu.com/s/1_ZpQD4rfiHzYjp1Gjy2ZYA
提取码:z137
总结
未完善的点
- 全屏子弹仅仅允许存在四个,子弹速度稍慢,创建对象池优化性能 (苹果,特效,子弹)
- 二段跳再次修改
( 1 ) 一段跳:两格半多一点,脖子到三格线
( 2 ) 二段跳:差一点点四格半,大概4.45
( 3 ) 跳跃速度:跳跃速度的提升,现在的手感为慢,需要将速度增加
- 弹幕的多样化(圆形散射,弹射,定点爆破等)
- 双人模式
感言
首先要感谢各个学长在这个寒假对我的帮助,博哥的帮助功不可没,还有各个先辈对我无厘头的问题的解答,没有这些帮助我可能需要做出来的时间更久更冗杂。
其次在这里项目中学到了基本unity的操作,了解了瓦片地图,全场景音效单例,刚体组件,特效制作,p图(贴图要改,所以素材还得自己修改),也在空闲时间制作了大概三四个视频,了解了i wanna的基础
再然后对于学长提出的联机作战十分感兴趣,也十分认同身为学计算机的人,不应该讲眼光局限于某一个语言,某一个方向,某一个职位,而是应该旷阔的吸收,毕竟计算机都是互通的,多学一门技术也会让自己的想法更加的天马行空,而游戏制作不正需要的就是创意吗。
最后,是对自己的批评,一是自己过于冒进,技术不成熟就想实习,时间没规划好就报了一堆比赛让自己差点忙不过来,最后导致现在算法算法没学好,游戏游戏没做完,英语英语还落着,身体身体没腹肌(呜呜呜),课程课程自学忙。所以自己以后要讲时间规划好,不能再眼大肚皮小了。
十分感谢各位对我的照顾,能做出东西的自豪感是很不错的,自己也会在未来学习更多然后变成全能fw的嘿嘿
