UE4学习笔记(4)官方教程代码
本篇文章主要是通过分析UE4虚幻引擎中的官方教程代码,来帮助大家去学习如何使用UE4做开发。
实现平台:win8.1 UE4.10
按教程练习。
1、实现Pawn移动(input)
MyPawn.h
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "GameFramework/Pawn.h" #include "MyPawn.generated.h" UCLASS() class MYPROJECT2_BP_API AMyPawn : public APawn { GENERATED_BODY() public: // Sets default values for this pawn's properties AMyPawn(); // Called when the game starts or when spawned virtual void BeginPlay() override; // Called every frame virtual void Tick( float DeltaSeconds ) override; // Called to bind functionality to input virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override; UPROPERTY(EditAnywhere) USceneComponent* OurVisibleComponent; void MoveForward(float Value); void MoveRight(float Value); void Bigger(); void Smaller(); bool bGrowing; FVector CurrentVelocity; };Mypawn.cpp
// Fill out your copyright notice in the Description page of Project Settings. #include "MyProject2_BP.h" #include "MyPawn.h" // Sets default values AMyPawn::AMyPawn() { // Set this pawn to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; AutoPossessPlayer = EAutoReceiveInput::Player0; RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("MyRootComponent")); UCameraComponent* Mycamera = CreateDefaultSubobject<UCameraComponent>(TEXT("OurCamera")); OurVisibleComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("OurVisibleComponent")); Mycamera->AttachTo(RootComponent); Mycamera->SetRelativeLocation(FVector(-250.0f, 0.0f, 250.0f)); Mycamera->SetRelativeRotation(FRotator(-45.0f, 0, 0)); OurVisibleComponent->AttachTo(RootComponent); } // Called when the game starts or when spawned void AMyPawn::BeginPlay() { Super::BeginPlay(); } // Called every frame void AMyPawn::Tick( float DeltaTime ) { Super::Tick( DeltaTime ); float CurrentScale = OurVisibleComponent->GetComponentScale().X; if (bGrowing) { // 在一秒的时间内增长到两倍的大小 CurrentScale += DeltaTime; } else { // 随着增长收缩到一半 CurrentScale -= (DeltaTime * 0.5f); } // 确认永不低于起始大小,或增大之前的两倍大小。 CurrentScale = FMath::Clamp(CurrentScale, 1.0f, 2.0f); OurVisibleComponent->SetWorldScale3D(FVector(CurrentScale)); // 基于"MoveX"和 "MoveY"坐标轴来处理移动 if (!CurrentVelocity.IsZero()) { FVector NewLocation = GetActorLocation() + (CurrentVelocity * DeltaTime); SetActorLocation(NewLocation); } } // Called to bind functionality to input void AMyPawn::SetupPlayerInputComponent(class UInputComponent* InputComponent) { Super::SetupPlayerInputComponent(InputComponent); InputComponent->BindAction("Bigger", IE_Pressed, this, &AMyPawn::Bigger); InputComponent->BindAction("Smaller", IE_Pressed, this, &AMyPawn::Smaller); InputComponent->BindAxis("MoveForward", this, &AMyPawn::MoveForward); InputComponent->BindAxis("MoveRight", this, &AMyPawn::MoveRight); } void AMyPawn::MoveForward(float Value) { CurrentVelocity.X = Value * 100.0f; } void AMyPawn::MoveRight(float Value) { CurrentVelocity.Y = Value * 100.0f; } void AMyPawn::Bigger() { bGrowing = true; } void AMyPawn::Smaller() { bGrowing = false; }
2、实现相机自动切换(注意需要两个camera)
CameraDirector.h
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "GameFramework/Actor.h" #include "CameraDirector.generated.h" UCLASS() class MYPROJECT2_BP_API ACameraDirector : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties ACameraDirector(); // Called when the game starts or when spawned virtual void BeginPlay() override; // Called every frame virtual void Tick( float DeltaSeconds ) override; UPROPERTY(EditAnywhere) AActor* CameraOne; UPROPERTY(EditAnywhere) AActor* CameraTwo; float TimeToNextCameraChange; };CameraDirector.cpp
// Fill out your copyright notice in the Description page of Project Settings. #include "MyProject2_BP.h" #include "CameraDirector.h" #include "Kismet/GameplayStatics.h" // Sets default values ACameraDirector::ACameraDirector() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; } // Called when the game starts or when spawned void ACameraDirector::BeginPlay() { Super::BeginPlay(); } // Called every frame void ACameraDirector::Tick( float DeltaTime ) { Super::Tick( DeltaTime ); const float TimeBetweenCameraChanges = 2.0f; const float SmoothBlendTime = 0.75; TimeToNextCameraChange -= DeltaTime; if (TimeToNextCameraChange<=0.0f) { TimeToNextCameraChange += TimeBetweenCameraChanges; APlayerController* OurPlayerController = UGameplayStatics::GetPlayerController(this, 0); if (OurPlayerController) { if ((OurPlayerController->GetViewTarget() != CameraOne) && (CameraOne != nullptr)) { // 立即切换到相机1。 OurPlayerController->SetViewTarget(CameraOne); } else if ((OurPlayerController->GetViewTarget() != CameraTwo) && (CameraTwo != nullptr)) { // 平滑地混合到相机2。 OurPlayerController->SetViewTargetWithBlend(CameraTwo, SmoothBlendTime); } } } }
CountDown.h
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "GameFramework/Actor.h" #include "CountDown.generated.h" UCLASS() class MYPROJECT2_BP_API ACountDown : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties ACountDown(); // Called when the game starts or when spawned virtual void BeginPlay() override; // Called every frame virtual void Tick( float DeltaSeconds ) override; UPROPERTY(EditAnywhere, Category = "CountNumber") int32 CountDownTime; UTextRenderComponent* CountDownText; void UpdataTimerDisplay(); void AdvanceTimer(); UFUNCTION(BlueprintNativeEvent, Category = "CountNumber") void CountdownHasFinish(); virtual void CountdownHasFinish_Implementation(); FTimerHandle CountdownTimerhandle; };
CountDown.cpp
// Fill out your copyright notice in the Description page of Project Settings. #include "MyProject2_BP.h" #include "CountDown.h" // Sets default values ACountDown::ACountDown() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; CountDownText = CreateDefaultSubobject<UTextRenderComponent>(TEXT("CountdownNumber")); CountDownText->SetHorizontalAlignment(EHTA_Center); CountDownText->SetWorldSize(150.0f); RootComponent = CountDownText; CountDownTime = 3.0f; } void ACountDown::UpdataTimerDisplay() { CountDownText->SetText((FString::FromInt(FMath::Max(CountDownTime, 0)))); } void ACountDown::AdvanceTimer() { --CountDownTime; UpdataTimerDisplay(); if (CountDownTime<1) { GetWorldTimerManager().ClearTimer(CountdownTimerhandle); CountdownHasFinish(); } } //void ACountDown::CountdownHasFinish() //{ // //CountDownText->SetText(TEXT("GO!")); //} void ACountDown::CountdownHasFinish_Implementation() { //Change to a special readout CountDownText->SetText(TEXT("GO!")); } // Called when the game starts or when spawned void ACountDown::BeginPlay() { Super::BeginPlay(); UpdataTimerDisplay(); GetWorldTimerManager().SetTimer(CountdownTimerhandle, this, &ACountDown::AdvanceTimer, 1.0f, true); } // Called every frame void ACountDown::Tick( float DeltaTime ) { Super::Tick( DeltaTime ); }
4、Pwan与碰撞(粒子特效) 注意碰撞时,按下space才会产生火焰
ColisionPawn.h// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "GameFramework/Pawn.h" #include "ColisionPawn.generated.h" UCLASS() class MYPROJECT2_BP_API AColisionPawn : public APawn { GENERATED_BODY() public: // Sets default values for this pawn's properties AColisionPawn(); // Called when the game starts or when spawned virtual void BeginPlay() override; // Called every frame virtual void Tick( float DeltaSeconds ) override; // Called to bind functionality to input virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override; UParticleSystemComponent* MyParticleSystem; class UColisionPawnMovementComponent* MyMovementComponent; virtual UPawnMovementComponent* GetMovementComponent() const override; void MoveForward(float AxisValue); void MoveRight(float AxisValue); void Turn(float AxisValue); void ParticleToggle(); };
ColisionPawn.cpp
// Fill out your copyright notice in the Description page of Project Settings. #include "MyProject2_BP.h" #include "ColisionPawn.h" #include "ColisionPawnMovementComponent.h" // Sets default values AColisionPawn::AColisionPawn() { // Set this pawn to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; // Our root component will be a sphere that reacts to physics USphereComponent* SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent")); RootComponent = SphereComponent; SphereComponent->InitSphereRadius(40.f); SphereComponent->SetCollisionProfileName(TEXT("Pawn")); // Create and position a mesh component so we can see where our sphere is UStaticMeshComponent* SphereVisual = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("VisualRepresentation")); SphereVisual->AttachTo(SphereComponent); static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere")); if (SphereVisualAsset.Succeeded()) { SphereVisual->SetStaticMesh(SphereVisualAsset.Object); SphereVisual->SetRelativeLocation(FVector(0, 0, 0)); SphereVisual->SetWorldScale3D(FVector(0.8f)); } // Create a particle system that we can activate or deactivate MyParticleSystem = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("MovementParticle")); MyParticleSystem->AttachTo(SphereVisual); MyParticleSystem->bAutoActivate = false; MyParticleSystem->SetRelativeLocation(FVector(-20.0f, 0.0f, 20.0f)); static ConstructorHelpers::FObjectFinder<UParticleSystem> ParticleAsset(TEXT("/Game/StarterContent/Particles/P_Fire.P_Fire")); if (ParticleAsset.Succeeded()) { MyParticleSystem->SetTemplate(ParticleAsset.Object); } // Use a spring arm to give the camera smooth, natural-feeling motion. USpringArmComponent* SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraAttachmentArm")); SpringArm->AttachTo(RootComponent); SpringArm->RelativeRotation = FRotator(-45.f, 0.f, 0.f); SpringArm->TargetArmLength = 400.0f; SpringArm->bEnableCameraLag = true; SpringArm->CameraLagSpeed = 3.0f; // Create a camera and attach to our spring arm UCameraComponent* MyCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("AcrualCamera")); MyCamera->AttachTo(SpringArm, USpringArmComponent::SocketName); // Take control of the default player AutoPossessPlayer = EAutoReceiveInput::Player0; // Create an instance of our movement component, and tell it to update the root. MyMovementComponent = CreateDefaultSubobject<UColisionPawnMovementComponent>(TEXT("CustomMovementComponent")); MyMovementComponent->UpdatedComponent = RootComponent; } UPawnMovementComponent* AColisionPawn::GetMovementComponent() const { return MyMovementComponent; } void AColisionPawn::MoveForward(float AxisValue) { if (MyMovementComponent && (MyMovementComponent->UpdatedComponent == RootComponent)) { MyMovementComponent->AddInputVector(GetActorForwardVector() * AxisValue); } } void AColisionPawn::MoveRight(float AxisValue) { if (MyMovementComponent && (MyMovementComponent->UpdatedComponent == RootComponent)) { MyMovementComponent->AddInputVector(GetActorRightVector() * AxisValue); } } void AColisionPawn::Turn(float AxisValue) { FRotator NewRotation = GetActorRotation(); NewRotation.Yaw += AxisValue; SetActorRotation(NewRotation); } void AColisionPawn::ParticleToggle() { if (MyParticleSystem && MyParticleSystem->Template) { MyParticleSystem->ToggleActive(); } } // Called when the game starts or when spawned void AColisionPawn::BeginPlay() { Super::BeginPlay(); } // Called every frame void AColisionPawn::Tick( float DeltaTime ) { Super::Tick( DeltaTime ); } // Called to bind functionality to input void AColisionPawn::SetupPlayerInputComponent(class UInputComponent* InputComponent) { Super::SetupPlayerInputComponent(InputComponent); InputComponent->BindAction("ParticleToggle", IE_Pressed, this, &AColisionPawn::ParticleToggle); InputComponent->BindAxis("MoveForward", this, &AColisionPawn::MoveForward); InputComponent->BindAxis("MoveRight", this, &AColisionPawn::MoveRight); InputComponent->BindAxis("Turn", this, &AColisionPawn::Turn); }
ColisionPawnMovementComponent.h
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "GameFramework/PawnMovementComponent.h" #include "ColisionPawnMovementComponent.generated.h" /** * */ UCLASS() class MYPROJECT2_BP_API UColisionPawnMovementComponent : public UPawnMovementComponent { GENERATED_BODY() public: virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction); };ColisionPawnMovementComponent.cpp
// Fill out your copyright notice in the Description page of Project Settings. #include "MyProject2_BP.h" #include "ColisionPawnMovementComponent.h" void UColisionPawnMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); // Make sure that everything is still valid, and that we are allowed to move. if (!PawnOwner||!UpdatedComponent||ShouldSkipUpdate(DeltaTime)) { return; } // Get (and then clear) the movement vector that we set in ACollidingPawn::Tick FVector DesireMovementthisFrame = ConsumeInputVector().GetClampedToMaxSize(1.0f)*DeltaTime*150.0f; if (!DesireMovementthisFrame.IsNearlyZero()) { FHitResult Hit; SafeMoveUpdatedComponent(DesireMovementthisFrame, UpdatedComponent->GetComponentRotation(), true, Hit); // If we bumped into something, try to slide along it if (Hit.IsValidBlockingHit()) { SlideAlongSurface(DesireMovementthisFrame, 1.f - Hit.Time, Hit.Normal, Hit); } } }
This TickComponent function makes use of a few of the powerful features offered by the UPawnMovementComponentclass.
ConsumeInputVector reports and clears the value of a built-in variable that we will use to store our movement inputs.
SafeMoveUpdatedComponent uses Unreal Engine physics to move our Pawn Movement Component while respecting solid barriers.
SlideAlongSurface handles the calculations and physics involved with sliding smoothly along collision surfaces like walls and ramps when a move results in a collision, rather than simply stopping in place and sticking to the wall or ramp.
There are more features included in Pawn Movement Components that are worthy of examination, but they are not needed for the scope of this tutorial. Looking at other classes, such as Floating Pawn Movement, Spectator Pawn Movement, or Character Movement Component, could provide additional usage examples and ideas.
5、User Interface With UMG
Work-In-Progress Code
HowTo_UMG.Build.cs
// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. using UnrealBuildTool; public class HowTo_UMG : ModuleRules { public HowTo_UMG(TargetInfo Target) { PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG" }); //PrivateDependencyModuleNames.AddRange(new string[] { }); // Uncomment if you are using Slate UI PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); // Uncomment if you are using online features // PrivateDependencyModuleNames.Add("OnlineSubsystem"); // if ((Target.Platform == UnrealTargetPlatform.Win32) || (Target.Platform == UnrealTargetPlatform.Win64)) // { // if (UEBuildConfiguration.bCompileSteamOSS == true) // { // DynamicallyLoadedModuleNames.Add("OnlineSubsystemSteam"); // } // } } }
2. Extend Our Game Mode
The menus we create will be made from User Widgets. We'll need to write a function that creates and displays a new User Widget, and then call that function when the game starts. We'll also need to keep track of what we have created so that we can remove it later. Since each project already comes with a custom GameMode class, we can simply open ours, which is defined in HowTo_UMGGameMode.h. The following functions and properties will need to be added to the bottom of the class:
public: /** Called when the game starts. */ virtual void BeginPlay() override; /** Remove the current menu widget and create a new one from the specified class, if provided. */ UFUNCTION(BlueprintCallable, Category = "UMG Game") void ChangeMenuWidget(TSubclassOf<UUserWidget> NewWidgetClass); protected: /** The widget class we will use as our menu when the game starts. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UMG Game") TSubclassOf<UUserWidget> StartingWidgetClass; /** The widget instance that we are using as our menu. */ UPROPERTY() UUserWidget* CurrentWidget;
In order to use User Widgets in our code, and add the following line to the top of the #include section:
#include "Blueprint/UserWidget.h"
Moving to HowTo_UMGGameMode.cpp now, we need to fill out the bodies of the two functions we declared. We'll start with overridingBeginPlay():
void AHowTo_UMGGameMode::BeginPlay() { Super::BeginPlay(); ChangeMenuWidget(StartingWidgetClass); }
Next, still in HowTo_UMGGameMode.cpp, we need to define how we change between menus. We will need to remove whatever User Widget we have active from the viewport, if any. Then we can create and a new User Widget and add it to the viewport.
void AHowTo_UMGGameMode::ChangeMenuWidget(TSubclassOf<UUserWidget> NewWidgetClass) { if (CurrentWidget != nullptr) { CurrentWidget->RemoveFromViewport(); CurrentWidget = nullptr; } if (NewWidgetClass != nullptr) { CurrentWidget = CreateWidget<UUserWidget>(GetWorld(), NewWidgetClass); if (CurrentWidget != nullptr) { CurrentWidget->AddToViewport(); } } }
We have built the code framework to create and display menus, and remove them when they are no longer needed. We're ready to return to theUnreal Editor and design our menu assets!
3. Create Menu Widget Blueprints
In Unreal Editor, we can press the Compile button to build our new code. This will allow us to use User Widgets as menus.
We will now create the User Widgets that our GameMode will use as menus. This is done with the Add New button in the Content Browser. The Widget Blueprint class is found in the User Interface category. We need to create two of these, one named MainMenu and one named NewGameMenu. Our game will begin at the Main Menu, and will have an option to proceed to the New Game Menu.
Double-clicking the MainMenu Widget we've just created will take us to the Blueprint Designer, where we can create our menu layout.
Let's drag a Button and a Text from the Common section of the Palette Panel to the Graph. This Button will eventually be used to open the New Game Menu.
The first step in making our layout look right is to adjust the location and size of our Button. We should make the following changes:
Set the size to 200x200.
Set the position to (200, 100).
Rename it NewGameButton, just to make it easier to recoginze when we hook up functionality to it later on.
Since we're not drawing a custom set of images for the Button, we can label it by dragging and dropping the Text Block onto it and making the following changes:
Set the text to say New Game.
Change the Visibility to Hit Test Invisible. This will prevent the Text Block from intercepting mouse-clicks that are intended for the Button underneath.
Set the name to NewGameText. This is not needed, but it's a good habit.
Next, we'll want to make a "Quit" feature with a second Button and Text Block. Set those up in the same way as the New Game Button and Text Block except the following changes.
Set the name of the Button to QuitButton
Set the position of the button to 600, 100
Set the name of the Text Block to QuitText
After that, we can add Events to our Buttons so that we can run code when a Button is clicked. This is done by locating and pressing the + next to the appropriate Event name in the Details Panel. In this case, OnClicked is the event we want to use. Create this event for both the NewGameButton amd QuitButton Widgets.
For the Event called OnClicked(NewGameButton), we'll want to:
Connect a ChangeMenuWidget node to use the function we added to our GameMode earlier.
Set the New Widget Class field on the ChangeMenuWidget node to the NewGameMenu asset.
For the OnClicked(QuitButton) Event, we'll want to:
Connect a Quit Game node.
4. Configure Our Game Mode
With our main menu built, we can set up a GameMode asset that will load it as soon as the level starts.
In the Content Browser, we will add a Blueprint Class based on our project's GameMode. This makes it possible to set the exposed variables on those two classes to whatever values we want. To do this:
Click the Add button in the Content Browser.
Pick HowTo_UMGGameMode as the parent class. It will be listed in the All Classes section.
Name the resulting Blueprint asset MenuGameMode.
In order to see our mouse cursor in-game, we'll need to create a Blueprint of the PlayerController as we did with our GameMode.
Click the Add button in the Content Browser again.
Select Player Controller from the Common Classes section.
Name the Blueprint MenuPlayerController.
Edit MenuPlayerController.
Check the Show Mouse Cursor box.
Edit MenuGameMode.
The Starting Widget Class must be set to the MainMenu asset in order to bring up our menu when the game begins.
The Default Pawn Class should be set to Pawn instead of DefaultPawn so that our player will not be able to fly around while in the menu.
The Player Controller Class should be set to the MenuPlayerController asset we created so that the mouse cursor will be shown in-game.
In order for our Blueprint to be used, we must return to the Level Editor window and change the World Settings for our current Level via the Settings button.
The World Settings Panel will open up. By default, it will be docked with the Details Panel, but it can be moved elsewhere. We need to set the Game Mode Override field to our MenuGameMode asset.
Our custom GameMode asset is now in effect on our level, configured to load our Main Menu, and use our Player Controller that shows the mouse cursor. If we run the game now, our Quit button will work as expected, and our New Game button will take us to an empty menu screen. Our next step will be to set up the New Game menu.
5. Build A Second Menu
In the Content Browser, find and open the NewGameMenu asset we created earlier. This menu will contain a name-entry Text Box, aButton to play the game that cannot be pressed until a name is entered, and a Button to return to the main menu.
To create the name entry box, we'll drag a Text Box (not a Text Block) into the layout.
The Text Box should be configured with the following values:
Change the name to NameTextEntry
Position is (325, 200). This leaves room for a Text Block placed to the left of the Text Box.
Size is 250x40.
Font Size (under the "Style" heading) is 20.
We can create the Play Game Button with a Text Block label the same way we created the Buttons on the previous menu.
For the Button: Change the name to PlayGameButton, Position to 200, 300, Size to 200, 100
For the Text Block: Change the name to PlayGameText, set Visibility to Hit Test Visible, and drag it on top of the PlayGameButton
The Play Game Button will have a special feature - it will be enabled only when the name entered in the Text Box is not empty. We can use Unreal Motion Graphics' (UMG) bind feature to create a new function for the Is Enabled field (under the Behavior section).
To ensure that the Button is enabled if and only if the Text Box is not empty, we can convert the text from the Text Box to a string and then check that its length is greater than zero. Here is how that logic would appear:
Let's add one more Button so we can back out and get to our Main Menu from here. This will be just like the Play Game Button from our Main Menu, but it will be positioned relative to the lower-right corner instead of the upper-left. To accomplish this, click the Anchorsdropdown in the Details Panel for the Button and find the appropriate graphical representation in the pop-up menu.
Change the name to MainMenuButton
Set the Position to -400, -200
Set the Size to 200x100
We will now add scripting to our new Buttons by once again adding OnClicked events. The Main Menu Button will simply reload the Main Menu Widget, while the Play Game Button will deactivate our menu entirely by having no new Widget provided in the call to ourChangeMenuWidget function. This is shown by the phrase Select Class being displayed instead of the name of an actual class or asset.
We should now have two screens that look roughly as follows: