一、游戏介绍
使用Unity3D引擎制作一个射箭游戏。玩家在场景中,可以以第一人称方式在地图上游走,到达特定的射击位可以进行射击,命中静止的或者移动的靶子得到分数。每一次游戏有n次机会。
二、实现
1、游走
使用标准资源库Standard Assets 中的第一人称控制器FPSController,将内置的FirstPersonCharacter设为主摄像机。该控制器支持游走、跳跃。
然后,创建一个脚本Controllers / CameraFollow.cs,挂载到 弓 这个预制件上,使之时刻跟随主摄像机。(弓CrossBow 使用资源包 RyuGiKen)。
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
public Transform cameraTransform; // 要跟随的物体
// public Vector3 offset = new Vector3(0f, 0.5f, -1f); // 相对位置偏移量
public Vector3 offset = new Vector3(0f, -0.4f, 0.2f); // 相对位置偏移量,x,y,z
private Vector3 targetPosition; // 目标位置
private Quaternion targetRotation;// 目标角度
public void Start(){
cameraTransform = Camera.main.transform;
}
private void LateUpdate()
{
if (cameraTransform != null)
{
// 获取相机的旋转角度
targetRotation = cameraTransform.rotation;
// 根据相机旋转角度计算弓的目标位置
targetPosition = cameraTransform.position + (targetRotation * offset);
// 设置弓的位置和旋转
transform.position = targetPosition;
transform.rotation = targetRotation;
}
}
}
创建脚本mainController.cs,挂载到FPSController上,作用是在场景中创建物体 弓 。由于弓自身挂载了CameraFollow,所以一旦在游戏中创建,就会跟随摄像机。
using System.Diagnostics;
using UnityEngine;
public class mainController : MonoBehaviour
{
public GameObject bow;
//public TreeGenController treeGenController;
void Start(){
bow=Instantiate(Resources.Load("Prefabs/myCrossbow")) as GameObject;
UnityEngine.Debug.Log("corssBow gen");
//后面部分介绍
gameObject.AddComponent<ShootScoreController>();
gameObject.AddComponent<TreeGenController>();
}
}
2、射击
首先,使用Animator动画控制 弓 的 蓄力拉弓、保持、射击 的动画变化过程。
其中,Empty、Fill、Shoot均为资源包 RyuGiKen中弓的动画单元。Empty_Fill是 两个动画的混合。
对应的,需要用脚本控制参数的变化进而控制动画。将下面的MouseController挂载到预制件 弓 (CrossBow)上。
using System.Diagnostics;
using UnityEngine;
using System;
public class MouseController : MonoBehaviour
{
public Animator animator;
private float clickStartTime;
public ArrowFactory arrowFactory;
public ShootScoreController shootScoreController;
void Start(){
animator = GetComponent<Animator>();
UnityEngine.Debug.Log(animator);
gameObject.AddComponent<ArrowFactory>();
arrowFactory = gameObject.GetComponent<ArrowFactory>();
shootScoreController = Singleton<ShootScoreController>.Instance;
GameObject place = GameObject.Find("shootPlace");
UnityEngine.Debug.Log(place.transform.position.x);
}
private void Update()
{
// 当鼠标点击时设置触发器为 true
if (Input.GetMouseButtonDown(0))
{
// 设置触发器为 true
animator.SetBool("prepared",true);
UnityEngine.Debug.Log(animator.GetBool("prepared"));
clickStartTime = Time.time;
}
if (Input.GetMouseButton(0))
{
float elapsedTime = Time.time - clickStartTime;
// 将经过的时间映射到0到1的范围
float PowerValue = Mathf.Clamp01(elapsedTime / 1.5f);
// 设置Animator Controller中的浮点数参数
animator.SetFloat("Power", PowerValue);
}
if (Input.GetMouseButtonUp(0))
{
// 将参数A重置为0
//animator.SetFloat("Power", 0f);
}
if (Input.GetMouseButtonDown(1))
{
if(animator.GetFloat("Power") == 0f) return;
if(!InShootPlace()) return;
// 设置触发器为 true
animator.SetTrigger("Fire");
UnityEngine.Debug.Log("shoot");
animator.SetBool("prepared",false);
//箭神,启动!!!!
GameObject arrow = arrowFactory.GetArrow();
UnityEngine.Debug.Log("启动");
//初始化箭的基本参数
// 初始位置
Vector3 offset = new Vector3(0f, 0f, 1.5f);
Transform cameraTransform = Camera.main.transform;
arrow.transform.position = cameraTransform.position + (transform.rotation * offset);
arrow.transform.rotation = cameraTransform.rotation;
// arrow.transform.position = transform.position + (transform.rotation * offset);
// arrow.transform.rotation = transform.rotation;
// 力
float maxShootForce = 1f;
// 获取Power
float power = animator.GetFloat("Power");
float shootForce = power * maxShootForce;
Vector3 shootDirection = arrow.transform.forward;
arrow.GetComponent<Rigidbody>().AddForce(shootForce * shootDirection, ForceMode.Impulse);
arrow.AddComponent<ArrowStop>();
animator.SetFloat("Power", 0f);
}
if (Input.GetKey(KeyCode.O) && Input.GetKey(KeyCode.K))
{
// "O" 和 "K" 同时按下的处理逻辑
UnityEngine.Debug.Log("按下了 O 和 K 键");
shootScoreController.ResetAll();
}
}
public bool InShootPlace(){
GameObject place = GameObject.Find("shootPlace");
// 判断 damnObject 是否存在,并获取其位置
if(place != null){
Vector3 placePosition = place.transform.position;
if(gameObject.transform.position.y>=placePosition.y + 3.5 &&
Mathf.Abs(gameObject.transform.position.x - placePosition.x)<=3 &&
Mathf.Abs(gameObject.transform.position.z - placePosition.z)<=3)
{
shootScoreController.DelCount(0);
return true;
}
}
GameObject placeMoving = GameObject.Find("shootPlaceMoving");
if(placeMoving != null){
Vector3 placeMovingPosition = placeMoving.transform.position;
if(gameObject.transform.position.y>=placeMovingPosition.y + 10 &&
Mathf.Abs(gameObject.transform.position.x - placeMovingPosition.x)<=10 &&
Mathf.Abs(gameObject.transform.position.z - placeMovingPosition.z)<=10)
{
shootScoreController.DelCount(1);
return true;
}
}
return false;
}
}
上面实现了弓的动画,下面要在弓射击时,实现箭的发射。
创建脚本ArrowFactory.cs,(弓箭工厂),生产 箭。
using System.IO;
using System.Diagnostics;
using System.Security.AccessControl;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArrowFactory : MonoBehaviour
{
List<GameObject> arrow_used;
List<GameObject> arrow_free;
// Start is called before the first frame update
void Start()
{
arrow_used = new List<GameObject>();
arrow_free = new List<GameObject>();
}
public GameObject GetArrow() {
GameObject arrow;
if (arrow_free.Count != 0) {
arrow = arrow_free[0];
arrow_free.Remove(arrow);
}
else {
//
arrow = Instantiate(Resources.Load("Prefabs/Arrow")) as GameObject;
}
arrow_used.Add(arrow);
arrow.SetActive(true);
UnityEngine.Debug.Log("generate arrow");
return arrow;
}
}
箭的发射在上面的MouseController中实现。
//箭神,启动!!!!
GameObject arrow = arrowFactory.GetArrow();
UnityEngine.Debug.Log("启动");
//初始化箭的基本参数
// 初始位置
Vector3 offset = new Vector3(0f, 0f, 1.5f);
Transform cameraTransform = Camera.main.transform;
arrow.transform.position = cameraTransform.position + (transform.rotation * offset);
arrow.transform.rotation = cameraTransform.rotation;
// arrow.transform.position = transform.position + (transform.rotation * offset);
// arrow.transform.rotation = transform.rotation;
// 力
float maxShootForce = 1f;
// 获取Power
float power = animator.GetFloat("Power");
float shootForce = power * maxShootForce;
Vector3 shootDirection = arrow.transform.forward;
arrow.GetComponent<Rigidbody>().AddForce(shootForce * shootDirection, ForceMode.Impulse);
arrow.AddComponent<ArrowStop>();
箭在触碰到其他物体时,需停下来。将下面的ArrowStop.cs挂载到 箭 预制件上。
using UnityEngine;
public class ArrowStop : MonoBehaviour
{
private Rigidbody arrowRigidbody;
private ShootScoreController shootScoreController;
private void Start()
{
arrowRigidbody = GetComponent<Rigidbody>();
shootScoreController = Singleton<ShootScoreController>.Instance;
}
private void OnCollisionEnter(Collision collision)
{
// 停止箭的运动
arrowRigidbody.velocity = Vector3.zero;
arrowRigidbody.isKinematic = true;
// 启用或禁用其他需要的组件或脚本
// ...
// if (collision.gameObject.CompareTag("targetMid")
// || collision.gameObject.CompareTag("targetLeft")
// || collision.gameObject.CompareTag("targetRight")){
// UnityEngine.Debug.Log("hit");
// }
if (collision.gameObject.name == "targetMid"
|| collision.gameObject.name == "targetLeft"
|| collision.gameObject.name == "targetRight"){
UnityEngine.Debug.Log("hit");
if(shootScoreController.GetCount(0)>0)
shootScoreController.AddScore(1);
}
if (collision.gameObject.name == "targetMovingUp" || collision.gameObject.name == "targetMovingDown" || collision.gameObject.name == "targetMovingRound") {
UnityEngine.Debug.Log("hitMove");
transform.SetParent(collision.transform);
if(shootScoreController.GetCount(1)>0)
shootScoreController.AddScore(3);
}
}
}
3、分数、次数、射击位
实现碰撞检测,当弓箭命中靶子时,分数增加。同时,每一次发射消耗一次次数。
其中,碰撞检测在ArrowStop.cs
using UnityEngine;
public class ShootScoreController : MonoBehaviour
{
private int score;
private ShootGUI shootGUI;
void Start(){
score = 0;
shootGUI = Singleton<ShootGUI>.Instance;
}
void Update(){
shootGUI.SetScore(score);
}
public void AddScore(int s){
score += s;
}
public int GetCount(int type){
return shootGUI.GetCount(type);
}
public void ResetAll(){
score = 0;
shootGUI.ResetAll();
}
public void DelCount(int type){
shootGUI.DelCount(type);
}
}
射击位:到达射击位处才能射击。在MouseController.cs实现。原理为弓和射击位之间的坐标对比。
4、场景--天空盒、树木
地形、天空 使用 资源 Fantasy Skybox FREE中的地形。树木同样适使用现有资源包。
实现天空随着时间变化:SkyboxController.cs挂载到主摄像机:
using UnityEngine;
using System.Collections;
public class SkyboxController : MonoBehaviour
{
public string skyboxMaterialPath_Sunrise = "SkyBox/FS017_Sunrise";
public string skyboxMaterialPath_Day = "SkyBox/FS000_Day_01"; // Skybox材质的资源路径
public string skyboxMaterialPath_Night = "SkyBox/FS000_Night_02";
public string skyboxMaterialPath_Sunset = "SkyBox/FS002_Sunset_Cubemap";
public string skyboxMaterialPath_Sunset2 = "SkyBox/FS017_Sunset";
public float changeInterval = 7f; // 更换材质的时间间隔
private Material[] skyboxMaterials;
private int currentMaterialIndex = 0;
void Start()
{
// 使用资源路径加载Skybox材质
Material skyboxMaterial_Sunrise = Resources.Load<Material>(skyboxMaterialPath_Sunrise);
Material skyboxMaterial_Day = Resources.Load<Material>(skyboxMaterialPath_Day);
Material skyboxMaterial_Night = Resources.Load<Material>(skyboxMaterialPath_Night);
Material skyboxMaterial_Sunset = Resources.Load<Material>(skyboxMaterialPath_Sunset);
Material skyboxMaterial_Sunset2 = Resources.Load<Material>(skyboxMaterialPath_Sunset2);
skyboxMaterials = new Material[] { skyboxMaterial_Sunrise, skyboxMaterial_Day, skyboxMaterial_Sunset, skyboxMaterial_Sunset2, skyboxMaterial_Night };
for(int i=0;i<skyboxMaterials.Length;i++){
if (skyboxMaterials[i] == null)
{
Debug.LogError("Failed to load Skybox material at path: " + skyboxMaterials[i]);
return;
}
}
StartCoroutine(ChangeSkyboxMaterial());
}
IEnumerator ChangeSkyboxMaterial()
{
while (true)
{
yield return new WaitForSeconds(changeInterval);
currentMaterialIndex++;
if (currentMaterialIndex >= skyboxMaterials.Length)
{
currentMaterialIndex = 0;
}
RenderSettings.skybox = skyboxMaterials[currentMaterialIndex];
}
}
}
地图中随机生成树木:TreeGenController挂载到FPSController
using UnityEngine;
public class TreeGenController : MonoBehaviour
{
private int treeNums = 120;
private Terrain terrain;
private void Start()
{
terrain = Terrain.activeTerrain;
GenerateTrees();
UnityEngine.Debug.Log(terrain.transform.position.x);
UnityEngine.Debug.Log(terrain.terrainData.size.x);
}
private void GenerateTrees(){
for(int i = 0;i<treeNums;i++){
Vector3 randomPosition = GetRandomPosition();
bool isPositionValid = IsPositionValid(randomPosition);
// If the position is not valid, find a new position within the valid area
while (!isPositionValid)
{
randomPosition = GetRandomPosition();
isPositionValid = IsPositionValid(randomPosition);
}
Instantiate(Resources.Load("Prefabs/Tree9_2"), randomPosition, Quaternion.identity);
}
for(int i = 0;i<treeNums;i++){
Vector3 randomPosition = GetRandomPosition();
bool isPositionValid = IsPositionValid(randomPosition);
// If the position is not valid, find a new position within the valid area
while (!isPositionValid)
{
randomPosition = GetRandomPosition();
isPositionValid = IsPositionValid(randomPosition);
}
Instantiate(Resources.Load("Prefabs/Tree9_3"), randomPosition, Quaternion.identity);
}
for(int i = 0;i<treeNums;i++){
Vector3 randomPosition = GetRandomPosition();
bool isPositionValid = IsPositionValid(randomPosition);
// If the position is not valid, find a new position within the valid area
while (!isPositionValid)
{
randomPosition = GetRandomPosition();
isPositionValid = IsPositionValid(randomPosition);
}
Instantiate(Resources.Load("Prefabs/Tree9_2"), randomPosition, Quaternion.identity);
}
}
private Vector3 GetRandomPosition()
{
// Generate a random position within the terrain bounds
float x = Random.Range(terrain.transform.position.x, terrain.transform.position.x + terrain.terrainData.size.x);
float z = Random.Range(terrain.transform.position.z, terrain.transform.position.z + terrain.terrainData.size.z);
float y = terrain.SampleHeight(new Vector3(x, 0, z));
return new Vector3(x, y, z);
}
private bool IsPositionValid(Vector3 position)
{
// Check if the position is within the restricted areas or too close to them
// You can use any method that suits your needs to define and check the restricted areas
// Example: Check distance from restricted areas
// Collider[] colliders = Physics.OverlapSphere(position, minDistanceFromRestrictedAreas);
// foreach (Collider collider in colliders)
// {
// if (collider.CompareTag("RestrictedArea"))
// {
// return false;
// }
// }
return true;
}
}
4、GUI
显示分数、次数、提示、准心,提供函数以实时修改分数等参数
using UnityEngine;
public class ShootGUI : MonoBehaviour
{
private int score;
private int count_static;
private int count_Moving;
void Start(){
score = 0;
count_static = 10;
count_Moving = 10;
}
public void SetScore(int score){
this.score = score;
}
public void DelCount(int type){
if(type == 0){
if(count_static > 0)
count_static-=1;
}
else if(type == 1){
if(count_Moving > 0)
count_Moving-=1;
}
}
public int GetCount(int type){
if(type == 0){
return count_static;
}
else if(type == 1){
return count_Moving;
}
return 0;
}
public void ResetAll(){
score = 0;
count_static = 10;
count_Moving = 10;
}
private void OnGUI()
{
// 获取屏幕中心的位置
Vector3 screenCenter = new Vector3(Screen.width / 2f, Screen.height / 2f, 0f);
// 设置GUI样式
GUIStyle style = new GUIStyle(GUI.skin.label);
style.fontSize = 20;
style.fontStyle = FontStyle.Bold;
style.alignment = TextAnchor.MiddleCenter;
style.normal.textColor = Color.red;
// 在屏幕中心绘制字母 "O"
GUI.Label(new Rect(screenCenter.x - style.fontSize/2, screenCenter.y - style.fontSize/2, style.fontSize, style.fontSize), "o", style);
// 在屏幕左上方绘制分数文本
GUI.Label(new Rect(10f, 10f, 100f, 30f), "Score: " + score.ToString(), style);
GUI.Label(new Rect(10f, 30f, 150f, 30f), "Case_1: " + count_static.ToString(), style);
GUI.Label(new Rect(10f, 50f, 150f, 30f), "Case_2: " + count_Moving.ToString(), style);
// 在屏幕右上方绘制三行文本
style.fontStyle = FontStyle.Normal;
GUI.Label(new Rect(Screen.width - 200f, 10f, 200f, 30f), "左键长按蓄力", style);
GUI.Label(new Rect(Screen.width - 200f, 40f, 200f, 30f), "左键点击取消", style);
GUI.Label(new Rect(Screen.width - 200f, 70f, 200f, 30f), "右键点击射击", style);
GUI.Label(new Rect(Screen.width - 200f, 100f, 200f, 30f), "ok键重置", style);
}
}
三、视频链接:unity小游戏-射箭游戏_单机游戏热门视频