My Street — Simulation of Crowds and City Economy on Customized Street Furniture

Guangwei Ren
8 min readNov 19, 2019

--

by Guangwei Ren, Rui Wang, Shuo Han

THESIS

Nowadays, public space in our city is too crowded with advertisements in order to gain commercial value in short time. The depressive feelings given by enormous and shining billboards actually make pedestrians less willing to wander on the street. While a rational street furniture design is always ignored, which could benefit our city in long term. Although it is no wrong to earn money in commercial furniture, a fantastic layout of different kinds of furniture can actually attract more pedestrians and realize city economic growth. So a new method to encourage the street furniture design is in urgent need.

Commercial Advertisements Are Too Crowded In Our Street
Appropriate Combination of Different Props and Green

However, the design of street furniture is not necessarily exclusive to urban designers or architects.Everyone can also propose their own idea of public space and find better solutions to change a specific portion of street. Our target is to provide such kind of street simulation platform that formulate rational evaluating system and provide great interaction for users that can really change our street from digital screen to real world.

Game Concept
From Simulation To Real World

SYSTEM

Street Economy System

At the beginning of the simulation, there is initial fund for player to decide how to use them. To make more benefits, each decision should be well-considered. Following factors influence the cost and income of the street:

  • +How many times Ads catch attention from pedestrians
  • +How long time Ads catch attention from pedestrians
  • +Retails interactions with pedestrians
  • -Construction of furniture
  • -Daily Maintenance of furniture

Attractiveness System

The attractiveness level of the street is a parameter that directly controls how many people would like to come to the street. It does not only show how welcome your street is, but also exerts huge impact on the street economy system. Obviously, no company is willing to pay a lot for a billboard that no one sees it. Not the number of billboards, but the amount of attention to a billboard can catch generate value.

Many Billboards with Few People VS Few Billboards with Many People

AGENTS

Moving Agents — Pedestrians Navigation Control

The agents of pedestrians are generated randomly outside the boundary of the community. The attractiveness parameter of the street decides how many pedestrians will come to the street. Pedestrians’ attention and stay time, are the two things that can interact with commercial programs that generate value for the street.

Pedestrian Agents System Diagram

Furniture Agents — Types of Furniture and Effects

We divide the furniture into four categories: Vegetation, Commercial, Convenience. Each of them have child types and they have different affects on the street.

Table of Parameters
Investment Mode

INTERACTIONS AND EFFECTS

The combination of different types of furniture is extremely important in this simulation game. Designer should think of a method that catch pedestrians’ attention and lead their behavior to generate more values for the street. For example, making a beautiful lawn to attract pedestrians so they have more chances to interact with the commercial furniture is clever:

Advertisement Itself Doesn’t Attract Attention
A Lawn Can Attract Pedestrians To Have Attention On Ads
The Longer Ads Attract Attention The More Value Generated
The More People Interact With Commercial The More Value Generated

SIMULATION RESULTS

Simulation VS Reality
My Street

CODE SNIPPET

Pedestrian Agents:

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.AI;using UnityEngine.SceneManagement;public class FieldOfView: MonoBehaviour{Vector3 FinalDestination;static public int IncomeBillboard=6,IncomeRetail=2,IncomeTitle=4;private GameObject ViewAsChild;public GameObject AttractedGem;private GameObject MyGem;private bool GeneratedGem=false;public const float AttentionGapTime=2.0f;private float timer;Mesh ViewMesh;MeshRenderer meshRenderer;GameObject MyView;public Material viewmaterial;public float FieldofViewRadius=10.0f;public float FieldofViewAngle=60.0f;public float MeshResolution=10;private Vector3 VisionOffset=new Vector3(0,1.0f,0);public GameObject[] CoinPopUp;public Vector3 DirectionFromAngle(float AngleInDegrees){AngleInDegrees+=transform.eulerAngles.y;return new Vector3(Mathf.Sin(AngleInDegrees*Mathf.Deg2Rad),0,Mathf.Cos(AngleInDegrees*Mathf.Deg2Rad));}public LayerMask TargetMask;public LayerMask ObstacleMask;public List<Transform> VisibleTargets=new List<Transform>();public List<GameObject> StoreVisibleTargets=new List<GameObject>();public Transform AttractionAsTarget;private float[] ChanceOfAttraction={0.4f,0.3f,0.2f,0.1f,0.15f};private float[] StayTime={12.0f,9.0f,6.0f,3.0f,5.0f};private float AttractionTimer=0.0f;private float ThisStayTime;private bool EnableAttraction=false;static public int NETWORTH=2400;static public int NETATTRACTION=40;void FindVisibleTargets(){VisibleTargets.Clear();StoreVisibleTargets.Clear();Collider[] TargetInViewRadius=Physics.OverlapSphere(transform.position+VisionOffset,FieldofViewRadius,TargetMask);for(int i=0;i<TargetInViewRadius.Length;i++){Transform target=TargetInViewRadius[i].transform;Vector3 DirToTarget=(target.position-(transform.position+VisionOffset)).normalized;if(Vector3.Angle(transform.forward,DirToTarget)<FieldofViewAngle/2){float DisToTarget=(target.position-(transform.position+VisionOffset)).magnitude;if(!Physics.Raycast((transform.position+VisionOffset),DirToTarget,DisToTarget,ObstacleMask)){VisibleTargets.Add(target);if(timer>=AttentionGapTime){StoreVisibleTargets.Add(target.gameObject);timer=0;}}}}CalCulateHitValue();//find attractionif(!EnableAttraction){for(int i=0;i<VisibleTargets.Count;i++){if(VisibleTargets[i].tag==”Lawn”){if(Random.Range(0.0f,1.0f)<=ChanceOfAttraction[0]/50){AttractionAsTarget=VisibleTargets[i];this.GetComponent<NavMeshAgent>().destination=AttractionAsTarget.position+new Vector3(Random.Range(-4.0f,4.0f),0,Random.Range(-4.0f,4.0f));ThisStayTime=StayTime[0];EnableAttraction=true;break;}}if(VisibleTargets[i].tag==”Spot”){if(Random.Range(0.0f,1.0f)<=ChanceOfAttraction[1]/50){AttractionAsTarget=VisibleTargets[i];this.GetComponent<NavMeshAgent>().destination=AttractionAsTarget.position+new Vector3(Random.Range(-3.0f,3.0f),0,Random.Range(-3.0f,3.0f));ThisStayTime=StayTime[1];EnableAttraction=true;break;}}if(VisibleTargets[i].tag==”Tree”){if(Random.Range(0.0f,1.0f)<=ChanceOfAttraction[2]/50){AttractionAsTarget=VisibleTargets[i];this.GetComponent<NavMeshAgent>().destination=AttractionAsTarget.position+new Vector3(Random.Range(-1.0f,1.0f),0,Random.Range(-1.0f,1.0f));ThisStayTime=StayTime[2];EnableAttraction=true;break;}}if(VisibleTargets[i].tag==”Shading”){if(Random.Range(0.0f,1.0f)<=ChanceOfAttraction[3]/50){AttractionAsTarget=VisibleTargets[i];this.GetComponent<NavMeshAgent>().destination=AttractionAsTarget.position+new Vector3(Random.Range(-1.0f,1.0f),0,Random.Range(-1.0f,1.0f));ThisStayTime=StayTime[3];EnableAttraction=true;break;}}if(VisibleTargets[i].tag==”Pot”){if(Random.Range(0.0f,1.0f)<=ChanceOfAttraction[4]/50){AttractionAsTarget=VisibleTargets[i];this.GetComponent<NavMeshAgent>().destination=AttractionAsTarget.position+new Vector3(Random.Range(-1.0f,1.0f),0,Random.Range(-1.0f,1.0f));ThisStayTime=StayTime[4];EnableAttraction=true;break;}}}}}void CalCulateHitValue(){for(int i=0;i<StoreVisibleTargets.Count;i++){{Vector3 CoinPopPos=StoreVisibleTargets[i].transform.position+new Vector3(0,6.0f,0);if(StoreVisibleTargets[i].gameObject.tag==”Billboard”){Instantiate(CoinPopUp[2],CoinPopPos,Quaternion.identity);NETWORTH+=IncomeBillboard;}if(StoreVisibleTargets[i].gameObject.tag==”Retail”){Instantiate(CoinPopUp[0],CoinPopPos,Quaternion.identity);NETWORTH+=IncomeRetail;}if(StoreVisibleTargets[i].gameObject.tag==”Title”){Instantiate(CoinPopUp[1],CoinPopPos,Quaternion.identity);NETWORTH+=IncomeTitle;}}}}private void ResetTarget(){EnableAttraction=false;this.GetComponent<NavMeshAgent>().destination=FinalDestination;AttractionTimer=0;Destroy(MyGem);GeneratedGem=false;}void DrawFieldOfView(){int StepCount=Mathf.RoundToInt(FieldofViewAngle*MeshResolution);float StepAngleSize=FieldofViewAngle/StepCount;List<Vector3> HitPoints=new List<Vector3>();for(int i=0;i<=StepCount;i++){float Angle=-FieldofViewAngle/2+StepAngleSize*i;ViewCastInfo newViewCast=ViewCast(Angle);HitPoints.Add(newViewCast.point);}int VertexCount=HitPoints.Count+1;Vector3 [] Vertices= new Vector3[VertexCount];int [] Triangles= new int [(VertexCount-2)*3];Vertices[0]=VisionOffset+this.transform.position;for(int i=0;i<VertexCount-1;i++){Vertices[i+1]=HitPoints[i];if(i<VertexCount-2){Triangles[i*3]=0;Triangles[i*3+1]=i+1;Triangles[i*3+2]=i+2;}}ViewMesh.Clear();ViewMesh.vertices=Vertices;ViewMesh.triangles=Triangles;ViewMesh.RecalculateNormals();MyView.GetComponent<MeshFilter>().mesh=ViewMesh;}ViewCastInfo ViewCast(float GlobalAngle){Vector3 dir=DirectionFromAngle(GlobalAngle);RaycastHit hit;if(Physics.Raycast(transform.position+VisionOffset,dir,out hit, FieldofViewRadius, ObstacleMask)||Physics.Raycast(transform.position+VisionOffset,dir,out hit, FieldofViewRadius, TargetMask))return new ViewCastInfo(true, hit.point,hit.distance,GlobalAngle);elsereturn new ViewCastInfo(false,transform.position+VisionOffset+dir*FieldofViewRadius,FieldofViewRadius,GlobalAngle);}public struct ViewCastInfo{public bool hit;public Vector3 point;public float distance;public float angle;public ViewCastInfo(bool _hit, Vector3 _point, float _distance, float _angle){hit=_hit;point=_point;distance=_distance;angle=_angle;}}public bool AttractionInSight;public bool ValueInSight;private NavMeshAgent nav;private float AddValue;private GameObject ObjectInView;private GameObject TempTarget;private Vector3 TempDestination;void Start(){FinalDestination=this.GetComponent<NavMeshAgent>().destination;ViewMesh=new Mesh();ViewMesh.name=”MyViewMesh”;MyView=Instantiate(new GameObject(),Vector3.zero,Quaternion.identity);MyView.name=”ViewVis”;MyView.layer=15;MyView.gameObject.AddComponent<MeshFilter>();meshRenderer=MyView.gameObject.AddComponent<MeshRenderer>();meshRenderer.material=viewmaterial;MyView.GetComponent<MeshFilter>().mesh=ViewMesh;}private void OnAttraction(){if(EnableAttraction){if(!GeneratedGem){MyGem=Instantiate(AttractedGem,this.transform.position+new Vector3(0,4.0f,0),Quaternion.identity);GeneratedGem=true;}AttractionTimer+=Time.deltaTime;if(AttractionTimer>ThisStayTime||AttractionAsTarget==null){ResetTarget();}}if(GeneratedGem){MyGem.transform.position=this.transform.position+new Vector3(0,4.0f,0);}}private void Update(){FindVisibleTargets();DrawFieldOfView();timer+=Time.deltaTime;OnAttraction();}private void OnDestroy(){Destroy(MyView);}}

UI Events System And Cast To World:

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;using UnityEngine.EventSystems;using UnityEngine.SceneManagement;using System.Linq;using TMPro;public class Inventory: MonoBehaviour{private const float IsoCameraAngle=0.94f;private const int MOUSEOFFSETONX=-42;public GameObject ToBeCreatedTree,ToBeCreatedLawn,TobeCreatedPot,ToBeCreatedBillBoard,ToBeCreatedTitle,ToBeCreatedRetail,ToBeCreatedShading,ToBeCreatedBin,ToBeCreatedSpot;private GameObject DraggingObject;public GameObject AttractionGem;private Vector3 mOffset;private Vector3 TargetPos;private float mZCoord;private Color TempClr;private Button MyButton;bool ValidPos=true;bool dragging=false;private bool ClickedDemolished=false;string TagContainer;Ray ray;RaycastHit hit;string UIthistag;List<RaycastResult> hitObjects=new List<RaycastResult>();public Texture2D cursorTexture_Demolish,cursorTexture_Normal;public CursorMode cursorMode = CursorMode.ForceSoftware;public Vector2 hotSpot = new Vector2(0,0);private int CureentAttract;private int CurrentCost;public int [] Attracts;public int [] Costs;private int [] Incomes={0,0,0,FieldOfView.IncomeBillboard,FieldOfView.IncomeTitle,FieldOfView.IncomeRetail,0,0,0};public Text CostInfo, AttractInfo, IncomeInfo;private string [] UItags={“TreeSlot”,”LawnSlot”,”PotSlot”,”BillboardSlot”,”TitleSlot”,”RetailSlot”,”ShadingSlot”,”BinSlot”,”SpotSlot”};private string [] GameObjTags={“Tree”,”Lawn”,”Pot”,”Billboard”,”Title”,”Retail”,”Shading”,”Bin”,”Spot”};private int TOTALOFFURNITURE=0;private int DAILYCOST=2;private float timerfordailykeep=0.0f;private const float timetocost=4.0f;public TextMeshProUGUI DailyCost;void Start(){Cursor.SetCursor(cursorTexture_Normal, hotSpot, cursorMode);}void Update(){if(FieldOfView.NETWORTH<=0||PedestrianGenerator.PedCounter<=0){SceneManager.LoadScene(“GameOver”, LoadSceneMode.Single);}GetInfo();DailyKeep();DailyCost.text=”-”+(TOTALOFFURNITURE*DAILYCOST+2).ToString();ray = Camera.main.ScreenPointToRay(Input.mousePosition);if(Input.GetMouseButtonDown(0)){if(!ClickedDemolished){if(GetObjectUnderMouse()!=null){if(GetObjectUnderMouse().tag==”Demolished”){ClickedDemolished=true;}if(GetObjectUnderMouse().layer==9){UIthistag=GetObjectUnderMouse().tag;dragging=true;MouseDown();}}}}if(ClickedDemolished){Cursor.SetCursor(cursorTexture_Demolish, hotSpot, cursorMode);if( Input.GetMouseButtonDown(1) ){ClickedDemolished=false;Cursor.SetCursor(cursorTexture_Normal, hotSpot, cursorMode);}DestroyAimObject();}if(dragging){MouseDrag();DraggingObject.GetComponent<Collider>().enabled=false;}if(Input.GetMouseButtonUp(0)){if(DraggingObject!=null){dragging=false;MouseUp();DraggingObject.GetComponent<Collider>().enabled=true;DraggingObject=null;}}}private void DailyKeep(){timerfordailykeep+=Time.deltaTime;if(timerfordailykeep>=timetocost){timerfordailykeep=0.0f;FieldOfView.NETWORTH-=(TOTALOFFURNITURE*DAILYCOST+2);}}private void GetInfo(){if(GetObjectUnderMouse()!=null){if(!ClickedDemolished){if(GetObjectUnderMouse().layer==9){for(int i=0;i<9;i++){if(GetObjectUnderMouse().tag==UItags[i]){CostInfo.text=Costs[i].ToString();AttractInfo.text=Attracts[i].ToString();IncomeInfo.text=Incomes[i].ToString();}}}else{CleanInfo();}}}else{CleanInfo();}if(ClickedDemolished){Ray ray = Camera.main.ScreenPointToRay( Input.mousePosition );RaycastHit hit;LayerMask layerMask = ~(1 << LayerMask.NameToLayer (“Road”) );if(Physics.Raycast( ray, out hit, Mathf.Infinity,layerMask ) ){if(hit.transform.gameObject.layer==10){for(int i=0;i<9;i++){if(hit.transform.gameObject.tag==GameObjTags[i]){CostInfo.text=(-Costs[i]/2).ToString();AttractInfo.text=(-Attracts[i]).ToString();IncomeInfo.text=”0";}}}else{CleanInfo();}}}}private void CleanInfo(){CostInfo.text=null;AttractInfo.text=null;IncomeInfo.text=null;}private GameObject GetObjectUnderMouse(){var pointer = new PointerEventData(EventSystem.current);pointer.position=Input.mousePosition;EventSystem.current.RaycastAll(pointer,hitObjects);if(hitObjects.Count<=0) return null;else return hitObjects.First().gameObject;}private void DestroyAimObject(){if( Input.GetMouseButtonDown(0) ){Ray ray = Camera.main.ScreenPointToRay( Input.mousePosition );RaycastHit hit;LayerMask layerMask = ~(1 << LayerMask.NameToLayer (“Road”) );GameObject Hitted=null;if( Physics.Raycast( ray, out hit, Mathf.Infinity,layerMask ) ){if(hit.transform.parent!=null)Hitted=hit.transform.parent.gameObject;elseHitted=hit.transform.gameObject;}if(Hitted!=null&&Hitted.layer==10){for(int i=0;i<9;i++){if(Hitted.tag==GameObjTags[i]){FieldOfView.NETATTRACTION-=Attracts[i];FieldOfView.NETWORTH-=Costs[i]/2;}}Destroy(Hitted);}}}private void MouseDrag(){TargetPos=GetWorldMousePos()+mOffset;TargetPos.y=0;DraggingObject.transform.position=TargetPos;}private void MouseUp(){//if…destory else change global para.//out of screen viewif(Input.mousePosition.x<=0||Input.mousePosition.x>=1920||Input.mousePosition.y<=300||Input.mousePosition.y>=1080){ValidPos=false;}//mouse on building or on roadif(Physics.Raycast(ray, out hit)){if(hit.collider.tag==”Building”||hit.collider.tag==”Road”||hit.collider.tag==”Car”)ValidPos=false;}//agent collider on building or on roadif(ReturnCollision.ConflictCollision){ValidPos=false;}if(FieldOfView.NETWORTH+CurrentCost<=0){ValidPos=false;}//destroy unvalidif(!ValidPos)Destroy(DraggingObject);//on succeed construction, calculate resultelse{if(DraggingObject.tag==”Shading”||DraggingObject.tag==”Lawn”||DraggingObject.tag==”Tree”||DraggingObject.tag==”Pot”||DraggingObject.tag==”Bin”||DraggingObject.tag==”Spot”){Instantiate(AttractionGem,DraggingObject.transform.position+new Vector3(0,10.0f,0),Quaternion.identity);}FieldOfView.NETATTRACTION+=CureentAttract;FieldOfView.NETWORTH+=CurrentCost;TOTALOFFURNITURE+=1;}}private void MouseDown(){ValidPos=true;TargetPos=GetWorldMousePos()+mOffset;TargetPos.y=0;if(UIthistag==”TreeSlot”){DraggingObject=Instantiate(ToBeCreatedTree,TargetPos,new Quaternion());CurrentCost=Costs[0];CureentAttract=Attracts[0];}if(UIthistag==”LawnSlot”){DraggingObject=Instantiate(ToBeCreatedLawn,TargetPos,new Quaternion());CurrentCost=Costs[1];CureentAttract=Attracts[1];}if(UIthistag==”PotSlot”){DraggingObject=Instantiate(TobeCreatedPot,TargetPos,new Quaternion());CurrentCost=Costs[2];CureentAttract=Attracts[2];}if(UIthistag==”BillboardSlot”){DraggingObject=Instantiate(ToBeCreatedBillBoard,TargetPos,new Quaternion());CurrentCost=Costs[3];CureentAttract=Attracts[3];}if(UIthistag==”TitleSlot”){DraggingObject=Instantiate(ToBeCreatedTitle,TargetPos,new Quaternion());DraggingObject.transform.Rotate(0,90f,0,Space.World);CurrentCost=Costs[4];CureentAttract=Attracts[4];}if(UIthistag==”RetailSlot”){DraggingObject=Instantiate(ToBeCreatedRetail,TargetPos,new Quaternion());CurrentCost=Costs[5];CureentAttract=Attracts[5];}if(UIthistag==”ShadingSlot”){DraggingObject=Instantiate(ToBeCreatedShading,TargetPos,new Quaternion());CurrentCost=Costs[6];CureentAttract=Attracts[6];}if(UIthistag==”BinSlot”){DraggingObject=Instantiate(ToBeCreatedBin,TargetPos,new Quaternion());CurrentCost=Costs[7];CureentAttract=Attracts[7];}if(UIthistag==”SpotSlot”){DraggingObject=Instantiate(ToBeCreatedSpot,TargetPos,new Quaternion());CurrentCost=Costs[8];CureentAttract=Attracts[8];}TagContainer=DraggingObject.tag;}private Vector3 GetWorldMousePos(){Vector3 MousePoint= Input.mousePosition;MousePoint.y/=Mathf.Cos(IsoCameraAngle);mOffset=new Vector3(MOUSEOFFSETONX,0,0);return Camera.main.ScreenToWorldPoint(MousePoint);}}

--

--