Unity 2.Space Shooter(碰撞器Collider,WebGL,刚体中属性,(定时)实例化、销毁游戏对象,触碰OnTriggerEnter/Exit,爆炸效果,音频,文字,定时调方法)

目录

项目介绍

WebGL发布

游戏对象设置

灯光、相机

背景

移动游戏对象

Debug

制作子弹

射击动作

清理离开边界的游戏对象

制作危险物

添加爆炸,移动小行星,作为预制件

创建游戏控制器

循环(有间隙时间)创建Spawning waves

添加音频

显示分数

Debug:

结束游戏循环

Shooter: Enemies, More Hazards, Scrolling BG

Mobile Development

上课笔记

完整工程下载


官方视频翻译

中英双字]Unity官方教程

Space Shooter

http://v.qq.com/vplus/4348d5936e9f34e564caa14b2714e050

https://id.tudou.com/i/UMTY3MTM2OTY1Mg==/videos?order=1&page=3&last_item=&last_pn=2&last_vid=580591452

参考新文字版:https://www.jianshu.com/p/b1240f75aa28

项目介绍

设置基础游戏对象:basic player game object

设置与其他对象的互动:move.shoot.interact

设置场景的相机、灯光、背景(background)

Setup game object,附加预制组件(attach pre-made components)

使用模型(models),artwork

场景设置完成后,C#捕获玩具数据、移动

制作激光器(lazer bolts)陨石(hazard)

游戏控制器(game controller)投掷陨石、计算得分

结束游戏、重新开始时间

添加音频效果(audio effects)、背景音乐(background music track)

在web平台上传build and deploy the game to the web

添加滚动背景scroll the background

添加星域field

WebGL发布

新建项目保存主场景(main scene)、设置基础架构(basic foundations)

https://v.qq.com/x/page/b0752yhy8bn.html

new projects,设置location

导入资料包import package of assets

Web Player在几年前就被弃用了。如果要在浏览器中播放,请使用WebGL选项

安装WebGL时不能更改安装文件夹,只能在与Unity同文件夹下,否则报错failed to locate Unity.exe。安装成功后需要重启Unity

更改target后,会在右侧显示Unity图标,并在顶部显示目标平台

在Build Setting点击Player Setting,或Edit-project Settings-Player。在右侧Inspector设置项目和平台。更改分辨率:Resolution

在Game窗口设置窗口形式(纵横比)

右上角layout-save layout保存编辑器布局

游戏对象设置

添加Ship models(在models-vehicle_playerShip)

让场景对准对象(聚焦场景视图相机):F或Edit-Frame Selected或双击层次目录下的物体

Origin三维场景的中心-reset

组件:游戏对象使用网格过滤器(Mesh Filter)网格渲染器(Mesh Renderer)、材料

使用physics、刚体组件

笼子(Collider碰撞器)定义对象体积:

Physics-Capsule Collider(胶囊碰撞器)由两个球体(与它们之间的空间)定义。计算碰撞占用空间。改变碰撞器方向大小

点击右上角轴,改变视角方向

Capsule Collider(胶囊碰撞器)、Box Collider、Sphere Collider、Mesh Collider(网格碰撞器-性能较低)、Compound Collider复合碰撞器

在原始(primitive)碰撞器无法满足、复合碰撞器无法使用时可用网格碰撞器

对撞机组件不再取代现有对撞机组件。在添加网格碰撞器之后,请手动删除手动胶囊碰撞器组件使用

网格碰撞器的绿线隐藏在渲染网格下方,隐藏renderer可看到,网格碰撞器最好使用简化的网格。其中的Mesh引用该对象的网格(Mesh filter),可用其他网格代替

Update: Please check 'Convex' on the Mesh Collider component, otherwise the Mesh Collider will not participate in physics collisions and will not be visible. 选上Convex,否则网格不可见

打开Model文件(展开),选择Mesh Asset

调整碰撞器设置,is Trigger。检测碰撞触发动作

For Unity 5, we must also select "Convex" on the Mesh Collider, in order for the Mesh Collider to work correctly with the updated Physics Engine in Unity 5.必须选Convex,物理引擎才能使用

添加装置:Prefabs-VFX-发动机

场景在工具栏选择Gizmos,拖动减小装置大小

参见刚体文档

http://docs.unity3d.com/Documentation/Components/class-Rigidbody.html?_ga=2.209081493.667635352.1541845822-2004125961.1541845822

灯光、相机

在层级目录点击相机,可以在Scene视图看到摄像机小窗预览

相机在游戏中通过组件控制

相机类型:Perspective透视orthographic正交投影

透视可以改变视野(Field of View)大小

正交可以改变投影尺寸

相机背景:Clear Flags

默认为skybox(天空)。Solid Color纯色

They skybox can be found in the "Lighting" tab under "Window/Lighting".

场景默认创建directional light

Please delete the GameObject "Directional Light".

设置在Edit-Render Setting或 lighting panel: Window > Lighting : Scene Tab.

Please remove the Skybox by selecting it and deleting it.

Make sure the Skybox is "None".(这样即使选择天空盒,相机看到的背景也是纯色)

光源种类:Ambient Light(没有点光源,没有方向)可改变颜色

key light、fill light、rim light组成的3点光源系统

确认默认自带的Directional Light已经删除

设置在:Directional Lights are now found under: Create/Light/Directional Light

方向(Orientation):reset rotation

改变光强Intensity

使用空游戏对象组织场景:空对象应当在原点(重置)

光源可以照亮整个场景,利用方向而不是光源位置

背景

创建背景图像:Create/3D Object/Quad(四边形)

添加纹理(Textures)点击查看图片导入器,拖动查看preview栏,底部为分辨率等信息

纹理拖拽到quad,自动创建material,quad其中的material引用图片,网格过滤器保存网格数据,网格渲染器只能使用材料中的纹理部分。(拖拽纹理到Mesh上时如果找不到Material,则自动创建)

材料着色器(shader):漫反射(Diffuse)-如同哑光涂料

Standard Shader set to default opaque.默认为不透明着色器

可以使用图层,单独制作不影响其他物品的光源

参见图层教程视频:https://unity3d.com/cn/learn/tutorials/topics/interface-essentials/layers

Specular镜面着色器

可编写自定义着色器

无照亮:Unlit-Texture独立于照明系统,完全与原始图像纹理一致

移动游戏对象

添加脚本组件

access components directly using "GetComponent".

脚本名称应以大写字母开头

物理移动对象:使用FixedUpdate(执行物理计算前调用)

Input.GetAxis获取对象轴的输入值,默认为预设轴,只返回0~1的值

刚体组件包括属性:UseGravity,Mass,Drag

we can pole, affect and manipulate

通过找到刚体组件,改变对象速度v:rigidbody.velocity速度为三维向量

三维向量vector3由xyz方向浮点数构成(y上下,x-Horizontal,z-Vertical)

浮点数表示:0.0f

保证船只在视野范围内:通过限制位置position。允许同句多行,但要保持缩进

文档中脚本参考快捷键不好用

Mathf:使用Epsilon、Pi、infinite等。Clamp钳制变量值在给定范围内

定义class,复用代码。:冒号继承

序列化serialised:存储、传递信息。序列化后可在Inspector中看到属性。

在class声明上方[System.Serializable]

在游戏界面拖动对象位置即可估出边界值

在移动时倾斜对象:XXX.rotation = Quaternion.Euler(x,y,z);

Debug

the shorthand reference "rigidbody" is obsolete. Please use the cached reference or GetComponent<Rigidbody>()

您不能再像过去那样直接访问附加到GameObject的组件。你现在必须使用GetComponent

原版为:rigidbody.xxx。改为声明rigidbodyxxx用Rigidbody rigidbodyxxx。(rigidbody = GetComponent<Rigidbody> ();

且声明不能作为初始化实例变量,应在方法体内部写。

原版为:renderer.XXX;改为使用GetComponent获取渲染器 GetComponent<Renderer>().xxx

//PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class Boundary{
	public float xMin, xMax, zMin, zMax;
}

public class PlayerController : MonoBehaviour {
	public float speed;
	public Boundary boundary;
	public float tilt;
	void FixedUpdate(){
		float moveHorizontal = Input.GetAxis ("Horizontal");
		float moveVertical = Input.GetAxis ("Vertical");
		Vector3 movement = new Vector3 (moveHorizontal,0.0f,moveVertical);
		Rigidbody playerRigidbody=GetComponent<Rigidbody> ();
		playerRigidbody.velocity = movement*speed;

		playerRigidbody.position = new Vector3 (
			Mathf.Clamp(playerRigidbody.position.x,boundary.xMin,boundary.xMax),
			0.0f,
			Mathf.Clamp(playerRigidbody.position.z,boundary.zMin,boundary.zMax)
		);
		playerRigidbody.rotation = Quaternion.Euler(0.0f,0.0f,playerRigidbody.velocity.x*-tilt);
	}
}

制作子弹

通过重用父游戏对象逻辑和视觉效果层,从镜头上分离视觉效果

建立四边形,添加激光纹理(通过新建Material)

关联材料:

As this Shader is now the Standard Shader, there are more Properties and different Properties available.

For the purposes of this lesson, the MainTexture field is now the Albedo field.

在材料属性中选择纹理或拖拽纹理到属性栏

将制作好的材料拖到场景框或对象属性-渲染中的材料框

修改为明亮的着色器:particles-Additive(粒子-添加器)该着色器中,黑色的值为0,不会添加内容。或移动着色器mobile-xxx(通过资源预算提高效率,牺牲质量与改变颜色的能力)

为父容器添加刚体组件

删除quad自带的Mesh Collider网格碰撞器,在父对象中添加胶囊(capsule)碰撞器(由两个半球定义)改变胶囊方向Direction

选择Is Trigger,触发器

激光:生成、具有速度

Start:在对象实例化的第一帧执行

Z轴移动也为前进后退transform.forward

临时创建对象实例

//Mover.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Mover : MonoBehaviour {
	public float speed;
	// Use this for initialization
	void Start () {
		Rigidbody rigidbody = GetComponent<Rigidbody> ();
		rigidbody.velocity = transform.forward*speed;
	}
}

射击动作

实例化Shot预制件的克隆副本

不需要物理计算,放在Update

实例化Instantiate需要对象、设定位置和旋转

位置position、旋转(rotation-euler)包含在transform组件

使用空对象作为实例的产生(attach)点-随着船对象一起移动

变量可以使用变换transform的类型而不是GameObject,仍然可以使用游戏对象作为Inspector中的引用对象。声明变量的类型为transform,也可以找到并使用转换组件

当玩家按下按钮时实例化(fire),限制开火率fireRate(间隔时长)、nextFire(下一次时间点)

实例化基本对象(此时不知道是什么类型的对象),作为游戏对象

GameObject clone=Instantiate(shot, shotSpawn.position, shotSpawn.rotation) as GameObject as GameObject;

实例化对象时,接收对象的引用(实例化函数将返回引用), 否则对象只是进入场景,无法很容易找到,并对它操作。

11:00

Input.GetButton("Fire1")含义:

http://docs.unity3d.com/ScriptReference/Input.html

"Fire1", "Fire2" "Fire3" are mapped to Ctrl, Alt, Cmd keys and three mouse or joystick buttons. New input axes can be added in the Input Manager.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class Boundary{
	public float xMin, xMax, zMin, zMax;
}

public class PlayerController : MonoBehaviour {
	public float speed;
	public Boundary boundary;
	public float tilt;

	public GameObject shot;
	public Transform shotSpawn;
	public float fireRate;
	private float nextFire;//初始即0
	void Update(){
		if (Input.GetButton("Fire1") && Time.time > nextFire)
		{
			nextFire = Time.time + fireRate;
			Instantiate(shot, shotSpawn.position, shotSpawn.rotation);
		}
	}
	void FixedUpdate(){
		float moveHorizontal = Input.GetAxis ("Horizontal");
		float moveVertical = Input.GetAxis ("Vertical");
		Vector3 movement = new Vector3 (moveHorizontal,0.0f,moveVertical);
		Rigidbody playerRigidbody=GetComponent<Rigidbody> ();
		playerRigidbody.velocity = movement*speed;

		playerRigidbody.position = new Vector3 (
			Mathf.Clamp(playerRigidbody.position.x,boundary.xMin,boundary.xMax),
			0.0f,
			Mathf.Clamp(playerRigidbody.position.z,boundary.zMin,boundary.zMax)
		);
		playerRigidbody.rotation = Quaternion.Euler(0.0f,0.0f,playerRigidbody.velocity.x*-tilt);
	}
}

清理离开边界的游戏对象

 “Create/3D object/Cube”

创建Cube,作为边界框,设为触发器

覆盖正交相机视野:scale=Orthographic Size*2

对Trigger Collider编写脚本操作:OnTriggerExit

Destroy(other.gameObject);

删除网格过滤器、渲染器

//DestroyByBoundary.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyByBoundary : MonoBehaviour {

	void OnTriggerExit(Collider other)
	{
		Destroy(other.gameObject);
	}
}

制作危险物

创建父对象Asteroid(流星),生成物品作为子对象。通过移动父对象可以改变星星生成位置

添加第一个陨石models作为子对象,重置transform。

为父对象添加刚体组件,取消重力。添加胶囊碰撞器

在场景视图中改变胶囊碰撞器形状:

 “edit collider” button on the Collider component.

使小行星翻滚:创建脚本RandomRotator

声明public变量tumble(翻滚)

在Start中设置刚体角速度angularVelocity为随机值*tumble

(GetComponent获得刚体组件(GetComponents获得组件数组))

Random.value获得0~1之间的浮点数。InsideUnitSphere返回随机三维向量(Vector3)

刚体模拟了摩擦、空气阻力(旋转会逐渐停止)。->Drag、Angular Drag

设置Angular Drag为0

子弹与小行星都是刚体、有碰撞器,但为触发器,所以没有反应

在Asteroid中添加脚本DestroyByContact(接触),处理触发器触发操作

OnTriggerEnter(与OnTriggerExit相似,但是被调用于其他碰撞器进入此触发器。而不是此碰撞器离开其他触发器的体内)被调用时,其他碰撞器的引用被传入。

如果触发时,销毁此对象(Asteroid)与其他对象。此对象下所有的物品、组件都会同时销毁。在同一帧中,所有需要销毁的对象被同时标记销毁,所以在此代码中顺序没有关系。

此时运行,小行星对象会立即销毁。

Debug:找到什么销毁了小行星--- Debug.Log(other.name);

得到:Boundary

通过tag(other.tag)识别Boundary,忽略它的触碰

//RandomRotator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RandomRotator : MonoBehaviour {
	public float tumble;
	// Use this for initialization
	void Start () {
		Rigidbody playerRigidbody=GetComponent<Rigidbody> ();
		playerRigidbody.angularVelocity = Random.insideUnitSphere*tumble;
	}
}
//DestroyByContact.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyByContact : MonoBehaviour {

	void OnTriggerEnter(Collider other){
		if (other.tag == "Boundary") {
			return;
		}
		Destroy (other.gameObject);
		Destroy (gameObject);
	}
}

添加爆炸,移动小行星,作为预制件

爆炸视觉效果

添加视觉效果的引用,实例化对象在小行星的位置

引用预制件explosion_asteroid

(gameObject与transform为内置对象)

改进玩家船与陨石相撞时,玩家的爆炸效果

玩家标签无需自定义

移动小行星(拖动Mover.cs,速度设为-5)

//DestroyByContact.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyByContact : MonoBehaviour {
	public GameObject explosion;
	public GameObject playerExplosion;
	void OnTriggerEnter(Collider other){
		if (other.tag == "Boundary") {
			return;
		}else if(other.tag == "Player") {
			Instantiate (playerExplosion, other.transform.position, other.transform.rotation);
		}
		Instantiate (explosion, transform.position, transform.rotation);
		Destroy (other.gameObject);
		Destroy (gameObject);
	}
}

创建游戏控制器

控制器产生危险物,显示分数,结束游戏

创建空对象放置控制器

创建GameController脚本

产生危险物:创建危险物引用

一些函数的调用由Unity自动完成,自建函数需要主动调用。

在Start中调用SpawnWave,实例化危险物(position中x为随机值)

Random.Range返回两个值之间的随机值

Quaternion.identity对应没有旋转的四元组

//GameController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameController : MonoBehaviour {
	public GameObject hazard;
	public Vector3 spawnValues;
	void SpawnWaves(){
		Vector3 spawnPosition = new Vector3 (Random.Range(-spawnValues.x,+spawnValues.x),spawnValues.y,spawnValues.z);
		Instantiate (hazard,spawnPosition,Quaternion.identity);
	}
	// Use this for initialization
	void Start () {
		SpawnWaves ();
	}
}

循环(有间隙时间)创建Spawning waves

将代码块放入循环

声明循环最大次数

初始化counter,保存循环条件

声明spawnWait保存循环每次执行等待时间

WaitForSecond(spawnWait);会停止整个游戏

使等待功能成为协程coroutines

将SpawnWave()方法改成协程,返回值IEnumberator(枚举)。

返回方法yield return new WaitForSecond(spawnWait);

调用协程:StartCoroutine (函数名 ());

添加开始游戏时的暂停时间startWait,给用户反应时间

用死循环包裹,声明waveWait,使每一波危险之间等待

定时销毁动态生成的爆炸效果Destroy (gameObject, lifetime);,写在新建脚本DestroyByTime的Start中。在销毁陨石时创建的爆炸效果与陨石同级,无法随陨石一起销毁,也不能(否则可能看不到效果)

在预制件VFX中的所有爆炸效果添加这个脚本

//GameController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameController : MonoBehaviour {
	public GameObject hazard;
	public Vector3 spawnValues;
	public int hazardCount;
	public float spawnWait;
	public float startWait;
	public float waveWait;
	IEnumerator SpawnWaves(){
		yield return new WaitForSeconds (startWait);
		while (true) {
			for (int i = 0; i < hazardCount; i++) {
				Vector3 spawnPosition = new Vector3 (Random.Range (-spawnValues.x, +spawnValues.x), spawnValues.y, spawnValues.z);
				Instantiate (hazard, spawnPosition, Quaternion.identity);
				yield return new WaitForSeconds (spawnWait);
			}
			yield return new WaitForSeconds (waveWait);
		}
	}
	// Use this for initialization
	void Start () {
		StartCoroutine(SpawnWaves ());
	}
}
//DestroyByTime.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyByTime : MonoBehaviour {
	public float lifeTime;
	// Use this for initialization
	void Start () {
		Destroy (gameObject, lifeTime);
	}

}

添加音频

爆炸声,激光枪爆破声

爆炸声,激光枪爆破声

音频组件:剪辑audio clips、音频源(audio sources)、audio Listener

audio clips保存音频文件、数据。

在audio文件夹中有文件(3种爆炸、一个背景音乐,两个武器),打开文件可以查看设置音频导入器信息

确认所有audio clips取消了3D sound的勾选

将音频导入为3D sound时,将比较位置clips的sources,与场景音频监听器比较,使用相应位置影响clips的音量、平移

现在3D音效的设置不在导入音效文件的地方,

而是在AudioSource的SpatialBlend属性,0表示2D,1表示3D

有audio clips、爆炸预制件、玩家船

爆炸被实例化时,玩家船发射武器时,需要播放audio clip

将clips通过使用audio sources组件与游戏对象关联,audio sources播放clips

可以在对象中添加sources引用一个音频,或将clip拖到对象(自动创建并引用)

将两种爆炸音频分别添加到爆炸效果上

确认勾选play on awake,(自动开始播放)

武器音频加入层级视图的player,不自动播放,通过player Controller播放(为什么不加到balt预制件上)

在player Controller的中Update(),创建武器处同时播放关联音频audio.Play()

no longer access components using “audio.” and we must access them directly using "GetComponent".不能直接使用audio组件

AudioSource audioSource = gameObject.GetComponent<AudioSource> ();

audioSource.Play ();

将背景音乐添加到game Controller,设为循环

混合音频,将其中一些效果降低,更改武器和背景音乐Volume为0.5

显示分数

每个危险物击中,奖励10分

为分数创建显示,创建得分值

创建GUI Text,重命名Score Text

To Create a GUIText GameObject in 4.6 and later...

* Create a new Empty GameObject

* Add a GUIText component...

     ... found at: Component/Rendering/GUIText

* Update the GUIText's transform position to (0.5, 0.5, 0.0) to place it in the middle of the screen.

* Update the GUIText.text with "Score Text".

Text属性为显示的内容,其余为位置、形态

Text图层在游戏图层之上,而不在游戏世界,无法在Scene界面看到。使用视口空间(Viewport Space)而不是屏幕空间(Screen Space)。屏幕空间以像素(Pixels)为单位定义,视口空间使用position的x,y 从左下角开始0-1值作为整个屏幕宽高。0.5,0.5为屏幕中间,0,1为左上角

GUItext组件中pixels offset设置像素偏移(10,-10)

使用gameController控制文字

Debug:

GUI Text不显示:

警告:This component is part of the legacy UI system and will be removed in a future release

该组件是遗留的UI系统的一部分,并将在未来的版本中删除

https://blog.csdn.net/lee514/article/details/80909177

https://blog.csdn.net/qq_40133830/article/details/78573690

选中Hierarchy面板中的MainCamera-->Component-->Rendering-->GUI Layer.

尝试后均失败,改用UI-Text

看起来像新版本的教程QUQ:https://www.jianshu.com/p/b1240f75aa28

UIText,不用改旋转,scaler设为Scale With Screen Size后,在游戏视图看,调位置,我text的xy是0,0时为左上角

为了让我们的UI适配我们设定的尺寸,所以我需要选中Canvas来修改UI缩放模式,在本例中,我们使用Scale With Screen Size模式,也就根据屏幕尺寸来缩放,然后将引用的屏幕分辨率设置为600*900,也就是我们的游戏屏幕尺寸

 

有了这个计分UI后,我们需要思考一下,如何才能让它正确显示玩家的分数呢?

 

首先我们得有一个变量来记录玩家的分数

然后我们需要有一个方法,来实现当分数发生变化是通知UI更新分数

接着我们需要在障碍被子弹击碎时增加一定分数

那么我们就来看看如何实现上面的内容,要实现上面内容的第一点和第二点,比较合理的方式是在我的GameController里面来实现,所以我们打开GameController脚本,增加以下代码:

 

首先我们引入了UnityEngine.UI的库文件,因为我们下面会需要在代码中定义Text类型的变量,而Text类型的变量属于UnityEngine.UI的内容,所以我们需要在开头进行引入,这样程序才能识别Text类型的变量

然后我们定义了一个私有变量score,来记录玩家的分数,因为我们不希望这个变量可以手动设置,所以设置为私有类型的

接着我们定义一个公共变量scoreText,用来设置显示分数的UI对象

然后我们在游戏开始时,将score设置为0,接着调用UpdateScore方法来更新分数UI

接着我们来看看UpdateScore方法的具体内容,非常简单,就是修改scoreText的文本内容,文本内容由两部分组成,Score:固定文字加上score变量数值

最后我们定义一个公共的方法addScore,之所以是公共的,因为我会在其他的脚本中调用,所以需要将其设置为public类型,函数里面我们就只有两句话,将现在的分数加上增加的分数,然后再更新分数

写完代码,保存一下,然后回到Unity编辑器,将我们的Score Text对象拖入到scoreText变量中

怎么实现子弹击碎障碍的时候来增加分数呢?我们先回忆一下,子弹击碎障碍的逻辑代码在哪个脚本里面?没错就是DestroyByContact

 

首先我们定义一个公共变量score,来设置每个障碍的击碎后的得分

然后我定义一个私有变量gameController,因为我们需要通过调用GameController脚本中的addScore方法来增加分数

接着我们在Start方法中,需要获取GameController的实例,这里我们的步骤是,首先找到GameController的GameObject,然后在通过GetComponent方法来获取GameController脚本的实例

这里面有两个if语句,第一个if是表示当我们获取的GameController对象不为空时,我才通过GetComponent方法来获取GameController脚本的实例

第二个if是表示当我们获取的GameController对象是空的时候,我们输出Debug信息,告诉我们脚本没有找到

最后我们在障碍被击碎的逻辑里面,调用GameController的addScore方法来增加分数

写完代码,保存一下,

然后回到Unity编辑器,将score设置为10,然后运行游戏,这时你会发现每次你击碎障碍后,分数就会增加10分

//GameController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GameController : MonoBehaviour {
	public GameObject hazard;
	public Vector3 spawnValues;
	public int hazardCount;
	public float spawnWait;
	public float startWait;
	public float waveWait;

	private int score;//私有
	public Text scoreText;
	IEnumerator SpawnWaves(){
		yield return new WaitForSeconds (startWait);
		while (true) {
			for (int i = 0; i < hazardCount; i++) {
				Vector3 spawnPosition = new Vector3 (Random.Range (-spawnValues.x, +spawnValues.x), spawnValues.y, spawnValues.z);
				Instantiate (hazard, spawnPosition, Quaternion.identity);
				yield return new WaitForSeconds (spawnWait);
			}
			yield return new WaitForSeconds (waveWait);
		}
	}
	void UpdateScore (){
		scoreText.text = "Score:" + score;//改的是zu件中的text属性,score可以直接取得
	}
	//addScore方法可使外部脚本diao用私有score属性、UpdateScore方法
	public void addScore(int value){
		score += value;
		UpdateScore ();
	}
	// Use this for initialization
	void Start () {
		score = 0;
		UpdateScore ();
		StartCoroutine(SpawnWaves ());
	}
}
//DestroyByContact.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyByContact : MonoBehaviour {
	public GameObject explosion;
	public GameObject playerExplosion;

	public int addScoreValue;
	private GameController gameController;//GameController方法
	void Start(){
		//先找到Controller的空dui像,再取其中的zu件shi例化
		GameObject gameControllerObject = GameObject.FindWithTag ("GameController");
		gameController= gameControllerObject.GetComponent<GameController> ();
	}
	void OnTriggerEnter(Collider other){
		if (other.tag == "Boundary") {
			return;
		} else if (other.tag == "Player") {
			Instantiate (playerExplosion, other.transform.position, other.transform.rotation);
		} else {
			//不是撞机才加分,若需要可加shoot的tag判断
			gameController.addScore(addScoreValue);
		}
		Instantiate (explosion, transform.position, transform.rotation);
		Destroy (other.gameObject);
		Destroy (gameObject);
	}
}

结束游戏循环

创建空对象DisplayText,保存目前分数,文字

设ScoreText为其子对象

需要两个文字,分布表示游戏结束,和什么时候可以重新开始

在Canvas中创建Restart Text

首先我们还是新建一个Text对象,然后命名为Restart Text,然后修改它的显示内容为GameOver,为了让他更加醒目,我们将他的字体调整为25,然后位置摆放在屏幕的中间,并且修改它的文本对象方式为中心对象,接着将其字体颜色设置为红色,具体属性设置如下图:

然后我们打开GameController脚本来控制重新开始的逻辑:

(以下图片代码不完整,我还添加了GameOver函数,时设置gameOverText文字,gameOver设为true

DestroyByContact中调用该方法。

用单引号括R,表示重新开始标签

GameOvertrue时,break,结束危险物产生循环

Application.LoadLevel加载指定的级别(应用程序加载级别)或场景文件

首先我们定义一个公共变量restartText,来设置显示重新开始信息的UI对象

然后我们定义一个私有变量restart,来标记游戏是否处于重新开始状态

接着我们在游戏开始的时候,将restartText的文本内容设置为空,因为一开始不需要显示任何信息,同时将restart标记为false

然后我们在Update方法中进行检测,如果游戏处于重新开始状态下,并且玩家按下了R键,我们就重新加载当前场景(即重新开始游戏)

最后我们在障碍生成的循环中,进行检测,如果游戏处于GameOver状态,我们就将restart标记为true,同时将restartText的文本内容设置为“Press 'R' to Restart”

写完代码,保存一下,回到Unity编辑器,然后将Restart Text拖入restartText,接着运行游戏,测试一下

//GameController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GameController : MonoBehaviour {
	public GameObject hazard;
	public Vector3 spawnValues;
	public int hazardCount;
	public float spawnWait;
	public float startWait;
	public float waveWait;

	private int score;//私有
	public Text scoreText;
	public Text restartText;
	private bool restart;
	public Text gameOverText;
	private bool gameOver;
	IEnumerator SpawnWaves(){
		yield return new WaitForSeconds (startWait);
		while (true) {
			for (int i = 0; i < hazardCount; i++) {
				Vector3 spawnPosition = new Vector3 (Random.Range (-spawnValues.x, +spawnValues.x), spawnValues.y, spawnValues.z);
				Instantiate (hazard, spawnPosition, Quaternion.identity);
				yield return new WaitForSeconds (spawnWait);
			}
			if (gameOver) {
				restart = true;
				restartText.text = "Press 'R' to Restart";
				break;
			}
			yield return new WaitForSeconds (waveWait);
		}
	}
	void UpdateScore (){
		scoreText.text = "Score:" + score;//改的是zu件中的text属性,score可以直接取得
	}
	//addScore方法可使外部脚本diao用私有score属性、UpdateScore方法
	public void addScore(int value){
		score += value;
		UpdateScore ();
	}
	public void GameOver(){
		gameOver = true;
		gameOverText.text="Game Over !";
	}
	// Use this for initialization
	void Start () {
		gameOverText.text = "";//先不show
		gameOver = false;
		restartText.text = "";//先不show
		restart = false;

		score = 0;
		UpdateScore ();
		StartCoroutine(SpawnWaves ());
	}
	void Update(){
		if (restart) {
			if (Input.GetKeyDown (KeyCode.R)) {
				Application.LoadLevel (Application.loadedLevel);
			}
		}
	}
}
//DestroyByContact.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyByContact : MonoBehaviour {
	public GameObject explosion;
	public GameObject playerExplosion;

	public int addScoreValue;
	private GameController gameController;//GameController方法
	void Start(){
		//先找到Controller的空dui像,再取其中的zu件shi例化
		GameObject gameControllerObject = GameObject.FindWithTag ("GameController");
		gameController= gameControllerObject.GetComponent<GameController> ();
	}
	void OnTriggerEnter(Collider other){
		if (other.tag == "Boundary") {
			return;
		} else if (other.tag == "Player") {
			Instantiate (playerExplosion, other.transform.position, other.transform.rotation);
			gameController.GameOver ();
		} else {
			//不是撞ji才加分,若需要可加shoot的tag判断
			gameController.addScore(addScoreValue);
		}
		Instantiate (explosion, transform.position, transform.rotation);
		Destroy (other.gameObject);
		Destroy (gameObject);
	}
}

放在服务器上,可以在内网中,浏览器中运行

如我现在是http://10.4.15.197:8080/Build/index.html

Shooter: Enemies, More Hazards, Scrolling BG

复制含有陨石模型的空父对象,可以容易地改变模型,而不变其他设置和组件

碰撞器Collider不合适,重新调整

将GameObject中的公共Hazards改成数组:

public GameObject[] hazards;

在实例化前,随机生成:

GameObject hazard = hazards[Random.Range (0, hazards.Length)];

拖拽引用预制件时一起拖拽到Hazards下拉列表范围就可以

 

添加星星背景:背景由两元素组成(scrolling、particle没听清)

在预制件找到startfield,拖动到层级目录

位置设为0,-5(在飞机下方),16

Partical System中属性(open Edit可以打开大窗查看)

确认looping、prewarm循环播放、预先加载

Shape是制造星星的盒子

可以设置星星颜色范围,生命周期长度等

Particle Effect可能是闪烁效果

Renderer中有其Material引用纹理文件夹

循环滚动背景图片:复制Background,作为原Background子对象。单位移动(+v不知道为什么失效了)新背景,接到原背景下(y=-1)

在background添加脚本GBScroller

使用Mathf.Repeat重复position。通过scrollSpeed(-0.25)与tileSizez(30)控制seamlessly

---不能用Vector3.back,否则会出现空地

增大scrollSpeed,与scaley值拉伸可在Boss战中加快速度

 

敌人:创建新空对象Enemy Ship,旋转为y=180。与船模型同级,加上预制件中的engine

为父对象加刚体、碰撞器。设为触发器,加上DestroyByContact脚本与其需要的引用,

为敌人添加脚本WeaponController

建立Fire方法,实例化子弹,播放声音。在Start中通过InvokeRepeating重复、定时调用Fire

新建空子对象ShotSpawn,位置z=-0.5,旋转180(这样子弹发射方向才与玩家相反),放入预制件bolt作为子对象(记得删除原声音), bolt下VFX的material复制一份,修改纹理为浅蓝色纹理,重新引用

为BoltEnemy添加并修改DestroyByContact,将判断Boundary标签改为other.CompareTag(“enemy”)。防止同为敌军被消灭。为explosion添加为空判断,允许无爆炸效果。

将Bolt Enemy添加到预制件,并删除层级目录中的bolt,设置敌船的引用,delay=0.5,firerate=1.5。添加weapon音效时取消勾选play on awake

Enemy ship添加mover(设为-5)

使敌机左右移动:新建脚本EvasiveManeuver,枚举协程方法IEnumerator Evade()

Mathf.Sign(f: float)这个函数将会返回f的正/负值,也就是符号。

获得当前刚体

FixedUpdate更新敌机position,通过Mathf.MoveTowards(参数:现在位置,目标位置,速度)缓缓移动。

速度x为刚刚得出的,z在Start中取得原速度

控制敌机在视野范围(如玩家控制,创建Boundary)

倾斜效果: Quaternion.Euler(0.0f,0.0f,playerRigidbody.velocity.x*-tilt);

boundary边界值x:-6,6;z:-20,20

Dodge:5

Smoothing:7.5

Tilt:10

StartWait:x=0.5;y=1

Maneuver:1,2

将敌机放入预制件,并加入危险物引用数组hazards(多次引用将增加敌机的比例)

在GameController生产危险物时,使用随机掷硬币方法

---不清楚作用:---

---获得生成危险物的引用,设其中旋转y=0,Mover组件的Speed为5(为什么其他陨石仍然旋转)

让敌机主动追踪玩家飞船:创建私有变量寻找玩家Transform

private Transform playerTansform;

在start中赋值:

playerTansform = GameObject.FindGameObjectWithTag ("Player").transform;

改变targetManeuver生成方式

当Player销毁时,transform也销毁了,需要对NULL处理

如果有问题可以在官网Community,Teaching板块,Free Live Training from Unity Technologies提问

在推特上关注

下面是全部完整代码:

//BGScroller.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BGScroller : MonoBehaviour {
	public float scrollSpeed;
	public float tileSizeZ;

	private Vector3 startPosition;

	// Use this for initialization
	void Start () {
		startPosition = transform.position;
	}
	
	// Update is called once per frame
	void Update () {
		float newPosition = Mathf.Repeat (Time.time * scrollSpeed, tileSizeZ);
		transform.position = startPosition + Vector3.forward*newPosition;
	}
}
//DestroyByBoundary.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyByBoundary : MonoBehaviour {

	void OnTriggerExit(Collider other)
	{
		Destroy(other.gameObject);
	}
}
//DestroyByContact.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyByContact : MonoBehaviour {
	public GameObject explosion;
	public GameObject playerExplosion;
	public int addScoreValue;

	private GameController gameController;//GameController方法
	void Start(){
		//先找到Controller的空dui像,再取其中的zu件shi例化
		GameObject gameControllerObject = GameObject.FindWithTag ("GameController");
		gameController= gameControllerObject.GetComponent<GameController> ();
	}
	void OnTriggerEnter(Collider other){
		if (other.CompareTag("Boundary")||other.CompareTag("Enemy")) {
			return;
		} else if (other.tag == "Player") {
			Instantiate (playerExplosion, other.transform.position, other.transform.rotation);
			gameController.GameOver ();
		} else {
			//不是撞ji才加分,若需要可加shoot的tag判断
			gameController.addScore(addScoreValue);
		}
		Instantiate (explosion, transform.position, transform.rotation);
		Destroy (other.gameObject);
		Destroy (gameObject);
	}
}
//DestroyByTime.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyByTime : MonoBehaviour {
	public float lifeTime;
	// Use this for initialization
	void Start () {
		Destroy (gameObject, lifeTime);
	}

}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EvasiveManeuver : MonoBehaviour {
	public Vector2 startWait;
	public Vector2 maneuverTime;
	public Vector2 maneuverWait;
	public float dodge;
	public float smoothing;
	public float tilt;
	public Boundary boundary;

	private float targetManeuver;
	private float speedZ;
	private Rigidbody rb;
	private Transform playerTansform;
	IEnumerator Evade(){
		yield return new WaitForSeconds (Random.Range(startWait.x,startWait.y));
		while (true) {
//			targetManeuver = Random.Range (1,dodge)*-Mathf.Sign(transform.position.x);//随机移
			if(playerTansform!=null){
				targetManeuver = playerTansform.position.x;//追踪
			}
			yield return new WaitForSeconds (Random.Range(maneuverTime.x,maneuverTime.y));
			targetManeuver = 0;
			yield return new WaitForSeconds (Random.Range(maneuverWait.x,maneuverWait.y));
		}
	}
	// Use this for initialization
	void Start () {
		StartCoroutine (Evade());
		rb = GetComponent<Rigidbody> ();
		playerTansform = GameObject.FindGameObjectWithTag ("Player").transform;
		speedZ = rb.velocity.z;//存疑,z=0
//		Debug.Log ("speedZ:"+speedZ);
	}
	void FixedUpdate(){
		float newManeuver = Mathf.MoveTowards (rb.velocity.x, targetManeuver, Time.deltaTime * smoothing);
		rb.velocity = new Vector3 (newManeuver, 0.0f, speedZ);
		rb.position = new Vector3 (
			Mathf.Clamp(rb.position.x,boundary.xMin,boundary.xMax),
			0.0f,
			Mathf.Clamp(rb.position.z,boundary.zMin,boundary.zMax)
		);
		rb.rotation = Quaternion.Euler(0.0f,0.0f,rb.velocity.x*-tilt);
	}
}
//GameController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GameController : MonoBehaviour {
//	public GameObject hazard;
	public GameObject[] hazards;
	public GameObject bigHazard;
	public Vector3 spawnValues;
	public int hazardCount;
	public float spawnWait;
	public float startWait;
	public float waveWait;

	private int score;//私有
	public Text scoreText;
	public Text restartText;
	private bool restart;
	public Text gameOverText;
	private bool gameOver;
	IEnumerator SpawnWaves(){
		yield return new WaitForSeconds (startWait);
		while (true) {
			for (int i = 0; i < hazardCount; i++) {
				GameObject hazard = hazards [Random.Range (0, hazards.Length)];
				Vector3 spawnPosition = new Vector3 (Random.Range (-spawnValues.x, +spawnValues.x), spawnValues.y, spawnValues.z);
				Instantiate (hazard, spawnPosition, Quaternion.identity);
				yield return new WaitForSeconds (spawnWait);
			}
			if (gameOver) {
				restart = true;
				restartText.text = "Press 'R' to Restart";
				break;
			}
			yield return new WaitForSeconds (spawnWait);
			Vector3 bigSpawnPosition = new Vector3(0.0f,0.0f,24.0f);
			Instantiate (bigHazard, bigSpawnPosition, Quaternion.identity);
			yield return new WaitForSeconds (waveWait);
		}
	}
	void UpdateScore (){
		scoreText.text = "Score:" + score;//改的是zu件中的text属性,score可以直接取得
	}
	//addScore方法可使外部脚本diao用私有score属性、UpdateScore方法
	public void addScore(int value){
		score += value;
		UpdateScore ();
	}
	public void GameOver(){
		gameOver = true;
		gameOverText.text="你被我吃掉啦!";
	}
	// Use this for initialization
	void Start () {
		gameOverText.text = "";//先不show
		gameOver = false;
		restartText.text = "";//先不show
		restart = false;

		score = 0;
		UpdateScore ();
		StartCoroutine(SpawnWaves ());
	}
	void Update(){
		if (restart) {
			if (Input.GetKeyDown (KeyCode.R)) {
				Application.LoadLevel (Application.loadedLevel);
			}
		}
	}
}
//Mover.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Mover : MonoBehaviour {
	public float speed;
	// Use this for initialization
	void Start () {
		Rigidbody rigidbody = GetComponent<Rigidbody> ();
		rigidbody.velocity = transform.forward*speed;
	}
}
//PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class Boundary{
	public float xMin, xMax, zMin, zMax;
}

public class PlayerController : MonoBehaviour {
	public float speed;
	public Boundary boundary;
	public float tilt;

	public GameObject shot;
	public Transform shotSpawn;
	public float fireRate;
	private float nextFire;//初始即0
	void Update(){
		if (Input.GetButton("Fire1") && Time.time > nextFire)
		{
			nextFire = Time.time + fireRate;
			Instantiate(shot, shotSpawn.position, shotSpawn.rotation);
			AudioSource audioSource = gameObject.GetComponent<AudioSource> ();
			audioSource.Play ();
		}
	}
	void FixedUpdate(){
		float moveHorizontal = Input.GetAxis ("Horizontal");
		float moveVertical = Input.GetAxis ("Vertical");
		Vector3 movement = new Vector3 (moveHorizontal,0.0f,moveVertical);
		Rigidbody playerRigidbody=GetComponent<Rigidbody> ();
		playerRigidbody.velocity = movement*speed;

		playerRigidbody.position = new Vector3 (
			Mathf.Clamp(playerRigidbody.position.x,boundary.xMin,boundary.xMax),
			0.0f,
			Mathf.Clamp(playerRigidbody.position.z,boundary.zMin,boundary.zMax)
		);
		playerRigidbody.rotation = Quaternion.Euler(0.0f,0.0f,playerRigidbody.velocity.x*-tilt);
	}
}
//RandomRotator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RandomRotator : MonoBehaviour {
	public float tumble;
	// Use this for initialization
	void Start () {
		Rigidbody playerRigidbody=GetComponent<Rigidbody> ();
		playerRigidbody.angularVelocity = Random.insideUnitSphere*tumble;
	}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WeaponController : MonoBehaviour {
	public GameObject shot;
	public Transform shotSpawn;
	public float fireRate;
	public float delay;

	private AudioSource audioSource;

	void Fire(){
		Instantiate (shot,shotSpawn.position,shotSpawn.rotation);
		audioSource.Play ();
	}
	// Use this for initialization
	void Start () {
		audioSource = GetComponent<AudioSource> ();
		InvokeRepeating ("Fire", delay, fireRate);
	}
	
	// Update is called once per frame
	void Update () {
		
	}
}

Mobile Development

To download the "Mobile Artwork" for this session please use the link [HERE].

重新下载手机预制件,修改build target

Input从键盘改成touch,button

语言障碍= =

上课笔记

安装standard Asset

地形

操控树木、植被、水面效果

光源、阴影

角色控制(现成控制器)

音效

内置系统

电子书第6章

Terrain地形

地形光滑

Textures纹理

Windows,lighting setting

引用方式传参:少了值传递中复制的过程,效率更高

找到其他游戏对象(通过名称、标签):Find函数比较耗时,避免在Update中使用,应当初始化时保存

跨脚本访问组件:创建未赋值对象变量,在Inspector赋值

19.6常用脚本API

协同程序19.6.5Coroutine可与主程序并行运行

完整工程下载

https://download.csdn.net/download/lagoon_lala/10795760

以下是自己发挥的修改版,但是视频中提到的小bug也修复了。

下载:https://download.csdn.net/download/lagoon_lala/10796106

修改内容:

尝试在Boss出来时将scrollSpeed从-0.25改成-30

Scale和TileSizeZ均改成60

修改scale时必须创建三维向量,不能单独改其中一个值

调用BGScroll中方法时需要先创建BGSCroll的私有变量,在start()中找到其引用

在游戏开始时,Boss来时,被击毙时分别调用

//BGScroller.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BGScroller : MonoBehaviour {
	public float scrollSpeed;
	public float tileSizeZ;
	public float bossScrollSpeed;
	public float bossTileSizeZ;
	public float bossZ;

	private Vector3 startPosition;
	private float nowScrollSpeed;
	private float nowTileSizeZ;
	public void BossCome(){
		nowScrollSpeed = bossScrollSpeed;
		nowTileSizeZ = bossTileSizeZ;
		transform.localScale =new Vector3(transform.localScale.x,bossZ,transform.localScale.z);
	}
	public void BossLeave(){
		nowScrollSpeed = scrollSpeed;
		nowTileSizeZ = tileSizeZ;
		transform.localScale = new Vector3(transform.localScale.x,30.0f,transform.localScale.z);
	}
	// Use this for initialization
	void Start () {
		startPosition = transform.position;
		BossLeave ();
	}
	
	// Update is called once per frame
	void Update () {
		float newPosition = Mathf.Repeat (Time.time * nowScrollSpeed, nowTileSizeZ);
		transform.position = startPosition + Vector3.forward*newPosition;
	}
}
//GameController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GameController : MonoBehaviour {
//	public GameObject hazard;
	public GameObject[] hazards;
	public GameObject bigHazard;
	public Vector3 spawnValues;
	public int hazardCount;
	public float spawnWait;
	public float startWait;
	public float waveWait;
	public float BossWait;

	private BGScroller bgScroller;
	private int score;//私有
	public Text scoreText;
	public Text restartText;
	private bool restart;
	public Text gameOverText;
	private bool gameOver;
	IEnumerator SpawnWaves(){
		yield return new WaitForSeconds (startWait);
		while (true) {
			for (int i = 0; i < hazardCount; i++) {
				GameObject hazard = hazards [Random.Range (0, hazards.Length)];
				Vector3 spawnPosition = new Vector3 (Random.Range (-spawnValues.x, +spawnValues.x), spawnValues.y, spawnValues.z);
				Instantiate (hazard, spawnPosition, Quaternion.identity);
				yield return new WaitForSeconds (spawnWait);
			}
			if (gameOver) {
				restart = true;
				restartText.text = "Press 'R' to Restart";
				break;
			}
			yield return new WaitForSeconds (spawnWait*BossWait);
			bgScroller.BossCome ();
			Vector3 bigSpawnPosition = new Vector3(0.0f,0.0f,24.0f);
			Instantiate (bigHazard, bigSpawnPosition, Quaternion.identity);
			yield return new WaitForSeconds (waveWait);
		}
	}
	void UpdateScore (){
		scoreText.text = "Score:" + score;//改的是zu件中的text属性,score可以直接取得
	}
	//addScore方法可使外部脚本diao用私有score属性、UpdateScore方法
	public void addScore(int value){
		score += value;
		UpdateScore ();
	}
	public void GameOver(){
		gameOver = true;
		gameOverText.text="你被我吃掉啦!";
		bgScroller.BossLeave ();
	}
	// Use this for initialization
	void Start () {
		gameOverText.text = "";//先不show
		gameOver = false;
		restartText.text = "";//先不show
		restart = false;
		GameObject backgroundObject = GameObject.FindWithTag ("Background");
		bgScroller=backgroundObject.GetComponent<BGScroller> ();
		score = 0;
		UpdateScore ();
		StartCoroutine(SpawnWaves ());
	}
	void Update(){
		if (restart) {
			if (Input.GetKeyDown (KeyCode.R)) {
				Application.LoadLevel (Application.loadedLevel);
			}
		}
	}
}

删除CSDN上传资源方法

http://download.csdn.net/index.php/user_console/del_my_source/123456

最后的号码改成资源号即可

下载教程例子文件位置:

C:\Users\LENOVO\AppData\Roaming\Unity\Asset Store-5.x\Unity Technologies\Sample Projects

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值